How to implement JSON:API using Fractal

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 Fractal-specific steps

  • composer require spatie/laravel-fractal
  • composer require spatie/laravel-query-builder
  • php artisan make:transformer ArticleTransformer
  • php artisan make:transformer AuthorTransformer
  • php artisan make:transformer CommentTransformer
  • Customize ArticleTransformer:
    protected $availableIncludes = [
        'author',
        'comments',
    ];
    
    public function transform(Article $article)
    {
        return [
            'id' => (int) $article->id,
            'title' => $article->title,
            'created_at' => $article->created_at->format('c'),
        ];
    }
    
    public function includeAuthor(Article $article)
    {
        return $this->item($article->author, new AuthorTransformer, 'authors');
    }
    
    public function includeComments(Article $article)
    {
        return $this->collection($article->comments, new CommentTransformer, 'comments');
    }
  • Customize CommentTransformer:
    protected $availableIncludes = [
        'author',
        'article',
    ];
    
    public function transform(Comment $comment)
    {
        return [
            'id' => (int) $comment->id,
            'body' => $comment->body,
        ];
    }
    
    public function includeAuthor(Comment $comment)
    {
        return $this->item($comment->author, new AuthorTransformer);
    }
    
    public function includeArticle(Comment $comment)
    {
        return $this->item($comment->article, new ArticleTransformer);
    }
  • php artisan vendor:publish --provider="Spatie\Fractal\FractalServiceProvider"
  • Edit config/fractal.php and set serializer:
    'default_serializer' => \League\Fractal\Serializer\JsonApiSerializer::class,
  • php artisan make:controller "Api\ArticleController"
  • php artisan make:controller "Api\ArticleCommentController"
  • Bind in routes/api.php:
    Route::get('articles', 'Api\ArticleController@index');
    Route::get('articles/{article}', 'Api\ArticleController@show');
    Route::get('articles/{article}/comments', 'Api\ArticleCommentController@index');
  • Update your ArticleController:
    public function index()
    {
        $articles = QueryBuilder::for(Article::class)
            ->allowedIncludes(['author', 'comments'])
            ->allowedSorts(['created_at', 'title'])
            ->paginate();
    
        return fractal($articles, new ArticleTransformer)->withResourceName('articles')->respond();
    }
    
    public function show($articleId)
    {
        $article = QueryBuilder::for(Article::class)
            ->allowedIncludes(['author', 'comments'])
            ->findOrFail($articleId);
    
        return fractal($articles, new ArticleTransformer)->withResourceName('articles')->respond();
    }
  • Update your ArticleCommentController:
    public function index($articleId)
    {
        $comments = QueryBuilder::for(Comment::class)
            ->allowedIncludes(['author', 'article'])
            ->where('article_id', $articleId)
            ->paginate();
    
        return fractal($comments, new CommentTransformer)->withResourceName('comments')->respond();
    }
    
  • Fractal macro for responding with the right content type:
    // In a service provider:
    Fractal::macro('respondJsonApi', function ($statusCode = 200) {
        return $this->respond($statusCode, [
            'Content-Type' => 'application/vnd.api+json',
        ]);
    });
    
    // In use:
    return fractal($comments, new CommentTransformer)
        ->withResourceName('comments')
        ->respondJsonApi();
  • @todo turn this into a real full post

Subscribe

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