route spec

route spec

The spec module allows you to export your API/microservices outside a Jooby application.

The goal of this module is to define a common way to write APIs and provide tools like live doc and testing with no extra effort.

You won’t be forced to learn any other tool or annotate your code with special annotations. All you have to do is write your application following a few simple suggestions.

This module will process, collect and compile routes from your application. It extracts HTTP method, path patterns, parameters, response types and documentation.

You will find the basics and the necessary documentation to build and expose rich APIs for free, but keep in mind that this module isn’t intended to be used directly. It is the base for tools like Swagger or RAML.

definition

Let’s review how to build rich APIs using the spec module using either the script or the mvc programming styles:

script API

{
  /**
   * 
   * Everything about your Pets. 
   */
  use("/api/pets")
      /**
       * 
       * List pets ordered by id.
       * 
       * @param start Start offset, useful for paging. Default is <code>0</code>.
       * @param max Max page size, useful for paging. Default is <code>50</code>.
       * @return Pets ordered by name. 
       */
      .get(req -> {
        int start = req.param("start").intValue(0);
        int max = req.param("max").intValue(50);
        DB db = require(DB.class);
        List<Pet> pets = db.findAll(start, max);
        return pets;
      })
      /**
       * 
       * Find pet by ID
       * 
       * @param id Pet ID.
       * @return Returns <code>200</code> with a single pet or <code>404</code> 
       */
      .get("/:id", req -> {
        int id = req.param("id").intValue();
        DB db = require(DB.class);
        Pet pet = db.find(id);
        if (pet == null) {
          throw new Err(Status.NOT_FOUND);
        }
        return pet;
      })
      /**
       * 
       * Add a new pet to the store.
       * 
       * @param body Pet object that needs to be added to the store.
       * @return Returns a saved pet. 
       */
      .post(req -> {
        Pet pet = req.body().to(Pet.class);
        DB db = require(DB.class);
        db.save(pet);
        return pet;
      })
      /**
       * 
       * Update an existing pet.
       * 
       * @param body Pet object that needs to be updated.
       * @return Returns a saved pet. 
       */
      .put(req -> {
        Pet pet = req.body().to(Pet.class);
        DB db = require(DB.class);
        db.save(pet);
        return pet;
      })
      /**
       * 
       * Deletes a pet by ID.
       * 
       * @param id Pet ID.
       * @return A <code>204</code> 
       */
      .delete("/:id", req -> {
        int id = req.param("id").intValue();
        DB db = require(DB.class);
        db.delete(id);
        return Results.noContent();
      })
      .produces("json")
      .consumes("json");
}

mvc API


/**
 * 
 * Everything about your Pets. 
 */
@Path("/api/pets")
@Consumes("json")
@Produces("json")
public class Pets {

  private DB db;

  @Inject
  public Pets(final DB db) {
    this.db = db;
  }

  /**
   * 
   * List pets ordered by name.
   * 
   * @param start Start offset, useful for paging. Default is <code>0</code>.
   * @param max Max page size, useful for paging. Default is <code>200</code>.
   * @return Pets ordered by name. 
   */
  @GET
  public List<Pet> list(final Optional<Integer> start, final Optional<Integer> max) {
    List<Pet> pets = db.findAll(Pet.class, start.orElse(0), max.orElse(200));
    return pets;
  }

  /**
   * 
   * Find pet by ID.
   * 
   * @param id Pet ID.
   * @return Returns a single pet 
   */
  @Path("/:id")
  @GET
  public Pet get(final int id) {
    Pet pet = db.find(Pet.class, id);
    return pet;
  }

  /**
   * 
   * Add a new pet to the store.
   * 
   * @param pet Pet object that needs to be added to the store.
   * @return Returns a saved pet. 
   */
  @POST
  public Pet post(@Body final Pet pet) {
    db.save(pet);
    return pet;
  }

  /**
   * 
   * Update an existing pet.
   * 
   * @param body Pet object that needs to be updated.
   * @return Returns a saved pet. 
   */
  @PUT
  public Pet put(@Body final Pet pet) {
    db.save(pet);
    return pet;
  }

  /**
   * 
   * Deletes a pet by ID.
   * 
   * @param id Pet ID. 
   */
  @DELETE
  public void delete(final int id) {
    db.delete(Pet.class, id);
  }
}

The previous examples, while feature identical, are written very differently. The API Spec for both is still the same:


GET /api/pets
  summary: Everything about your Pets.
  doc: List pets ordered by name.
  consumes: [application/json]
  produces: [application/json]
  params: 
    start: 
      paramType: QUERY
      type: int
      value: 0
      doc: Start offset, useful for paging. Default is <code>0</code>.
    max: 
      paramType: QUERY
      type: int
      value: 200
      doc: Max page size, useful for paging. Default is <code>200</code>.
  response: 
    type: java.util.List<apps.model.Pet>
    doc: Pets ordered by name.
GET /api/pets/:id
  summary: Everything about your Pets.
  doc: Find pet by ID
  consumes: [application/json]
  produces: [application/json]
  params: 
    id: 
      paramType: PATH
      type: int
      doc: Pet ID.
  response: 
    type: apps.model.Pet
    doc: Returns <code>200</code> with a single pet or <code>404</code>
POST /api/pets
  summary: Everything about your Pets.
  doc: Add a new pet to the store.
  consumes: [application/json]
  produces: [application/json]
  params: 
    <body>: 
      paramType: BODY
      type: apps.model.Pet
      doc: Pet object that needs to be added to the store.
  response: 
    type: apps.model.Pet
    doc: Returns a saved pet.
PUT /api/pets
  summary: Everything about your Pets.
  doc: Update an existing pet.
  consumes: [application/json]
  produces: [application/json]
  params: 
    <body>: 
      paramType: BODY
      type: apps.model.Pet
      doc: Pet object that needs to be updated.
  response: 
    type: apps.model.Pet
    doc: Returns a saved pet.
DELETE /api/pets/:id
  summary: Everything about your Pets.
  doc: Deletes a pet by ID.
  consumes: [application/json]
  produces: [application/json]
  params: 
    id: 
      paramType: PATH
      type: int
      doc: Pet ID.
  response: 
    type: void
    doc: A <code>204</code>

NOTE: We use a textual representation here for simplicity and readability, but keep in mind that the output is compiled into a binary format.

script rules

Consider the previous examples again. Do you notice anything special? No, right?

There are however some minor issues that you need to keep in mind for getting or collecting route metadata from script routes:

params

Parameters need to be in one sentence/statement, like:

req -> {
  int id = req.param("id").intValue();
}

not like:

req -> {
  Mutant p = req.param("id");
  int id = p.intValue();
}

response type (a.k.a return type)

There should be ONLY one return statement and the return type needs to be declared as a variable, like this:

req -> {
  ...
  Pet pet = db.find(id); // variable pet
  ...
  return pet;  // single return statement
}

not like this:

req -> {
  ...
  return db.find(id); // we aren't able to tell what type returns db.find
}

or

req -> {
  ...
  // multiple returns statement
  if (...) {
    return ...; // here is one
  } else {
    return ...; // here is another
  }
}

If these rules don’t make sense to you or if the algorithm fails to resolve the correct type, there is a workaround outlined in the next section.

writing documentation

By taking a few minutes to write quality documentation for your code, you will reap huge benefits!

The spec tool will export the doc as part of your API!

Here is an example on how to document script routes:

  /** * Everything about your Pets. */
  use("/api/pets")
     /**
      * 
      * List pets ordered by name.
      * 
      * @param start Start offset, useful for paging. Default is <code>0</code>.
      * @param max Max page size, useful for paging. Default is <code>200</code>.
      * @return Pets ordered by name. 
      */
    .get(req -> {
      int start = req.param("start").intValue(0);
      int max = req.param("max").intValue(200);
      DB db = require(DB.class);
      List<Pet> pets = db.findAll(Pet.class, start, max);
      return pets;
    });

The route params for /api/pets looks like:

  params: 
    start: 
      paramType: QUERY
      type: int
      value: 0
      doc: Start offset, useful for paging. Default is <code>0</code>.
    max: 
      paramType: QUERY
      type: int
      value: 200
      doc: Max page size, useful for paging. Default is <code>200</code>.
  response: 
    type: java.util.List<apps.model.Pet>
    doc: Pets ordered by name.

response

With JavaDoc, you can control the default type returned by the route and/or the status codes. For example:


  /**
   * 
   * Find pet by ID.
   * 
   * @param id Pet ID.
   * @return Returns a {@link Pet} with <code>200</code> status or <code>404</code> 
   */
  get(req -> {
    DB db = require(DB.class);
    return db.find(Pet.class, id);
  });

Produces:

response:
    type: apps.model.Pet
    statusCodes: 
      200: Success
      404: Not Found
    doc: Returns <code>200</code> with a single pet or <code>404</code>

In this example you tell the spec tool that the response is a Pet via JavaDoc type references: {@link Type}.

This is useful when the tool isn’t able to detect the type for you and/or you aren’t able to follow the return rules described earlier.

The status codes section has entries for 200 and 404 with default messages. You can override the default message by using code = message like:

@return Returns a {@link Pet} with <code>200 = Success</code> status or <code>404 = Missing</code>

how does it work?

The spec module scans and parses the source code files: *.java and produces a list of RouteSpec.

This is required for getting information from script routes and to extract JavaDoc.

However, this is not needed for mvc routes because all the information is available through Reflection and the java.lang.Method.

Since the spec tool needs the source code in order to work properly you must set up the jooby:spec maven plugin in order to use the spec tool at deploy time:

<plugin>
  <groupId>org.jooby</groupId>
  <artifactId>jooby-maven-plugin</artifactId>
  <executions>
    <execution>
      <goals>
        <goal>spec</goal>
      </goals>
    </execution>
  </executions>
</plugin>

or the gradle plugin:

buildscript {

  dependencies {
    /**
     * joobyRun 
     */
    classpath group: 'org.jooby', name: 'jooby-gradle-plugin', version: '1.1.3'
  }
}

apply plugin: 'jooby'

gradle joobySpec

The maven and gradle plugins export the API to a binary format: .spec which can be parsed at a later time.

That’s all about the spec, as you’ve seen there are some minor rules to follow while writing script routes. Now it’s time see what tools and integrations are available!

raml

RESTful API Modeling Language (RAML) makes it easy to manage the whole API lifecycle from design to sharing. It’s concise - you only write what you need to define - and reusable. It is machine readable API design that is actually human friendly. More at http://raml.org

exports

dependency

<dependency>
 <groupId>org.jooby</groupId>
 <artifactId>jooby-raml</artifactId>
 <version>1.1.3</version>
</dependency>

usage

{
  // define your API... via script or MVC:
  /**
   * 
   * Everything about your pets 
   */
   use("/api/pets")
     /**
      * 
      * Get a pet by ID.
      * @param id Pet ID 
      */
     .get("/:id", req -> {
       int id = req.param("id").intValue();
       DB db = require(DB.class);
       Pet pet = db.find(Pet.class, id);
       return pet;
     })
     ...;

  new Raml().install(this);

}

Now the jooby:spec maven plugin:

<plugin>
  <groupId>org.jooby</groupId>
  <artifactId>jooby-maven-plugin</artifactId>
  <executions>
    <execution>
      <goals>
        <goal>spec</goal>
      </goals>
    </execution>
  </executions>
</plugin>

Start your app and try:

  • The api-console at /raml
  • The .raml file at /raml/api.raml

options

There are a few options available, let’s see what they are:

path

The path option controls where to mount the RAML routes:

{
  ...

  new Raml("docs").install(this);

}

Produces: /docs for api-console and /docs/api.raml. Default path is: /raml.

filter

The filter option controls what is exported to RAML:

{
  ...

  new Raml()
    .filter(route -> {
      return route.pattern().startsWith("/api");
    })
    .install(this);
}

Default filter keeps /api/* routes.

noConsole

This option turn off the api-console:

{
  ...

  new Raml()
    .noConsole()
    .install(this);
}

theme

Set the ui-theme for api-console. Available options are light and dark. Default is: light.

{
  ...

  new Raml()
    .theme("dark")
    .install(this);
}

clientGenerator

Shows/hide the client generator button from api-console.

tryIt

Expand/collapse the try it panel from api-console.

live demo

Check out the a live demo for RAML.

Source code available at github

swagger

Swagger is a simple yet powerful representation of your RESTful API. With the largest ecosystem of API tooling on the planet, thousands of developers are supporting Swagger in almost every modern programming language and deployment environment. With a Swagger-enabled API, you get interactive documentation, client SDK generation and discoverability. More at http://swagger.io ## exports

  • /swagger rout with SwaggerUI
  • /swagger/swagger.json route
  • /swagger/swagger.yml route

dependency

<dependency>
  <groupId>org.jooby</groupId>
  <artifactId>jooby-swagger</artifactId>
  <version>1.1.3</version>
</dependency>

usage

{
  // define your API... via script or MVC:
  /**
   * 
   * Everything about your pets 
   */
   use("/api/pets")
     /**
      * 
      * Get a pet by ID.
      * @param id Pet ID 
      */
     .get("/:id", req -> {
       int id = req.param("id").intValue();
       DB db = require(DB.class);
       Pet pet = db.find(Pet.class, id);
       return pet;
     })
     ...;

  new SwaggerUI().install(this);

}

Now the jooby:spec maven plugin:

<plugin>
  <groupId>org.jooby</groupId>
  <artifactId>jooby-maven-plugin</artifactId>
  <executions>
    <execution>
      <goals>
        <goal>spec</goal>
      </goals>
    </execution>
  </executions>
</plugin>

Start your app and try:

  • The Swagger-UI at /swagger
  • The swagger.json at /swagger/swagger.json
  • The swagger.yml at /swagger/swagger.yml

options

There are a few options available, let’s see what they are:

path

The path option controls where to mount the Swagger routes:

{
  ...

  new SwaggerUI("docs").install(this);

}

Produces: /docs for swagger-ui, /docs/swagger.json and /docs/swagger.yml. Default path is: /swagger.

filter

The filter option controls what is exported to Swagger:

{
  ...

  new SwaggerUI()
    .filter(route -> {
      return route.pattern().startsWith("/api");
    })
    .install(this);
}

Default filter keeps /api/* routes.

tags

One ore more routes are grouped by a tag. The default tag provider produces pets for a route at /api/pets or /pets. You can specify your own tag provider via:

{
  new SwaggerUI()
    .tag(route -> route.name())
    .install(this);
}

noUI

This option turn off the swagger-ui:

{
  ...

  new SwaggerUI()
    .noUI()
    .install(this);
}

live demo

Check out the a live demo for Swagger.

Source code available at github