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 Laravel Package

nilportugues/json-api

A PHP package for building JSON API (jsonapi.org) compliant responses, with Laravel support. It transforms Eloquent models and collections into standardized resources, handling includes, pagination, links, meta and error objects so your API follows the spec without manual serialization.

View on GitHub
Deep Wiki
Context7

Getting Started

  1. Install the package and a PSR-7 implementation. The package only produces PSR-7 responses, it does not ship one. On modern Laravel you'll typically pull in nyholm/psr7 (or guzzlehttp/psr7) since the original dependency, Zend Diactoros, is abandoned.

    composer require nilportugues/json-api nyholm/psr7
    
  2. Look at the base serializer first. nilportugues/json-api is a thin JSON API layer over nilportugues/serializer. The Mapping abstract class (aliases, properties, relationships, URLs) lives in that base package — read its README before touching the JSON API classes. The JSON API package itself only adds the transformer + response wrapper.

  3. Create a mapping per resource. The mapping is the single source of truth for what gets serialized.

    use NilPortugues\Serializer\Mapping;
    use NilPortugues\Serializer\Relationship;
    
    class BookMapping extends Mapping
    {
        public function getClass()       { return Book::class; }
        public function getAlias()       { return 'books'; }        // becomes JSON API "type"
        public function getId()          { return 'id'; }
        public function getProperties()  { return ['id', 'title', 'author']; }
        public function getAttributes()  { return ['title']; }
        public function getRelationships() {
            return ['author' => fn() => new Relationship('author', 'authors', AuthorMapping::class)];
        }
        public function getUrl()         { return '/books/{id}'; }  // self link template
    }
    
  4. Wire up the transformer and return a response. In a controller:

    use NilPortugues\Api\JsonApi\JsonApiTransformer;
    use NilPortugues\Api\JsonApi\Http\Message\Response;
    use Nyholm\Psr7\Stream;
    
    $transformer = new JsonApiTransformer([Book::class => BookMapping::class]);
    
    $stream = new Stream(fopen('php://temp', 'wb+'));
    $response = new Response($stream, $transformer->transform($book));
    
    return $response->withHeader('Content-Type', 'application/vnd.api+json');
    

    Verify the exact Response constructor signature in your version — it's a thin ResponseInterface decorator, so you can also build it manually and just write json_encode($transformer->transform($book)) into the body.

  5. First use case: a GET /books/{id} endpoint that returns a single resource with one belongsTo relationship (author). Once that round-trips and included shows the author, you've validated the mapping + transformer + response wiring.

Implementation Patterns

  • One mapping class per Eloquent model. Keep them in app/JsonApi/Mappings and register them all in a single service provider:

    $this->app->singleton(JsonApiTransformer::class, fn () => new JsonApiTransformer([
        Book::class  => BookMapping::class,
        Author::class => AuthorMapping::class,
    ]));
    

    Inject the transformer into controllers — building it per-request re-parses mappings each time.

  • Type == alias. JSON API's type field comes straight from getAlias(). Treat it as your API contract and don't rename it casually; clients key off it for relationships and included lookups.

  • Relationships drive included. Only serialized relationships become compound documents. Use ?include=author-style query handling yourself (the package doesn't parse request params) and conditionally populate getRelationships() — or eager-load and let everything be included. There is no built-in sparse-fieldset support; add it by filtering getProperties()/getAttributes() based on fields[books].

  • Controller pattern. Keep three responsibilities split: (a) fetch/authorize model, (b) transform, (c) wrap in PSR-7. A base ApiController with respond($data) and respondCollection($data, $meta) methods avoids repeating the stream boilerplate in every action.

  • Collections. Pass an array/Collection to transform(); results land in data as an array of resource objects. If you need pagination links/meta, either use the sibling serializer's pagination helpers or build links yourself and merge into the document array after transformation.

  • Errors. The package is not opinionated here. Return 4xx/5xx as a JSON API errors object manually ({"errors":[{"status":"404","title":"..."}]}) with the correct application/vnd.api+json content type; don't let Laravel's default {message: ...} leak through on API routes.

  • Testing. Assert against the decoded array rather than raw JSON strings: check data.type, data.id, data.attributes, and that related resources live under included with matching type/id. Snapshot the response for regressions when you touch mappings.

Gotchas and Tips

  • 2018 vintage — dependency friction is the #1 issue. The package targets PHP 7.x, PSR-7 v1, and zendframework/zend-diactoros. On Laravel 10/11 with PSR-7 v2 you may hit composer conflicts. Workarounds: use nyholm/psr7 (v2-compatible), or pin psr/http-message in a dedicated resolver. If you can't resolve, the transformer is still usable — just call transform() and encode the result yourself instead of using the package's Response.

  • Response needs a writable stream. fopen('php://temp', 'wb+') is the classic trick; passing an already-consumed or read-only stream produces empty bodies that look like serialization bugs. When debugging "empty response", check the stream first.

  • N+1 queries. The transformer walks every property and relationship. Eager-load relationships in the controller (Book::with('author')->find(...)) or you'll issue a query per resource per relationship. This is the most common production surprise.

  • included is not deduplicated by default in some versions when the same related resource appears under multiple parents. If clients complain about duplicate type/id entries, dedupe defensively before emitting.

  • getId() must return a property name, not a value. A common mistake is returning $this->id in the mapping — the mapping is instantiated without the model, so you must return the name of the property that holds the id.

  • Lazy relationships via closures. Using fn() => new Relationship(...) in getRelationships() lets you conditionally register relationships per request (useful for ?include=), but it also means the mapping is instantiated cheaply and relationships are resolved only when serialized — don't rely on side effects in those closures.

  • URL templates. getUrl() returning '/books/{id}' is used to build the self link. If responses unexpectedly omit links.self, it's usually a missing/incorrect template or a property mismatch with getId().

  • Content-Type matters to spec-compliant clients. Always set application/vnd.api+json; Ember Data and similar clients reject application/json. Also handle 406/415 if clients send Accept or Content-Type with application/vnd.api+json; ext=... — the package won't do this for you.

  • Debug before the PSR-7 wrapper. When output looks wrong, dd($transformer->transform($model)) — this is a plain array and far easier to inspect than a consumed stream.

  • Extension point for polymorphism. For models with inheritance, register a mapping per concrete subclass (Child::class => ChildMapping::class) and ensure getAlias() differs, otherwise JSON API types collide.

  • No maintenance means vendor it. Given the last release date, treat the transformer as a reference implementation: if you need sparse fieldsets, sorting, filtering, or spec-compliant error handling, plan to fork or wrap rather than wait for upstream fixes.

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