Generating a JSONAPI compliant API for your resources

Repetition is boring. Web applications oftentimes require the same functionality: to create, read, update and delete resources. Even if they operate in different domains. Or, in terms of a REST API, endpoints to GET, POST, PATCH and DELETE resources. Since productivity is one of the driving forces behind the mu.semte.ch architecture, the platform provides a microservice – mu-cl-resources – that generates  a JSONAPI compliant API for your resources based on a simple configuration describing the domain. In this article we will explain how to setup such a configuration.

Adding mu-cl-resources to your project

Like all microservices in the mu.semte.ch stack, mu-cl-resources is published as a Docker image. It just needs two configuration files in the /config folder:

  • domain.lisp: describing the resources and relationships between them in your domain
  • repository.lisp: defining prefixes for the vocabularies used in your domain

To provide the configuration files, you can either mount the files in the /config folder of the Docker image…

services:
  resource:
    image: semtech/mu-cl-resources:1.15.0
    links:
      - db:database
    volumes:
      - /path/to/your/config:/config

… or build your own Docker image by extending the mu-cl-resources image and copying the configuration files in /config.

FROM semtech/mu-cl-resources:1.15.0

COPY domain.lisp /config/domain.lisp
COPY repository.lisp /config/repository.lisp

The former option may be easier during development, while the latter is better suited in a production environment. With this last variant you can publish and release your service with its own version, independent of the mu-cl-resources version.

When adding mu-cl-resources to our application, we also have to update the dispatcher configuration such that incoming requests get dispatched to our new service. We will update the dispatcher configuration at the end of this tutorial once we know on which endpoints our resources will be available.

Describing your domain

Next step is to describe your domain in the configuration files. The configuration is written in Common Lisp. Don’t be intimidated, just follow the examples, make abstraction of all the parentheses and your’re good to go 🙂 As an example, we will describe the domain of the ember-data-table demo consisting of books and their authors.

repository.lisp

The repository.lisp file describes the prefixes for the vocabularies used in our domain model.

To start, each configuration file starts with:

(in-package :mu-cl-resources)

Next, the prefixes are listed one per line as follows:

(in-package :mu-cl-resources)

(add-prefix "dcterms" "http://purl.org/dc/terms/")
(add-prefix "schema" "http://schema.org/")

domain.lisp

The domain.lisp file describes your resources and the relationships between them. In this post we will describe the model of a book. In a next post we will add an author model and specify the relationship between books and authors.

Start the domain.lisp file also with the following line:

(in-package :mu-cl-resources)

Next, add the basis of the book model:

(in-package :mu-cl-resources)

(define-resource book ()
  :class (s-prefix "schema:Book")
  :resource-base (s-url "http://mu.semte.ch/services/github/madnificent/book-service/books/")
:on-path "books")

Although you may not have written a letter of Common Lisp before, you will probably be able to understand the lines of code above.

  • We define a book resource
  • Each book will get schema:Book as RDF class
  • Each book instance will be identified with a URI starting with “http://mu.semte.ch/services/github/madnificent/book-service/books/”  – mu-cl-resources just appends a generated UUID to it
  • The resources will be published on the /books API endpoint

Finally, define the properties of a book:

(in-package :mu-cl-resources)

(define-resource book ()
  :class (s-prefix "schema:Book")
  :properties `((:title :string ,(s-prefix "schema:headline"))
                (:isbn :string ,(s-prefix "schema:isbn"))
                (:publication-date :date ,(s-prefix "schema:datePublished"))
                (:genre :string ,(s-prefix "schema:genre"))
                (:language :string ,(s-prefix "schema:inLanguage")) 
                (:number-of-pages :integer ,(s-prefix "schema:numberOfPages")))
  :resource-base (s-url "http://mu.semte.ch/services/github/madnificent/book-service/books/")
:on-path "books")

Each property is described according to the format:

(:dasherized-property-name :type, (s-prefix "my-prefix:my-predicate"))

and will result in a triple:

<http://mu.semte.ch/services/github/madnificent/book-service/books/ead2e61a-ab1e-4261-9b6d-9c142ae94765> my-prefix:my-predicate "some-value"

Configuring the dispatcher

Our book resources will be available on the /books paths.  The mu-cl-resources service provides GET, POST, PATCH and DELETE operations on this path for free. Assuming the books service is known as ‘resource’ in our dispatcher, we will add the following dispatch rule to our dispatcher configuration to forward the incoming requests to the books service:

match "/books/*path" do
 Proxy.forward conn, path, "http://resource/books/"
end

That’s it. The books can now be produced and consumed by the frontend through your JSONAPI compliant API.

Conclusion

The mu-cl-resources microservice offers you a service to setup a JSONAPI compliant API for your resources in just a few minutes. Although the configuration in Common Lisp may look intimidating at first, it’s easy to describe your domain with its resources and relationships in this language. We’re working on support for a configuration in JSON format. In a next post we will elaborate on relationships and features like pagination, sorting and filtering that mu-cl-resources provides for free.