Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Json Api Server Laravel Package

tobyz/json-api-server

A JSON:API server for Laravel that turns Eloquent models into spec-compliant APIs with resource classes, relationship includes, filtering, sorting, pagination, and validation. It streamlines building standardized JSON:API endpoints.

View on GitHub
Deep Wiki
Context7

Getting Started

Minimal steps:

  1. composer require tobyz/json-api-server at an explicit version (1.0.0-rc.1 as of this release) plus a PSR-7/PSR-17 implementation (nyholm/psr7 is the least friction if your framework doesn’t already give you PSR-7).
  2. Instantiate the API with your public base URL — links (self, pagination, related) are generated from it. Optionally add API-level metadata with JsonApi::meta().
  3. Register one resource type: its type, its ID field (in 1.0 this is $resource->id() returning an Id field, not a getId() method), attributes, relationships, links, and the data callbacks that fetch/persist records.
  4. In your framework entry point, convert the incoming request to a PSR-7 ServerRequestInterface, call the API, then copy the PSR-7 response back into your framework’s response object. Use Context::createResponse() for any response you build by hand — it injects the jsonapi object for you.

The first use case to ship is still a read-only articles endpoint (list + show) with one relationship, because it exercises the parts you’ll rely on daily: include, sparse fieldsets, sorting, and pagination. Then immediately generate the OpenAPI schema from the same definitions — that documentation is the quickest way to prove the schema is right.

Illustrative bootstrap — verify exact method names against the README for your tagged version, since the DSL has changed significantly in 1.0:

use Tobyz\JsonApiServer\JsonApi;
use Nyholm\Psr7\ServerRequest;

$api = new JsonApi('https://example.com/api');
$api->meta(['version' => '1.0']);

$api->resource('articles', function ($resource) {
    $resource->id();                    // Id field: type, validation, writability
    $resource->attribute('title');
    $resource->attribute('body');
    $resource->relationship('author', 'users');

    // ->links([...]) for custom resource-level links
    // wire read/write callbacks to your repository here
});

$response = $api->handle(ServerRequest::fromGlobals());

Where to look first:

  • README for the resource/field registration DSL and supported query parameters.
  • src/Schema for field, type, ID, link, and relationship primitives — this is where the “automation” lives.
  • Endpoint classes (Index, Show, Create, Update, Delete) — 1.0 refactored these into separate *Resource and *Relationship endpoints with hooks (headers(), response(), schema(), saved()).
  • Serializer / document classes for how data, included, links, and meta are assembled.
  • OpenAPI generation for the introspection side of the same schema — treat generated output as a second view of your field definitions.
  • tests/ — for a package this size, the tests are the most reliable documentation of edge-case behavior (sparse fieldsets, empty includes, pagination links).

Implementation Patterns

One schema registration per JSON:API type. Keep attributes and relationships declarative; don’t put query logic inside the schema. The schema is the contract that powers serialization, sparse fieldsets, validation, and OpenAPI output.

Model IDs as a first-class field. Define IDs with $resource->id() instead of a method: it carries type constraints (string/integer), custom validation rules, getter/setter callbacks, and — the one you’ll reach for most — writableOnCreate() for client-generated IDs (UUID/ULID). Treat this as the single place ID policy lives.

Data providers behind the schema. Wire each resource to repository methods or callbacks. Return plain arrays/objects and let the serializer shape the document. This keeps the JSON:API layer thin and testable independently of your data layer.

Declare relationships to get compound documents for free. Once a relationship target is registered, ?include=author should produce a proper included array. Eager-load those relations in your provider. Use linkageMeta() when a resource identifier needs extra metadata.

Let endpoints validate query parameters. 1.0 lets endpoints and pagination implementations declare their query parameters, which drives both validation (400s instead of 500s) and schema generation. Prefer that over hand-rolled checks in controllers.

Whitelist query features. Sparse fieldsets, sorting, filtering, and pagination are driven by query params. Configure allowed sort fields and filters per resource rather than passing user input through to SQL.

Hook into the write lifecycle. Create::saved() / Update::saved() run after the model/fields are saved but before serialization — the right place for audit logging, tenant stamping, and event dispatch. Context::$data gives you the resolved create/update data inside callbacks.

Customize responses at the endpoint, not after the fact. Endpoint::headers() (with the Header schema class) and Endpoint::response() replace post-processing the PSR-7 response in your controller. Context::createResponse() covers ad-hoc responses and always adds the jsonapi member.

Errors as configuration. Specific exception classes now exist for every error, and JsonApi::errors([...]) registers overrides keyed by exception class name, with :key placeholders in detail. That lets you centralize error copy/translation rather than wrapping every callback in try/catch. For validation, convert failures into errors[] with source.pointer (e.g. /data/attributes/title); an exception now represents a single error, so wrap multiple validation failures in JsonApiErrorsException.

Async and redirect flows are built in. Create::async() returns 202 Accepted (pair with Show::seeOther() → 303 for the polling redirect). Use these only if your clients can handle the contract; otherwise standard 201 + Location is simpler.

Laravel integration is manual. There is no first-party Laravel adapter; it’s a framework-agnostic PSR-7 package. The reliable pattern:

  • Bind the configured API instance as a singleton in a service provider.
  • Add a catch-all route or controller that builds a PSR-7 request from the Laravel request (symfony/psr-http-message-bridge + nyholm/psr7), calls the API, then copies status, headers, and body onto a Laravel Response.
  • Register exception handling around the call so failures become JSON:API error documents, not Laravel HTML error pages.

Auth is your job. JSON:API says nothing about authentication. Enforce it before serialization, and for field-level visibility filter or reject attributes per user. A policy check per resource/action plus an attribute visibility filter covers most needs.

Testing is clean because the entry point is a PSR-7 request. Construct a ServerRequest with Accept: application/vnd.api+json and assert on the decoded body. That’s usually faster and more representative than going through the full framework stack. Test generated OpenAPI separately from runtime behavior — they can drift.

Gotchas and Tips

  • 1.0-rc.1 is a large breaking release — pin it and read the notes. The migration checklist: Resource::getId() → Resource::id(); getJsonApiErrors() → getJsonApiError() (singular); Exception\Concerns\SingleError → JsonApiError with fluent source()/meta()/links() instead of set*(); Extension\Atomic → Extension\Atomic\Atomic; json_api_response() removed; Context::queryParam() removed; Pagination\Concerns\BuildsUrls removed.
  • Context::queryParam() → Context::parameter(). Use parameter() for parameters your endpoints declare (validated), and Context::$request->getQueryParameters() only when you genuinely need raw access — it bypasses validation.
  • json_api_response() → Context::createResponse(). Don’t hand-build PSR-7 JSON responses; you’ll forget the jsonapi member or the media type.
  • BuildsUrls → Context::currentUrl(array $params = []). Any custom pagination or link code written against the removed trait needs rewriting; currentUrl() also gives you query-param overrides for free.
  • One exception = one error now. If your getJsonApiError() returns an array of errors, wrap them in JsonApiErrorsException instead. This is the most likely upgrade regression in write validation.
  • ToOne custom deserializers now receive the resolved model, not the raw relationship object — matching ToMany. Silent type errors are likely if you have custom deserializers.
  • Show and Update are now aggregates of *Resource and *Relationship endpoints. If you overrode or hooked Show/Update, check whether your customization needs to apply to both halves.
  • Profiles change the response Content-Type. Cursor pagination activates https://jsonapi.org/profiles/ethanresnick/cursor-pagination automatically; Context::activateProfile() adds yours. Clients doing strict media-type string comparison will break — negotiate on type/subtype plus parameters, or document the profile.
  • Content negotiation is strict. Clients should send Accept: application/vnd.api+json. A sloppy application/json may be rejected or may not produce the correct media type on the response. Set the response Content-Type explicitly.
  • IDs are strings on the wire. The new Id field lets you constrain the type (integer, string, etc.) and support client-generated IDs, but serialized IDs remain strings. Don’t leak raw integers from your provider.
  • The base URL is load-bearing. Pagination, relationship, and self links are absolute. A wrong base URL breaks clients that follow links even when the payload looks fine.
  • Sparse fieldsets can empty a resource. fields[articles]= should still emit a valid resource object with id and type only. Check that your serializer handles empty attribute sets without null errors.
  • Includes and sparse fieldsets interact per type. Included users are governed by fields[users]=..., not by the primary resource’s fieldset. It’s a common source of “missing field” bug reports.
  • N+1 queries hide in includes. The automated relationship resolution will fetch whatever you declared. Batch/eager-load in the data provider, or a single ?include= request can fan out badly.
  • Sorting and filtering must be whitelisted. Passing raw query params into SQL is both a security problem and a source of generic 500s instead of 400s. Endpoint-declared parameters help here.
  • Status codes matter. Create → 201 with a Location header; delete → 204 with no body; update → 200 with the updated resource. Async create → 202; Show::seeOther() → 303. Don’t return 200 with an empty envelope for a delete, and don’t return 303 to clients expecting 200.
  • Pagination needs defaults and a max. The package won’t guess your page[size] limits. Set a default page size and a hard cap, verify link round-tripping, and check Page::$rangeTruncated behavior for cursor pagination.
  • Relationship endpoints are separate from included relationships. /articles/1/relationships/author is a different surface from ?include=author — and in 1.0 it’s literally a different endpoint class. Decide which ones you expose and test them independently.
  • Type schemas are richer than they look. nullable(), Obj, Any, AnyOf, AllOf, OneOf, Not feed both validation and OpenAPI generation. A permissive Any keeps validation quiet but produces useless docs; be precise where clients codegen.
  • Ecosystem is small (~70 stars). Treat the source and tests as the spec. Pin the version (1.0.0-rc.1 is a release candidate, not semver-stable), read the changelog before upgrading, and expect the DSL to keep evolving. The last release date has looked like a placeholder/future timestamp — confirm the actual tagged version you’re installing.
  • Extension points worth knowing: custom field/type classes for domain value objects, Resource::links() and Schema\Link for rich link objects, JsonApi::meta() for API-level metadata, serializer overrides for document-level meta, endpoint headers()/response()/schema() hooks, saved() callbacks for post-write work, and JsonApi::errors() for centralized error shaping. Field::location() is handy when writing your own introspection or serialization code.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect
youbar/easy-crud
c-point-labs/laravel-hostyourai
dbravoan/dbcms-admin-theme-dbcms
dbravoan/dbcms-theme-db-cyber
dbravoan/dbcms-blocks-basic
dbravoan/dbcms-module-content-audit
dbravoan/dbcms-module-ai-assistant
paygate-to/paygate-php
padosoft/askmydocs-connector-base
defstudio/supercharged-carbon
bernskioldmedia/laravel-matomo