Coming Up for Air

Contract-First REST APIs with RAML

Wednesday, October 15, 2014 |

Yesterday, at the OKC JUG, I presented on the topic of Contract-first REST API development with RAML. This post is a rough blogification of that discussion. For those of you who were at the meeting, the preamble to the source demo (introduction, background, options discussion, etc) have been not been reproduced here.

tl;dr: You can find the demo source here and play around with it.

Introduction

RAML is a specification for describing REST interfaces with the intention of generating servers, clients, and documentation. It’s a product of the RAML Workgroup. One of the first questions that may pop up for anyone who has been working with REST (especially in the Java space) is, "What about Swagger?" Mulesoft answers that question:

Mulesoft originally started with Swagger but realised that standard was best suited to documenting an existing API, not for designing an API from scratch. RAML evolved out of the need to support up-front API design in a succint, human-centric language.

The Root Section

That said, let’s take a look at a sample RAML spec file. We’ll start with the basics:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#%RAML 0.8
title: "JUG - RAML"
version: v1
baseUri: https://localhost:8080/raml/resources
protocols: [ HTTPS ]
mediaType: application/json
documentation:
    - title: Home
      content: |
        JUG - Contract-first with RAML

This "root" section describes some top-level metadata for the API. Of the values here, only title and baseUri are required, and should be self-explanatory. The mediaType value describes the default media type for the resources described in the spec.

Schemas

Typically, when designing a system, I like to think in terms of data models first, as it helps me grasp exactly what I’ll be working with, so we’re going to take a look at how we describe models in a RAML document, which happens under the schemas key:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
schemas:
    - author: |
        {   "$schema": "http://json-schema.org/draft-03/schema",
            "type": "object",
            "description": "A single author",
            "properties": {
                "id": { "type": "integer", "required": true},
                "name":  { "type": "string", "required": true },
                "books": {
                    "type": "array",
                    "items": {
                        "$ref":"book"
                    }
                }
            }
        }
    - book: |
        {   "$schema": "http://json-schema.org/draft-03/schema",
            "type": "object",
            "description": "A single book",
            "properties": {
                "id": { "type": "integer", "required": true},
                "name":  { "type": "string", "required": true },
                "isbn":  { "type": "string", "required": true },
                "author_id" : { "type": "integer" }
            }
        }
    - authors: |
        {   "$schema": "http://json-schema.org/draft-03/schema",
            "type": "object",
            "description": "a collection of authors",
            "properties": {
                "size":  { "type": "integer", "required": true },
                "authors":  {
                    "type": "array",
                    "items": { "$ref": "author" }
                }
              }
        }
    - books: |
        {   "$schema": "http://json-schema.org/draft-03/schema",
            "type": "object",
            "description": "a collection of books",
            "properties": {
                "size":  { "type": "integer", "required": true },
                "books":  {
                    "type": "array",
                    "items": { "$ref": "book" }
                }
              }
        }

Here, we have a list of schemas: author, book, authors, and books. The first thing you may notice is that the schema definitions look and awful lot like JSON, and that’s because they are. Internally, the RAML tools use jsonschema2pojo to generate the Java classes for the models. Looking at the author example, we see that the model will have three properties:

  • id, which will be the primary key

  • name, which is the author’s name

  • books, which is an array of book instances.

Note the value for the items property in the schema definition. The $ref key tells the schema generator that the array will hold references to book instances. We’ll see that type show up in the generated code:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
/**
 * A single author
 *
 */
@JsonInclude(JsonInclude.Include.NON_NULL)
@Generated("org.jsonschema2pojo")
@JsonPropertyOrder({
    "id",
    "name",
    "books"
})
public class Author {

    /**
     *
     * (Required)
     *
     */
    @JsonProperty("id")
    @NotNull
    private Integer id;
    /**
     *
     * (Required)
     *
     */
    @JsonProperty("name")
    @NotNull
    private String name;
    @JsonProperty("books")
    @Valid
    private List<Book> books = new ArrayList<Book>();
    @JsonIgnore
    private Map<String, Object> additionalProperties = new HashMap<String, Object>();

    /**
     *
     * (Required)
     *
     * @return
     *     The id
     */
    @JsonProperty("id")
    public Integer getId() {
        return id;
    }

    /**
     *
     * (Required)
     *
     * @param id
     *     The id
     */
    @JsonProperty("id")
    public void setId(Integer id) {
        this.id = id;
    }

    public Author withId(Integer id) {
        this.id = id;
        return this;
    }

    /**
     *
     * (Required)
     *
     * @return
     *     The name
     */
    @JsonProperty("name")
    public String getName() {
        return name;
    }

    /**
     *
     * (Required)
     *
     * @param name
     *     The name
     */
    @JsonProperty("name")
    public void setName(String name) {
        this.name = name;
    }

    public Author withName(String name) {
        this.name = name;
        return this;
    }

    /**
     *
     * @return
     *     The books
     */
    @JsonProperty("books")
    public List<Book> getBooks() {
        return books;
    }

    /**
     *
     * @param books
     *     The books
     */
    @JsonProperty("books")
    public void setBooks(List<Book> books) {
        this.books = books;
    }

    public Author withBooks(List<Book> books) {
        this.books = books;
        return this;
    }

    @JsonAnyGetter
    public Map<String, Object> getAdditionalProperties() {
        return this.additionalProperties;
    }

    @JsonAnySetter
    public void setAdditionalProperty(String name, Object value) {
        this.additionalProperties.put(name, value);
    }

    public Author withAdditionalProperty(String name, Object value) {
        this.additionalProperties.put(name, value);
        return this;
    }

}

There are a lot of unexpected (to me) annotations in the generated class, but those are Jackson annotations, as we instructed the Maven plugin (which we’ll look at below) to use Jackson as the JSON mapper. Notice also that each property has some Javadoc for it. If you want to provide a more helpful description, you can specify that in your schema definition:

1
"name":  { "type": "string", "required": true, "description": "The author's full name" }

which generates

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
    /**
     * The author's full name
     * (Required)
     *
     * @return
     *     The name
     */
    @JsonProperty("name")
    public String getName() {
        return name;
    }

The authors and books schemas deserve a special mention. If you walk through the official RAML tutorial, this is the pattern they use. For the collection resources (those that return all of the author and book instances, for example), they wrap the list in a simple model which exposes two properties: the size of the list, and the list itself. While I can’t say why they did that, I can say that in my attempts to "fix" that ran into an issue: the POJO mapping feature in Jersey that I used in the demo does not know how to serialize a List (or a Map) as the root object type. Obviously, it can handle a List as a property as the books properties are both mapped as List s. Either I’m missing something in terms of configuring the feature, or using a List here would require a custom MessageBodyWriter to handle. For the sake of simplicity, I opted to follow the example from the RAML tutorial.

Resources

Now that we’ve defined the data we’ll be working with, let’s expose those models via REST resources. To define an endpoint, you start the key name with a /:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
/authors:
    get:
        description: Get a list of all the authors in the system
        responses:
            200:
                body:
                    application/json:
                        schema: authors
    post:
        description: Add author
        body:
            application/json:
                schema: author
        responses:
            201:
                body:
                    application/json:
                        schema: author
    /id/{authorId}:
        uriParameters:
            authorId:
                displayName: Author ID
                type: integer
        get:
            description: Retrieve a specific author
            responses:
                200:
                    body:
                        application/json:
                            schema: author
        post:
            description: Update an author
            body:
                application/json:
                    schema: author
            responses:
                200:
                    body:
                        application/json:
                            schema: author
        delete:
            description: Delete an author
            responses:
                200:
/books:
    get:
        description: Get a list of all of the books in the system
        responses:
            200:
                body:
                    application/json:
                        schema: books
    post:
        description: Create a book
        body:
            application/json:
                schema: book
        responses:
            201:
                body:
                    application/json:
                        schema: book
    /id/{bookId}:
        uriParameters:
            bookId:
                description: Book ID
                type: integer
        get:
            description: Get a book
            responses:
                200:
                    body:
                        application/json:
                            schema: author

We’ve defined 4 resources here: 2 root-level, and 2 sub-resources. Taking the /books resource as an example:

  • The resource supports the GET method, which can return the HTTP status code 200. The response body will be a books instance encoded as JSON.

  • The resource also supports the POST method. It takes a book instance, encoded as JSON, as the payload. Upon success, it will return a 201 (Created) and the newly created book instance, also encoded as JSON.

  • The resource also has a sub-resource, identified by /id/{bookId}. This subresource will return a specific book, identified by its id. The book’s ID is specified as a path parameter via a syntax that should be familiar to JAX-RS users. We also describe that parameter, called a uriParameter in the RAML spec, as an integer whose description is "Book ID".

When this spec is processed, you’ll get a class that looks like this:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
@Path("books")
public interface Books {


    /**
     * Get a list of all of the books in the system
     *
     */
    @GET
    @Produces({
        "application/json"
    })
    Books.GetBooksResponse getBooks()
        throws Exception
    ;

    /**
     * Create a book
     *
     * @param entity
     *
     */
    @POST
    @Consumes("application/json")
    @Produces({
        "application/json"
    })
    Books.PostBooksResponse postBooks(Book entity)
        throws Exception
    ;

    /**
     * Get a book
     *
     * @param bookId
     *     Book ID
     */
    @GET
    @Path("id/{bookId}")
    @Produces({
        "application/json"
    })
    Books.GetBooksIdByBookIdResponse getBooksIdByBookId(
        @PathParam("bookId")
        Long bookId)
        throws Exception
    ;

    public class GetBooksIdByBookIdResponse
        extends com.steeplesoft.jug.raml.support.ResponseWrapper
    {


        private GetBooksIdByBookIdResponse(Response delegate) {
            super(delegate);
        }

        /**
         *
         * @param entity
         *
         */
        public static Books.GetBooksIdByBookIdResponse jsonOK(Author entity) {
            Response.ResponseBuilder responseBuilder = Response.status(200).header("Content-Type", "application/json");
            responseBuilder.entity(entity);
            return new Books.GetBooksIdByBookIdResponse(responseBuilder.build());
        }

    }

    public class GetBooksResponse
        extends com.steeplesoft.jug.raml.support.ResponseWrapper
    {


        private GetBooksResponse(Response delegate) {
            super(delegate);
        }

        /**
         *
         * @param entity
         *
         */
        public static Books.GetBooksResponse jsonOK(com.steeplesoft.jug.raml.model.Books entity) {
            Response.ResponseBuilder responseBuilder = Response.status(200).header("Content-Type", "application/json");
            responseBuilder.entity(entity);
            return new Books.GetBooksResponse(responseBuilder.build());
        }

    }

    public class PostBooksResponse
        extends com.steeplesoft.jug.raml.support.ResponseWrapper
    {


        private PostBooksResponse(Response delegate) {
            super(delegate);
        }

        /**
         *
         * @param entity
         *
         */
        public static Books.PostBooksResponse jsonCreated(Book entity) {
            Response.ResponseBuilder responseBuilder = Response.status(201).header("Content-Type", "application/json");
            responseBuilder.entity(entity);
            return new Books.PostBooksResponse(responseBuilder.build());
        }

    }

}

Notice that this is a Java interface, so we’re not quite ready to deploy this just yet. The code generator also creates several ResponseWrapper subclasses and uses those as the return types for the Java methods. These classes handle building the Response to hand off to the JAX-RS runtime. This might be a slightly different approach than you may be used to using in your JAX-RS resources, but that’s the price you pay when have a third party handle code generation. :)

Resource Types

If you look back at the RAML file, you’ll see a lot of duplication (e.g., the post fields on the root-level resources are almost identical, with the only difference being the schema type). Fortunately, RAML provides a way to avoid a lot of this duplication via the concept of resourceTypes:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
resourceTypes:
    - collection:
        get:
            responses:
                200:
                    body:
                        application/json:
                            schema: <<schema>>
        post:
            body:
                application/json:
                    schema: <<schema>>
            responses:
                201:
                    body:
                        application/json:
                            schema: <<schema>>
    - member:
        get:
            responses:
                200:
                    body:
                        application/json:
                            schema: <<schema>>
        post:
            body:
                application/json:
                    schema: <<schema>>
            responses:
                200:
                    body:
                        application/json:
                            schema: <<schema>>
        delete:
            responses:
                200:

In looking at our resources, you can see we have two types: one returns a collection of a certain type, and the other returns a specific, single instance of that type. We have, then, collection and member resources, which we’ve modeled in our RAML spec above.

The first type, collection, defines two methods, GET and POST, with the types of expectations and behaviors we described above. Note, though, that the schema definition looks a bit different. The value [schema] declares a variable whose value will be defined when we use the resourceType. The member type does the same thing, mirroring what we did for our subresource above.

So how do we do these? It requires one small addition to the resource definition, and allows us to remove quite a bit at the same time. Here is the /books resource, modified to use the appropriate resource types:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/books:
    displayName: BooksResource
    type: {
    	collection: {
    		schema: books
    	}
    }
    get:
        description: Get a list of all of the books in the system
    post:
        description: Create a book
    /id/{bookId}:
        type: {
        	member: {
        		schema: book
        	}
        }
        uriParameters:
            bookId:
                displayName: Book ID
                type: integer
        get:
            description: Get a book

Notice that we’ve added type key to the root resource, as well as the subresource. Taking the first instance, we’re telling RAML that this resource’s type is collection, and its schema (which is the variable we declared in the resource type definition) is books. The subresource is member type and uses book as its schema. Any other variables you may wish to define in the resource type would be defined inside the innermost block (where schema is defined).

Having made this change, we can regenerate the source and see that we haven’t change a thing there. We’ve simply moved some of the boilerplate out of the resource definition, making them much simpler.

This concept of resource types is, I think, really significant, as it lets us describe what all resources of this type will look like. In my experience with writing the Java code first, things tend to get out of sync overtime: a number of Java resources are written, then a new requirement is handed to development, requiring, say, certain query parameters to be added to each resource to enable some fancy new feature. The developers then have to change the code for each resource, making sure to change each and every method. While there are other ways to mitigate that some in Java-first approaches, this contract-first approach, coupled with the resourceTypes concept, makes it much easier: the appropriate type is updated as needed, the Java interface s are generated, and the code refuses to compile until the implementation is updated. We catch the changes at compile-time, rather than letting them slip through to QA or, even worse, production to be discovered by accident when someone tries to use the new feature. I think that’s a pretty handy feature. :)

Traits

Let’s take a look at one more concept in RAML: traits. If you’re familiar with traits or mixins in languages like Scala, this will be pretty simple. Bascially, you’re defining some added characteristics you want to expose. For example, let’s say you want to allow the user to page through a collection (if this were an Amazon API, you certainly wouldn’t want all of the authors or books in the system). To do that, we’d describe the trait like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
traits:
    - paged:
        queryParameters:
            start:
                displayName: start
                description: The first page to return
                type: integer
            pages:
                displayName: pages
                description: The number of pages to return
                type: integer

This trait adds two query parameters: start and pages, which related metadata. To apply the trait, add the is key to the resource or resource type:

1
2
3
4
resourceTypes:
    - collection:
        get:
            is: [ paged ]

Now, all resources of type collection will have paging support:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
    /**
     * Get a list of all the authors in the system
     *
     * @param pages
     *     The number of pages to return
     * @param start
     *     The first page to return
     */
    @GET
    @Produces({
        "application/json"
    })
    AuthorsResource.GetAuthorsResponse getAuthors(
        @QueryParam("start")
        Long start,
        @QueryParam("pages")
        Long pages)
        throws Exception
    ;

A trait can also declare variables:

1
2
3
4
5
6
7
    - searchable:
        queryParameters:
            query:
                description: |
                    JSON array [{"field1","value1","operator1"},{"field2","value2","operator2"},...,{"fieldN","valueN","operatorN"}] <<description>>
                example: |
                    <<example>>

which can be defined using a child object in the is usage:

1
2
3
4
5
6
        is: [
        	searchable: {
        		description: "with valid searchable fields: name",
        		example: "[\"name\", \"Wheel of Time\"]"
        	}
        ]

In this example, we see the value of is is an array, albeit with one value. If you want to apply multiple traits, you’d simple add more items to this array, separating them by commas:

1
	is: [ trait1, trait2: { foo: "bar" } ]

Documentation

Documentation generation, something developers have a love/hate relationship with, is pretty simple to generate from the RAML spec. I prefer to have the documentation generated as part of the build process, but I have not found a Maven (or Gradle) plugin to handle that. I did find, however, a nice command line tool, raml2html, which produces nice, clean output:

1
2
$ npm i -g raml2html
$ raml2html src/main/resources/raml/spec.raml

You can see a sample of the output here:

example

The Build

While not the point of this entry, I do want to show you the relevant portions of my pom.xml to help get you going:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
<plugin>
    <groupId>org.raml.plugins</groupId>
    <artifactId>raml-jaxrs-maven-plugin</artifactId>
    <version>1.0-SNAPSHOT</version>
    <configuration>
        <sourceDirectory>${basedir}/src/main/resources/raml</sourceDirectory>
        <basePackageName>com.steeplesoft.jug.raml</basePackageName>
        <jaxrsVersion>2.0</jaxrsVersion>
        <useJsr303Annotations>true</useJsr303Annotations>
        <jsonMapper>jackson2</jsonMapper>
        <removeOldOutput>true</removeOldOutput>
    </configuration>
    <executions>
        <execution>
            <goals>
                <goal>generate</goal>
            </goals>
            <phase>generate-sources</phase>
        </execution>
    </executions>
</plugin>

This will output the generated sources in ${project.build.directory}/generated-sources/raml-jaxrs and update the project model accordingly, so any IDE with decent Maven support should pick up your changes seamlessly.

Conclusion

As I noted in my presentation, I haven’t used this in anger yet, but it certainly looks promising. There are certainly some code style issues I either need to solve or get over (e.g., I’d love to see JPA annotations on the models, and I’d rather see subresources emitted as classes rather than methods on the parent). Overall, though, I think this is definitely a tool (and an approach) worth keeping an eye on.

While clearly still under development (the current version is 0.8), RAML is already showing a good deal of promise for clean, simple, contract-first development. The YAML syntax is concise and readable, and the code generation seems to be

Search

    Quotes

    Sample quote

    Quote source

    About

    My name is Jason Lee. I am a software developer living in the middle of Oklahoma. I’ve been a professional developer since 1997, using a variety of languages, including Java, Javascript, PHP, Python, Delphi, and even a bit of C#. I currently work for Red Hat on the WildFly/EAP team, where, among other things, I maintain integrations for some MicroProfile specs, OpenTelemetry, Micrometer, Jakarta Faces, and Bean Validation. (Full resume here. LinkedIn profile)

    I am the president of the Oklahoma City JUG, and an occasional speaker at the JUG and a variety of technical conferences.

    On the personal side, I’m active in my church, and enjoy bass guitar, running, fishing, and a variety of martial arts. I’m also married to a beautiful woman, and have two boys, who, thankfully, look like their mother.

    My Links

    Publications