simple-raml-with-resource-types-and-traits
Eliminate Redundancies in RAML with Resource Types and Traits
1. Overview
In our RAML tutorial
article, we introduced the RESTful API Modeling Language and created a
simple API definition based on a single entity called Foo. Now imagine
a real-world API in which you have several entity-type resources, all
having the same or similar GET, POST, PUT, and DELETE operations. You
can see how your API documentation can quickly become tedious and
repetitive.
In this article, we show how the use of the resource types and
traits features in RAML can eliminate redundancies in resource and
method definitions by extracting and parameterizing common sections,
thereby eliminating copy-and-paste errors while making your API
definitions more concise.
2. Our API
In order to demonstrate the benefits of resource types and traits,
we will expand our original API by adding resources for a second entity
type called Bar. Here are the resources that will make up our revised
API:
-
GET /api/v1/foos
-
POST /api/v1/foos
-
GET /api/v1/foos/{fooId}
-
PUT /api/v1/foos/{fooId}
-
DELETE /api/v1/foos/{fooId}
-
GET /api/v1/foos/name/{name}
-
GET /api/v1/foos?name={name}&ownerName={ownerName}
-
GET /api/v1/bars
-
POST /api/v1/bars
-
GET /api/v1/bars/{barId}
-
PUT /api/v1/bars/{barId}
-
DELETE /api/v1/bars/{barId}
-
GET /api/v1/bars/fooId/{fooId}
3. Recognizing Patterns
As we read through the list of resources in our API, we begin to see
some patterns emerge. For example, there is a pattern for the URIs and
methods used to create, read, update, and delete single entities, and
there is a pattern for the URIs and methods used to retrieve collections
of entities. The collection and collection-item pattern is one of the
more common patterns used to extract resource types in RAML
definitions.
Let’s look at a couple of sections of our API:
[Note: In the code snippets below, a line containing only three
dots (…) indicates that some lines are being skipped for brevity.]
/foos:
get:
description: |
List all foos matching query criteria, if provided;
otherwise list all foos
queryParameters:
name?: string
ownerName?: string
responses:
200:
body:
application/json:
type: Foo[]
post:
description: Create a new foo
body:
application/json:
type: Foo
responses:
201:
body:
application/json:
type: Foo
...
/bars:
get:
description: |
List all bars matching query criteria, if provided;
otherwise list all bars
queryParameters:
name?: string
ownerName?: string
responses:
200:
body:
application/json:
type: Bar[]
post:
description: Create a new bar
body:
application/json:
type: Bar
responses:
201:
body:
application/json:
type: Bar
When we compare the RAML definitions of the /foos and /bars
resources, including the HTTP methods used, we can see several
redundancies among the various properties of each, and we again see
patterns begin to emerge.
Wherever there is a pattern in either a resource or method definition,
there is an opportunity to use a RAML resource type or trait.
4. Resource Types
In order to implement the patterns found in the API, resource types
use reserved and user-defined parameters surrounded by double angle
brackets (<< and >>).
4.1 Reserved Parameters
Two reserved parameters may be used in resource type definitions:
-
[resourcePath] represents the entire URI (following the
baseURI), and -
[resourcePathName] represents the part of the URI following the
right-most forward slash (/), ignoring any braces \{ }.
When processed inside a resource definition, their values are calculated
based on the resource being defined.
Given the resource /foos, for example, [resourcePath] would
evaluate to “/foos” and [resourcePathName] would evaluate to “foos”.
Given the resource /foos/{fooId}, [resourcePath] would evaluate
to “/foos/{fooId}” and [resourcePathName] would evaluate to “foos”.
4.2 User-Defined Parameters
A resource type definition may also contain user-defined parameters.
Unlike the reserved parameters, whose values are determined dynamically
based on the resource being defined, user-defined parameters must be
assigned values wherever the resource type containing them is used,
and those values do not change.
User-defined parameters may be declared at the beginning of a resource
type definition, although doing so is not required and is not common
practice, as the reader can usually figure out their intended usage
given their names and the contexts in which they are used.
4.3 Parameter Functions
A handful of useful text functions are available for use wherever a
parameter is used in order to transform the expanded value of the
parameter when it is processed in a resource definition.
Here are the functions available for parameter transformation:
-
!singularize
-
!pluralize
-
!uppercase
-
!lowercase
-
!uppercamelcase
-
!lowercamelcase
-
!upperunderscorecase
-
!lowerunderscorecase
-
!upperhyphencase
-
!lowerhyphencase
Functions are applied to a parameter using the following construct:
<<parameterName | !functionName>>
If you need to use more than one function to achieve the desired
transformation, you would separate each function name with the pipe
symbol (“|”) and prepend an exclamation point (!) before each function
used.
For example, given the resource /foos, where <<resourcePathName>>
evaluates to “foos”:
-
<<resourcePathName | !singularize>> =⇒ “foo”
-
<<resourcePathName | !uppercase>> =⇒ “FOOS”
-
<<resourcePathName | !singularize | !uppercase>> =⇒ “FOO”
And given the resource /bars/{barId}, where <<resourcePathName>>
evaluates to “bars”:
-
<<resourcePathName | !uppercase>> =⇒ “BARS”
-
<<resourcePathName | !uppercamelcase>> =⇒ “Bar”
5. Extracting a Resource Type for Collections
Let’s refactor the /foos and /bars resource definitions shown above,
using a resource type to capture the common properties. We will use
the reserved parameter [resourcePathName], and the user-defined
parameter [typeName] to represent the data type used.
5.1 Definition
Here is a resource type definition representing a collection of items:
resourceTypes:
collection:
usage: Use this resourceType to represent any collection of items
description: A collection of <<resourcePathName>>
get:
description: Get all <<resourcePathName>>, optionally filtered
responses:
200:
body:
application/json:
type: <<typeName>>[]
post:
description: Create a new <<resourcePathName|!singularize>>
responses:
201:
body:
application/json:
type: <<typeName>>
Note that in our API, because our data types are merely capitalized,
singular versions of our base resources’ names, we could have applied
functions to the <<resourcePathName>> parameter, instead of
introducing the user-defined <<typeName>> parameter, to achieve the
same result for this portion of the API:
resourceTypes:
collection:
...
get:
...
type: <<resourcePathName|!singularize|!uppercamelcase>>[]
post:
...
type: <<resourcePathName|!singularize|!uppercamelcase>>
5.2 Application
Using the above definition that incorporates the <<typeName>>
parameter, here is how you would apply the “collection” resource type
to the resources /foos and /bars:
/foos:
type: { collection: { "typeName": "Foo" } }
get:
queryParameters:
name?: string
ownerName?: string
...
/bars:
type: { collection: { "typeName": "Bar" } }
Notice that we are still able to incorporate the differences between the
two resources — in this case, the queryParameters section — while
still taking advantage of all that the resource type definition has to
offer.
6. Extracting a Resource Type for Single Items of a Collection
Let’s focus now on the portion of our API dealing with single items of a
collection: the /foos/{fooId} and /bars/{barId} resources. Here is
the code for/foos/{fooId}:
/foos:
...
/{fooId}:
get:
description: Get a Foo
responses:
200:
body:
application/json:
type: Foo
404:
body:
application/json:
type: Error
example: !include examples/Error.json
put:
description: Update a Foo
body:
application/json:
type: Foo
responses:
200:
body:
application/json:
type: Foo
404:
body:
application/json:
type: Error
example: !include examples/Error.json
delete:
description: Delete a Foo
responses:
204:
404:
body:
application/json:
type: Error
example: !include examples/Error.json
The /bars/{barId} resource definition also has GET, PUT, and DELETE
methods and is identical to the /foos/{fooId} definition, other than
the occurrences of the strings “foo” and “bar” (and their respective
pluralized and/or capitalized forms).
6.1 Definition
Extracting the pattern we just identified, here is how we define a
resource type for single items of a collection:
resourceTypes:
...
item:
usage: Use this resourceType to represent any single item
description: A single <<typeName>>
get:
description: Get a <<typeName>>
responses:
200:
body:
application/json:
type: <<typeName>>
404:
body:
application/json:
type: Error
example: !include examples/Error.json
put:
description: Update a <<typeName>>
body:
application/json:
type: <<typeName>>
responses:
200:
body:
application/json:
type: <<typeName>>
404:
body:
application/json:
type: Error
example: !include examples/Error.json
delete:
description: Delete a <<typeName>>
responses:
204:
404:
body:
application/json:
type: Error
example: !include examples/Error.json
6.2 Application
And here is how we apply the “item” resource type:
/foos:
...
/{fooId}:
type: { item: { "typeName": "Foo" } }
...
/bars:
...
/{barId}:
type: { item: { "typeName": "Bar" } }
7. Traits
Whereas a resource type is used to extract patterns from resource
definitions, a trait is used to extract patterns from method
definitions that are common across resources.
7.1 Parameters
Along with <<resourcePath>> and <<resourcePathName>>, one
additional reserved parameter is available for use in trait definitions:
<<methodName>> evaluates to the HTTP method (GET, POST, PUT, DELETE,
etc) for which the trait is defined. User-defined parameters may also
appear within a trait definition, and where applied, take on the value
of the resource in which they are being applied.
7.2 Definition
Notice that the “item” resource type is still full of redundancies.
Let’s see how traits can help eliminate them. We’ll start by
extracting a trait for any method containing a request body:
traits:
hasRequestItem:
body:
application/json:
type: <<typeName>>
Now let’s extract traits for methods whose normal responses contain
bodies:
hasResponseItem:
responses:
200:
body:
application/json:
type: <<typeName>>
hasResponseCollection:
responses:
200:
body:
application/json:
type: <<typeName>>[]
Finally, here’s a trait for any method that could return a 404 error
response:
hasNotFound:
responses:
404:
body:
application/json:
type: Error
example: !include examples/Error.json
7.3 Application
We then apply this trait to our resource types:
resourceTypes:
collection:
usage: Use this resourceType to represent any collection of items
description: A collection of <<resourcePathName|!uppercamelcase>>
get:
description: |
Get all <<resourcePathName|!uppercamelcase>>,
optionally filtered
is: [ hasResponseCollection: { typeName: <<typeName>> } ]
post:
description: Create a new <<resourcePathName|!singularize>>
is: [ hasRequestItem: { typeName: <<typeName>> } ]
item:
usage: Use this resourceType to represent any single item
description: A single <<typeName>>
get:
description: Get a <<typeName>>
is: [ hasResponseItem: { typeName: <<typeName>> }, hasNotFound ]
put:
description: Update a <<typeName>>
is: | [ hasRequestItem: { typeName: <<typeName>> }, hasResponseItem: { typeName: <<typeName>> }, hasNotFound ]
delete:
description: Delete a <<typeName>>
is: [ hasNotFound ]
responses:
204:
We can also apply traits to methods defined within resources. This is
especially useful for “one-off” scenarios where a resource-method
combination matches one or more traits but does not match any defined
resource type:
/foos:
...
/name/{name}:
get:
description: List all foos with a certain name
is: [ hasResponseCollection: { typeName: Foo } ]
8. Conclusion
In this tutorial, we’ve shown how to significantly reduce or, in some
cases, eliminate redundancies from a RAML API definition.
First, we identified the redundant sections of our resources, recognized
their patterns, and extracted resource types. Then we did the same for
the methods that were common across resources to extract traits. Then
we were able to eliminate further redundancies by applying traits to
our resource types and to “one-off” resource-method combinations that
did not strictly match one of our defined resource types.
As a result, our simple API with resources for only two entities, was
reduced from 177 to just over 100 lines of code. To learn more about
RAML resource types and traits, visit the
RAML.org
1.0 spec.
The full implementation of this tutorial can be found in
the
github project.
Here is our final RAML API in its entirety:
#%RAML 1.0
title: Baeldung Foo REST Services API
version: v1
protocols: [ HTTPS ]
baseUri: http://rest-api.baeldung.com/api/{version}
mediaType: application/json
securedBy: basicAuth
securitySchemes:
basicAuth:
description: |
Each request must contain the headers necessary for
basic authentication
type: Basic Authentication
describedBy:
headers:
Authorization:
description: |
Used to send the Base64 encoded "username:password"
credentials
type: string
responses:
401:
description: |
Unauthorized. Either the provided username and password
combination is invalid, or the user is not allowed to
access the content provided by the requested URL.
types:
Foo: !include types/Foo.raml
Bar: !include types/Bar.raml
Error: !include types/Error.raml
resourceTypes:
collection:
usage: Use this resourceType to represent a collection of items
description: A collection of <<resourcePathName|!uppercamelcase>>
get:
description: |
Get all <<resourcePathName|!uppercamelcase>>,
optionally filtered
is: [ hasResponseCollection: { typeName: <<typeName>> } ]
post:
description: |
Create a new <<resourcePathName|!uppercamelcase|!singularize>>
is: [ hasRequestItem: { typeName: <<typeName>> } ]
item:
usage: Use this resourceType to represent any single item
description: A single <<typeName>>
get:
description: Get a <<typeName>>
is: [ hasResponseItem: { typeName: <<typeName>> }, hasNotFound ]
put:
description: Update a <<typeName>>
is: [ hasRequestItem: { typeName: <<typeName>> }, hasResponseItem: { typeName: <<typeName>> }, hasNotFound ]
delete:
description: Delete a <<typeName>>
is: [ hasNotFound ]
responses:
204:
traits:
hasRequestItem:
body:
application/json:
type: <<typeName>>
hasResponseItem:
responses:
200:
body:
application/json:
type: <<typeName>>
hasResponseCollection:
responses:
200:
body:
application/json:
type: <<typeName>>[]
hasNotFound:
responses:
404:
body:
application/json:
type: Error
example: !include examples/Error.json
/foos:
type: { collection: { typeName: Foo } }
get:
queryParameters:
name?: string
ownerName?: string
/{fooId}:
type: { item: { typeName: Foo } }
/name/{name}:
get:
description: List all foos with a certain name
is: [ hasResponseCollection: { typeName: Foo } ]
/bars:
type: { collection: { typeName: Bar } }
/{barId}:
type: { item: { typeName: Bar } }
/fooId/{fooId}:
get:
description: Get all bars for the matching fooId
is: [ hasResponseCollection: { typeName: Bar } ]
Next »
« Previous