in Laravel, Programming

Laravel API Resources + JSON API Spec

Laravel 5.5 launched with API Resources, a new feature for building APIs. API Resources serve as a way to transform models and collections to JSON formatted API responses. It standardizes how to define the output format of models and includes helper methods for common tasks like customizing metadata, headers and pagination. You can read how to write API Resources in the Laravel docs.

JSON API is a spec created from common conventions for building JSON powered APIs. The goal of JSON API spec is to help teams avoid wasting time debating API structure and features. JSON API encourages RESTful design, readability/discoverability, and object relationships through a flat document structure. There’s examples for almost every feature on the Specification page, and you can learn a lot about why JSON API is designed the way that it is by reading through the FAQ page.

JSON API was considered for the default format of API Resources, however in the end it was decided to be too much work for the average use-case of a Laravel-powered API. For this reason, API Resources don’t automatically output to JSON API format – but it is very similar, even including the same root offsets. In this article we’ll take a look at what it takes to make API Resources fully JSON API compliant.

Making API Resources JSON API Compliant

API Resources use the following same root offsets as JSON API: data, meta and links.

  • data – the primary resource (or collection of resources)
  • meta – additional information about a resource that is not an attribute or relationship
  • links linkage eg. “self” link or pagination urls

With Laravel’s API Resources, you can generate two types of classes: resources and resource collections. A resource class is for outputting a single model you pass to it, and a resource collection is for multiple models (eg. a paginated list of items). A resource, without customizing it, will just call toArray() on the model and place the result under the data offset. A resource collection will do the same for each model, and add them as an array to the data offset. A resource collection will also automagically add pagination information to the meta and links offsets.

API Resources don’t however include automation for JSON API’s compound documents and their relationships / included offsets; a requirement in JSON API. Let’s take a look at at how we can write a resource and a resource collection to comply with the JSON API spec. We’ll use a sales platform concept for examples where we have orders of products, and for simplicity we’ll say an order can only be for one or more of a single product.

Resources

First let’s take a look at an endpoint which returns a single resource (eg. /orders/1). In this example we have an Order model, and each Order has a one-to-one relationship with a Product. By default when a model is passed to a resource class like OrderResource, it outputs a response in the structure of the one on the left in the table below. Compare this to the equivalent response in JSON API format on the right:

API Resource (Default) JSON API Spec (End Goal)
{
  "data": {
    "id": 1,
    "order_id": 246752514,
    "product_id": 2,
    "quantity": 9,
    "created_at": "2009-12-22 21:40:02",
    "updated_at": "2017-08-18 22:07:53",
    "product": {
      "id": 2,
      "name": "Nike Shoes",
      "created_at": "2017-08-17 19:49:32",
      "updated_at": "2017-08-17 19:49:32"
    }
  }
}
{
  "data": {
    "type": "order",
    "id": "1",
    "attributes": {
      "order_id": 246752514,
      "quantity": 9,
      "created_at": 1261518002,
      "updated_at": 1503094073
    },
    "relationships": {
      "product": {
        "data": {
          "type": "product",
          "id": "2"
        }
      }
    }
  },
  "included": [
    {
      "type": "product",
      "id": "2",
      "attributes": {
        "name": "Nike Shoes",
        "created_at": 1502999372,
        "updated_at": 1502999372
      }
    }
  ]
}

Note: If you are new to JSON API, it may appear that JSON API’s version of the output is at a disadvantage in response size, but keep in mind the product may not be included by default. In addition, JSON API is providing added clarity that the related product’s data will not be able to be PATCH’ed on this endpoint; it should instead be updated via it’s own resource’s endpoint (eg. PATCH /products/6). You could add a links offset to the product object with a self url to further encourage discoverability.

How to write Resource classes to output to JSON API format

After creating the resource class, update the toArray() function to serialize the model in the correct format. This includes setting the type and id fields required by JSON API, and putting the rest of the resource’s data under attributes.  You can also add a with()function to include any additional root offsets. We’ll use with() to add the related Product resource in the included offset. Here’s an example of what the OrderResource class would look like:

class OrderResource extends Resource
{
   public function toArray($request)
   {
       return [
           'type' => 'order',
           'id' => (string) $this->id,
            'attributes' => [
                'name' => $this->name,
                'created_at' => $this->created_at,
                'updated_at' => $this->updated_at,
           ],
           'relationships' => [
              'product' => [
                  'data' => [
                      'type' => 'product',
                      'id' => (string) $this->product_id
                   ]
              ]
           ]
        ];
    }

    public function with($request)
    {
        return ['included' => [new ProductResource($this->product)]];
    }
}

Now when you use the OrderResource you’ll automatically get JSON API compliant output.

Resource Collections

Now let’s take a look at endpoints that return a collection of resources (eg. /orders?page=2). On the left we have the the default output from a ResourceCollection class OrderCollection, and on the right what the equivalent would look like following JSON API format:

ResourceCollection (Default) JSON API Spec (End Goal)
{
  "data": [
    {
      "id": 18,
      "order_id": 344501705,
      "product_id": 6,
      "quantity": 2,
      "created_at": "2010-01-22 03:36:03",
      "updated_at": "2015-07-23 12:19:02",
      "product": {
        "id": 6,
        "name": "Tommy Jersey",
        "created_at": "2017-08-17 19:49:32",
        "updated_at": "2017-08-17 19:49:32"
      }
    },
    {
      "id": 19,
      "order_id": 446228260,
      "product_id": 6,
      "quantity": 2,
      "created_at": "2010-01-22 07:23:13",
      "updated_at": "2010-01-22 07:25:52",
      "product": {
        "id": 6,
        "name": "Tommy Jersey",
        "created_at": "2017-08-17 19:49:32",
        "updated_at": "2017-08-17 19:49:32"
      }
    }
  ],
  "links": {
    "first": "http://api.dev/orders?page=1",
    "last": "http://api.dev/orders?page=330",
    "prev": "http://api.dev/orders?page=6",
    "next": "http://api.dev/orders?page=8"
  },
  "meta": {
    "current_page": 7,
    "from": 13,
    "last_page": 330,
    "path": "http://api.dev/orders",
    "per_page": "2",
    "to": 14,
    "total": 659
  }
}
{
  "data": [
    {
      "type": "order",
      "id": "18",
      "attributes": {
        "order_id": 344501705,
        "quantity": 2,
        "created_at": 1264131363,
        "updated_at": 1437653942
      },
      "relationships": {
        "product": {
          "data": {
            "type": "product",
            "id": "6"
          }
        }
      }
    },
    {
      "type": "order",
      "id": "19",
      "attributes": {
        "order_id": 446228260,
        "quantity": 2,
        "created_at": 1264144993,
        "updated_at": 1264145152
      },
      "relationships": {
        "product": {
          "data": {
            "type": "product",
            "id": "6"
          }
        }
      }
    }
  ],
  "links": {
    "first": "http://api.dev/orders?page=1",
    "last": "http://api.dev/orders?page=330",
    "prev": "http://api.dev/orders?page=6",
    "next": "http://api.dev/orders?page=8"
  },
  "meta": {
    "current_page": 7,
    "from": 13,
    "last_page": 330,
    "path": "http://api.dev/orders",
    "per_page": "2",
    "to": 14,
    "total": 659
  },
  "included": [
    {
      "type": "product",
      "id": "6",
      "attributes": {
        "name": "Tommy Jersey",
        "created_at": 1502999372,
        "updated_at": 1502999372
      }
    }
  ]
}
How to write ResourceCollection classes in JSON API format

For endpoints that return a collection of resources, we can reuse the individual Resource classes like the one we wrote in the previous section and map each model to it. For including related resources, keep in mind the benefit of having them under the included offset instead of embedding them recursively is so the data isn’t duplicated over and over. For example, in the output above the product “Tommy Jersey” is duplicated in each order. To make sure you don’t have duplicates in the included offset, you can use the unique() method on collections. Here’s an example of what the OrderCollection class would look like:

class OrderCollection
{
   public function toArray($request)
   {
       return [
           'data' => $this->collection->map(function ($order) use ($request) {
               return (new OrderResource($order))->toArray($request);
           })
       ];
   }


   public function with($request)
   {
       return [
           'included' => $this->collection->pluck('product')->unique()->values()->map(function ($product) {
               return new ProductResource($product);
           })
       ];
   }
}

At this point we have responses for both individual resources and their collections which comply with the JSON API spec! You can find a fully working version of the code above in the feature/api-resources branch of an example API codebase here.

What JSON API Features Are Missing?

With these changes, you now have an API that outputs in valid JSON API format, but API Resources don’t automatically support some of the more advanced (optional) features such as giving clients control over inclusion of related resources using the “include” query param, or the ability for clients to reduce payload size by specifying to return only the fields they need using sparse fieldsets.

Prior to the release of API Resources in Laravel 5.5, Fractal was a popular PHP package which offered an alternative implementation of a transformation layer in APIs. It has support for outputting to JSON API format built-in by default. As well, Fractal includes the more advanced features we mentioned above like the “include” query param, and sparse fieldsets. In my next blog post I’ll explore if you can swap out Fractal for native Laravel API Resources, AND if you should.

Subscribe or follow me on twitter so you don’t miss the next one!


Also published on Medium.