How to implement JSON:API using Eloquent Resources

This is a rough draft of a guide I'll flesh out after Laracon Online as I continue to develop the best pattern for this.

See a practical (incomplete) example of this on my "JSON-API Examples" GitHub repo.

Prep (same across all implementations)

  • laravel new myproject && cd myproject
  • php artisan make:model Article -mfs
  • php artisan make:model Comment -mfs
  • add guarded = ['id'] to both models
  • Add article() { return $this->belongsTo(Article::class); } to Comment
  • Add author() { return $this->belongsTo(User::class, 'author_id'); } to Comment
  • Add comments() { return $this->hasMany(Comment::class); } to Article
  • Add author() { return $this->belongsTo(User::class, 'author_id'); } to Article
  • Add comments() { return $this->hasMany(Comment::class); } to User
  • Add article() { return $this->hasMany(Article::class); } to User
  • Modify comments migration, to add:
    $table->text('body');
    $table->unsignedBigInteger('article_id');
    $table->foreign('article_id')->references('id')->on('articles');
    $table->unsignedBigInteger('author_id');
    $table->foreign('author_id')->references('id')->on('users');
  • Modify articles migration, to add:
    $table->string('title');
    $table->text('body');
    $table->unsignedBigInteger('author_id');
    $table->foreign('author_id')->references('id')->on('users');
  • Factories @todo

Rough overview of the Eloquent Resource specific steps, if you're ignoring the JSON:API flat include spec

  • php artisan make:resource Article
  • php artisan make:resource ArticleCollection
  • php artisan make:resource Comment
  • php artisan make:resource CommentCollection
  • php artisan make:controller "Api\ArticleController"
  • php artisan make:controller "Api\ArticleCommentController"
  • Add routes to routes/api.php:
    Route::get('articles', 'Api\ArticleController@index');
    Route::get('articles/{article}', 'Api\ArticleController@show');
    Route::get('articles/{article}/comments', 'Api\ArticleCommentController@index');
  • composer require spatie/laravel-query-builder
  • Create two traits: ParsesIncludes and ReturnsJsonApi.
  • Code for ParsesIncludes:
    trait ParsesIncludes
    {
        public function requestedIncludes($request)
        {
            if (! $request->input('include')) {
                return collect([]);
            }
    
            $includes = collect(explode(',', $request->input('include')));
    
            $includes->each(function ($include) {
                if (! in_array($include, $this->allowedIncludes)) {
                    throw new Exception("Invalid include requested: {$include}");
                }
            });
    
            return $includes;
        }
    }
  • Code for ReturnsJsonApi:
    trait ReturnsJsonApi
    {
        public function withResponse($request, $response)
        {
            $response->header('Content-Type', 'application/vnd.api+json');
        }
    }
  • Edit your article resource to look like this:
    use ParsesIncludes, ReturnsJsonApi;
    
    public $allowedIncludes = [
        'author',
        'comments',
    ];
    
    public function toArray($request)
    {
        return [
            'type' => 'articles',
            'id' => $this->id,
            'attributes' => [
                'title' => $this->title,
                'body' => $this->body,
                'created_at' => $this->created_at->format('c'),
                'updated_at' => $this->created_at->format('c'),
            ],
            $this->mergeWhen($this->requestedIncludes($request)->isNotEmpty(), [
                'relationships' => $this->relationships($request),
            ]),
        ];
    }
    
    public function relationships()
    {
        // @todo, this is complicated -- see the tightenco/json-api-examples repo
        // at the eloquent-resources branch, and I'll update this article ASAP
    }
    
    public function with()
    {
        // @todo, this is complicated -- see the tightenco/json-api-examples repo
        // at the eloquent-resources branch, and I'll update this article ASAP
    }
    Check out the sample json-api-examples repo to see how I'm trying to make relationships work, and how I'm trying to make the included section work via the with method.
  • Edit your article collection resource to look like this:
    use ParsesIncludes, ReturnsJsonApi;
    
    public $allowedIncludes = [
        'author',
        'comments',
    ];
    
    public funtion toArray($request)
    {
        return [
            'data' => parent::toArray($request),
            $this->mergeWhen($this->requestedIncludes($request)->isNotEmpty(), [
                'included' => $this->included($request),
            ]),
        ];
    }
    
    public function included($request)
    {
        // I haven't finished writing this method yet. Check back soon :)
        $includes = $this->requestedIncludes($request);
    
        if ($includes->isEmpty()) {
            return [];
        }
    
        $included = [];
    
        if ($includes->contains('author')) {
            // @todo all authors, not just one.
            // $included[] = new User($this->author);
        }
    
        // @todo flat map comments across all
        return ['@todo'];
    }
  • Do the same for the comment resource and comment collection resource, but modify the includes and transformers appropriately.
  • Finally, let's build the controllers, using Spatie's Laravel-Query-Builder to do most of the hard work. First, the ArticleController:
    public function index()
    {
        $articles = QueryBuilder::for(Article::class)
            ->allowedIncludes(['author', 'comments'])
            ->allowedSorts(['created_at', 'title'])
            ->paginate();
    
        return new \App\Http\Resources\ArticleCollection($articles)
    }
    
    public function show($articleId)
    {
        $article = QueryBuilder::for(Article::class)
            ->allowedIncludes(['author', 'comments', 'comments.author'])
            ->allowedSorts(['created_at', 'title'])
            ->findOrFail($articleId);
    
        return new \App\Http\Resources\Article($article);
    }
  • And next, the ArticleCommentController:
    public function index($articleId)
    {
        $comments = QueryBuilder::for(Comment::class)
            ->allowedIncludes(['author', 'article'])
            ->where('article_id', $articleI)
            ->paginate();
    
        return new CommentCollection($comments)
    }

With this complete, you have the basics of a Laravel-JSON-API structure. You haven't handled errors or complex content negotation, and your includes aren't done yet. You can either dig into flat includes yourself or using something like Fractal, or you can choose to just embed your includes into the relationships array--your call.

If you want to do flat included...

First, I'd recommend considering Fractal or Laravel-JSON-API instead. But if you really want to go down this road, check out the eloquent-resources branch of my json-api-examples repo, where I've started the process of trying to figure out how to do that.

Subscribe

For quick links to fresh content, and for more thoughts that don't make it to the blog.