Towards Resource-Oriented REST Development

In many areas, we see the use of shared conventions to foster productivity. Docker resp. OCI images start to take over the delivery and installation of server software. Kubernetes seems to be achieving the same for the orchestration of software. Graph databases are leading the way when it comes to modeling and evolving complex, interrelated data sets. In stark contrast, REST development seems to be heading in the opposite direction. Instead of relying on shared conventions, people specify their personal flavor with OpenApi. Developers have to fiddle with path mappings and status codes; maybe with one or the other library offering some short-cuts for common patterns.  In contrast, highly efficient, asynchronous frameworks get built that scale to thousands of concurrent requests that only a few of us actually need. This article outlines a number of different possibilities to address these issues, move Java REST development to a higher level, and gain more productivity, consistency, and simplicity.

Ideas to achieve such goals are well established. For example, the Richardson Maturity Model specifies four levels of maturity for REST services. Services are supposed to make use of HTTP, have a resource-oriented architecture, perform proper mapping to URLs, and introduce links to facilitate the discovery of the API. Ideally, REST libraries would come with native support for such patterns. Such libraries may make use of three main building blocks in one way or the other:

These are three separate, orthogonal concerns and mixing those concerns can lead to a variety of issues. For example, it is typical for REST libraries to let developers annotate methods with the set of allowed HTTP methods, parameters, and path mappings and then return some data based upon that. This gives developers flexibility. And what could go wrong here? Since it's IT, just about everything. Among other concerns, it makes it very easy and tempting to violate the principles laid out before:

These and other mismatches make REST development inefficient and error-prone, hinder the use of third-party tooling, and complicate the evolution of the application.

What would be interesting, rather, would be to have REST libraries that implement the outlined principles natively and are backed by standards. Aspects like path mappings, linking, structuring of results, sorting, paging, filtering, and more, would be available out-of-the-box. Fortunately, there are a few libraries out there.

GraphQL

GraphQL is currently one of the more popular APIs out there. A graph is nothing more than resources and relationships. As such, it has a slightly different terminology but fits well with the scope of this article. An application is built with GraphQL by first declaring a model. The model is based on types and fields, for example:

type Project {
  name: String
  tagline: String
  contributors: [User]
}

A query then looks like:

{
  project(name: "GraphQL") {
    tagline
  }
}

And the matching result:

{
  "project": {
    "tagline": "A query language for APIs"
  }
}

With this, GraphQL moves away from the definition of endpoints towards the definition of types, just like the resource-based architecture outlined before. However, GraphQL also moves away from REST by establishing a query language to request data. One may argue that with this step it also moves away from the simplicity, accessibility, and discoverability that made REST popular in the first place.

OData

OData can be considered one of the early standards in the area. It is promoted as "SQL for the Web." It has a strong resource-based model. It supports sorting, filtering, and paging. Bulk updates can atomically create, insert, and update resources, something classical REST architectures do not directly address. But in some areas, it may also lack a bit of simplicity. There are smaller things like atypical path mappings:

GET api/People('russellwhyte')

rather than:

GET api/people/russellwhyte

Or the exchanged documents make use of property names like 

@odata.context

Such properties are not easily addressable from languages like JavaScript when processing JSON due to the use of special characters. The application cannot declare interfaces directly but have to introduce mappers or address them through string keys.

{
    "@odata.context": "serviceRoot/$metadata#People",
    "@odata.nextLink": "serviceRoot/People?%24skiptoken=8",
    "value": [
        {
            "@odata.id": "serviceRoot/People('russellwhyte')",
            "@odata.etag": "W/"08D1694BD49A0F11"",
            "@odata.editLink": "serviceRoot/People('russellwhyte')",
            "UserName": "russellwhyte",
            "FirstName": "Russell",
            "LastName": "Whyte",
            "Emails": [
                "Russell@example.com",
                "Russell@contoso.com"
            ],
            "AddressInfo": [
                {
                    "Address": "187 Suffolk Ln.",
                    "City": {
                        "CountryRegion": "United States",
                        "Name": "Boise",
                        "Region": "ID"
                    }
                }
            ],
            "Gender": "Male",
            "Concurrency": 635404796846280400
        },
        ......
    ]
}

JSON API

JSON API currently seems to sit in a bit of a sweet spot. The naming one may consider suboptimal as both JSON and API are ubiquitous terms and as such it is easy to mix it up with either of those. But it is a vendor and tool-agnostic standard with many implementations. It follows the principles of Richardson's Maturity Model with a resource-oriented architecture and HATEOAS. The specification covers:

In general, JSON API strives for simplicity. The specification is easy to follow. In some areas, only recommendations are given to leave applications the necessary flexibility to go beyond those recommendations. Paging and filtering are two examples of such recommendations. For example, the response to a request for http://localhost:8080/api/movie/ taken from crnk-example looks like:

{
  "data" : [
    {
      "id" : "6fed1645-bebb-349f-9567-1903a8a2e6da",
      "type" : "movie",
      "attributes" : {
        "year" : 2006,
        "name" : "Iron Man",
        "version" : 13
      },
      "relationships" : {
        "roles" : {
          "links" : {
            "self" : "http://localhost:8080/api/movie/6fed1645-bebb-349f-9567-1903a8a2e6da/relationships/roles",
            "related" : "http://localhost:8080/api/movie/6fed1645-bebb-349f-9567-1903a8a2e6da/roles"
          }
        },
        "history" : {
          "links" : {
            "self" : "http://localhost:8080/api/movie/6fed1645-bebb-349f-9567-1903a8a2e6da/relationships/history",
            "related" : "http://localhost:8080/api/movie/6fed1645-bebb-349f-9567-1903a8a2e6da/history"
          }
        }
      },
      "links" : {
        "self" : "http://localhost:8080/api/movie/6fed1645-bebb-349f-9567-1903a8a2e6da"
      },
      ...
    }
  ]
}

Some noteworthy features are that:

JSON API With Java

Among many languages, there are two implementations for Java: elide and crnk. Elide closely follows a JPA-related programming model. In doing so, it allows, for example, one to quickly expose JPA entities from Hibernate as JSON API resources. In contrast, crnk is more generic with a core engine and optional modules. The core engine implements the JSON API specification and recommendations. Developers can make use of its API to implement JSON API endpoints to access any kind of data store. It works equally well, for example, with JPA entities, Elastic Search indices, Neo4J graph databases, in-memory datasets, and others. It may also aggregate and interlink multiple such data sources trough JSON API relationships.

On top of the core engine there are numerous integrations into frameworks like JEE and Spring and modules that provide predefined building blocks:

The JSON API movie resource example from above is taken from crnk-example. It is one example where no custom implementation is necessary. Instead, it is just a JPA entity exposed with crnk-jpa as a JSON API resource:

@Entity
public class MovieEntity {

    @Id
    private UUID id;

    @JsonProperty
    @NotEmpty
    private String name;

    private int year;
    @OneToMany(mappedBy = "movie")

    private List<RoleEntity> roles = new ArrayList<>();

    @Version
    private Integer version;

    ...
}

The endpoint supports the full set of JSON API features. A number of example URLs to play are:

The documentation and example application show a number of further, more advanced use cases like mapping the entities to DTOs or introducing additional, computed attributes based on the JPA ones.

In contrast, a resource that is explicitly implemented with the crnk core API can look like:

@JsonApiResource(type = "vote")
public class Vote {

  @JsonApiId
  private UUID id;

  @JsonApiRelation
  private MovieEntity movie;

  private int count;

  ..
}

It makes use of JSON API specific annotations like @JsonApiResource and @JsonApiRelation. A repository to provide access to such resources can look as simple as:

public class VoteRepositoryImpl extends ResourceRepositoryBase<Vote, UUID> implements ResourceRepositoryV2<Vote, UUID> {

    public Map<UUID, Vote> votes = new HashMap<>();

    public VoteRepositoryImpl() {
        super(Vote.class);
    }

    @Override
    public ResourceList<Vote> findAll(QuerySpec querySpec) {
        return querySpec.apply(votes.values());
    }

    @Override
    public <S extends Vote> S save(S entity) {
        votes.put(entity.getId(), entity);
        return entity;
    }

    @Override
    public void delete(UUID id) {
        votes.remove(id);
    }
}

Here, data is stored in-memory within a HashMap. Sorting, filtering, and paging happen in-memory with QuerySpec.apply(...). From a consumer perspective, it works in almost exactly the same fashion as the earlier JPA-based example. Both follow the JSON API specifications and recommendations. While this example is overly simple, it can still serve two important purposes:

Connect Data With JSON API Relationships

Both the movie entity and vote resource example have relationships defined. The design and implementation of a relationship can greatly vary depending on the use case:

In crnk, this translates into two different kinds of implementation strategies:

The vote resource from earlier is one example of the latter strategy where no relationship repository is necessary. Instead, relationships are directly fetched and updated in memory on the vote resources. All relationship-specific path mappings continue to work, for example:

http://localhost:8080/api/vote/ffde251b-1a0a-3bbd-9de0-6e08aa7677ec/movie

The single-valued JPA relationship stored as a foreign key is the second typical example for the latter strategy. Since the relationship is stored in the same table, there is no need to handle it separately. The next example implements a similar use case:

@JsonApiResource(type = "screening")
public class Screening {

  @JsonApiId
  private UUID id;

  @JsonApiRelationId
  private UUID movieId;

  @JsonApiRelation(
    serialize = SerializeType.ONLY_ID,
    lookUp = LookupIncludeBehavior.AUTOMATICALLY_WHEN_NULL
  )
  private MovieEntity movie;

  private String location;

  ...
}

Here the relationship is defined by two attributes: movie and movieId.  moviecan hold the full resource while movieId holds only the ID. The movieId can be set and updated by the resource repository. moviecan remain nulland the crnk engine will look it up from the opposite resource repository if necessary through the use of LookupIncludeBehavior.AUTOMATICALLY_WHEN_NULLSerializeType.ONLY_ID further triggers that movieId is always written to the response, regardless of whether the related movie was requested or not. In this example again, there is no need to set up a dedicated relationship repository if the screening resource repository is able to handle updates of movieId.

Interconnecting Different Data Sets

crnk-example introduces a history to each resource to showcase the implementation of a custom relationship repository that works independently of the resource repositories and can connect arbitrary, possibly different, kinds of data stores:

@JsonApiResource(type = "history")
public class History {

  @JsonApiId
  private UUID id;

  private String newValue;

  private String oldValue;

  ...
}

The subsequent  HistoryFieldProvider introduces a new history relationship for every resource. The historized resource must not even define that relationship, it gets dynamically added:

@Component
public class HistoryFieldProvider implements ResourceFieldContributor {

    @Override
    public List<ResourceField> getResourceFields(ResourceFieldContributorContext context) {
        InformationBuilder.Field fieldBuilder = context.getInformationBuilder().createResourceField();
        fieldBuilder.name("history");
        fieldBuilder.genericType(new TypeToken<List<History>>() {
        }.getType());
        fieldBuilder.oppositeResourceType("history");
        fieldBuilder.fieldType(ResourceFieldType.RELATIONSHIP);
        fieldBuilder.lookupIncludeBehavior(LookupIncludeBehavior.AUTOMATICALLY_ALWAYS);
        fieldBuilder.accessor(new ResourceFieldAccessor() {
            @Override
            public Object getValue(Object resource) {
                return null;
            }

            @Override
            public void setValue(Object resource, Object fieldValue) {
            }
        });
        return Arrays.asList(fieldBuilder.build());
    }
}

This is useful for crnk-example as some of its resources are not explicitly defined, but backed by JPA entities. If all resources were to explicitly define that history relationship, this step can also be omitted.

The history elements are then served throughHistoryRelationshipRepository:

public class HistoryRelationshipRepository extends ReadOnlyRelationshipRepositoryBase<Object, Serializable, History, UUID> {

    @Override
    public RelationshipMatcher getMatcher() {
        return new RelationshipMatcher().rule().target(History.class).add();
    }

    @Override
    public ResourceList<History> findManyTargets(Serializable sourceId, String fieldName, QuerySpec querySpec) {
        DefaultResourceList list = new DefaultResourceList();
        for (int i = 0; i < 10; i++) {
            History history = new History();
            history.setId(UUID.nameUUIDFromBytes(("historyElement" + i).getBytes()));
            history.setNewValue("new" + i);
            history.setOldValue("old" + i);
            list.add(history);
        }
        return querySpec.apply(list);
    }

}

getMatcher() specifies that the relationship repository is to be applied to all relationships pointing to History. findManyTargets(...) fetches the actual history elements for a given record. crnk-example further implements a HistoryResourceRepositoryto access history elements directly.

Conclusions

In this article, the benefits of resource-oriented REST architectures have been discussed. The GraphQL, OData, and JSON API standards have been presented that implement such an architecture natively. Resources and relationships are first class citizens here. With crnk-example, an example application is shown making use of all these concepts. For more information, you may take a closer look at it and the official documentation. There is much more to look at. For example, the way resources are secured on resource and attribute levels goes way beyond typical authorization schemes and also covers aspects like sorting and filter parameters.

 

 

 

 

Top