loading...

Jooby: Export your Java/Kotlin API to Swagger/RAML

edgarespina profile image edgar ・5 min read

Overview

Documentation is essential while writing software, specially if you write complex business requirements, open source software, APIs, etc..

Everybody wants good documentation, but nobody wants to write it.

Writing good documentation is hard, takes time and always always get out of date.

I found these rules useful while writing documentation:

  1. keep documentation simple (without being too simple)
  2. treat documentation like a requirement
  3. use a tool to keep documentation up to date

For example, JavaDoc (rule #3) it is a nice tool that:

  • keep documentation close to source code
  • generates a static site that reflect the current state of your source code

Of course, you need to keep JavaDoc comments updated but at least the documentation is close enough to your source code and for example can be enforced as part of development process via tooling and/or code reviews (rule #2).

In this post we are going export a Jooby API to Swagger and RAML.

ApiTool

The ApiTool module is similar to JavaDoc. It is a tool that scans your code and produces an intermediate representation that can be exported to Swagger or RAML.

The goals of the ApiTool module are:

  • Developer first. You focus on your API and business requirements (don’t need to think about Swagger/RAML details).
  • Clean source code. Your methods are not full of annotations.
  • Uses JavaDoc. Documentation is extracted from existing JavaDoc comments (write documentation once).

demo

We are going to mimic subset of the Petstore API demo from swagger.io.

To make it a bit more fun we are going to:

  • Write the API using Kotlin
  • Use Jackson for JSON processing
  • Use Hikari database connection pool
  • Run database migrations with Flyway
  • Access database with Jdbi

Sounds like a lot, right? Luckily enough the API is just a couple of lines in Jooby:

/**
 * Kotlin ApiTool.
 */
class App : Kooby({

    /** JSON: */
    use(Jackson())

    /** Database: */
    use(Jdbc())
    use(Flywaydb())
    use(Jdbi3()
            .doWith { jdbi ->
                jdbi.installPlugin(SqlObjectPlugin())
                jdbi.installPlugin(KotlinPlugin())
                jdbi.installPlugin(KotlinSqlObjectPlugin())
            }
            /** Simple transaction per request and bind the PetRepository to it:  */
            .transactionPerRequest(
                    TransactionalRequest()
                            .attach(PetRepository::class.java)
            ))

    /** Export API to Swagger and RAML: */
    use(ApiTool()
            .filter { r -> r.pattern().startsWith("/api") }
            .swagger()
            .raml())

    // Home page redirect to Swagger:
    get { Results.redirect("/swagger") }

    /**
     * Everything about your Pets.
     */
    path("/api/pet") {
        /**
         * List pets ordered by id.
         *
         * @param start Start offset, useful for paging. Default is `0`.
         * @param max Max page size, useful for paging. Default is `20`.
         * @return Pets ordered by name.
         */
        get {
            val db = require(PetRepository::class)

            val start = param("start").intValue(0)
            val max = param("max").intValue(20)

            db.list(start, max)
        }

        /**
         * Find pet by ID
         *
         * @param id Pet ID.
         * @return Returns `200` with a single pet or `404`
         */
        get("/:id") {
            val db = require(PetRepository::class)

            val id = param<Long>("id")

            val pet = db.findById(id) ?: throw Err(Status.NOT_FOUND)

            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 {
            val db = require(PetRepository::class)

            val pet = body<Pet>()

            val id = db.insert(pet)

            Pet(id, pet.name)
        }

        /**
         * Update an existing pet.
         *
         * @param body Pet object that needs to be updated.
         * @return Returns a saved pet.
         */
        put {
            val db = require(PetRepository::class)

            val pet = body<Pet>()
            if (!db.update(pet)) {
                throw Err(Status.NOT_FOUND)
            }
            pet
        }

        /**
         * Deletes a pet by ID.
         *
         * @param id Pet ID.
         * @return A `204`
         */
        delete("/:id") {
            val db = require(PetRepository::class)

            val id = param<Long>("id")

            if (!db.delete(id)) {
                throw Err(Status.NOT_FOUND)
            }
            Results.noContent()
        }
    }
})

We install a couple of modules via use method.

    /** JSON: */
    use(Jackson())

    /** Database: */
    use(Jdbc())
    use(Flywaydb())
    use(Jdbi3()
            .doWith { jdbi ->
                jdbi.installPlugin(SqlObjectPlugin())
                jdbi.installPlugin(KotlinPlugin())
                jdbi.installPlugin(KotlinSqlObjectPlugin())
            }
            /** Simple transaction per request and bind the PetRepository to it:  */
            .transactionPerRequest(
                    TransactionalRequest()
                            .attach(PetRepository::class.java)
            ))

The Jackson module for JSON processing.

The Jdbc module creates a high performance connection pool using Hikari. Database details (url, user, password) are listed in the application.conf file.

The Flyway module picks the database created previously and run database migrations from the src/resources/db/migration directory.

The Jdbi module gives you database access via DAO/Repository like classes. Jdbi is also a very lightweight library you we must install or add new feature via plugins. We add here repository support and Kotlin related plugins.

And finally we install the ApiTool:

    /** Export API to Swagger and RAML: */
    use(ApiTool()
            .filter { r -> r.pattern().startsWith("/api") }
            .swagger()
            .raml())

We don't want to export ALL our routes, only those under the /api path and that is what the filter line does.

The calls swagger() and raml() export our API to those formats.

If you run the application you should see something like:

Jooby+Swagger

apitool goals

Let's review one of the routes:

   /**
     * Everything about your Pets.
     */
    path("/api/pet") {
        /**
         * List pets ordered by id.
         *
         * @param start Start offset, useful for paging. Default is `0`.
         * @param max Max page size, useful for paging. Default is `20`.
         * @return Pets ordered by name.
         */
        get {
            val db = require(PetRepository::class)

            val start = param("start").intValue(0)
            val max = param("max").intValue(20)

            db.list(start, max)
        }
    ...

As promised the ApiTool module

  • let you focus on your API,
  • keep the code clean and
  • complement the output with JavaDoc comments.

There is nothing related to Swagger/RAML in the source code.

final note

The ApiTool module automatically exports your API to Swagger/RAML. You don't need to waste time adding anything extra.

There is a bit of overhead while running the application in development mode, due the ApiTool scans and analyzes the code to produce Swagger/RAML. This overhead is eliminated once you deploy your application by adding a compile-time process to your Maven/Gradle project.

The ApiTool also:

  • Works with Kotlin and Java code
  • Supports script/lambda routes, as well as Mvc routes

Source code is available here

Happy coding!

Posted on by:

edgarespina profile

edgar

@edgarespina

Software engineer. Author of http://jooby.org , http://handlebars.java and others

Discussion

markdown guide