[go: up one dir, main page]

DEV Community

Sebastian G. Vinci
Sebastian G. Vinci

Posted on

Scala REST API documented with Swagger (Part 1 of 2)

In this post series we are going to build a REST API using SBT, Scala and Scalatra. Something I like a lot about Scalatra is that it has a library to document your API using Swagger with very little efort.

Our API will be very simple. It will manage a catalog of professionals, but contact information will only be provided if the client is authenticated.

DOWNSIDE: Scalatra swagger only supports swagger v1, which is deprecated now. There are tools to convert swagger v1 JSONs to swagger v2, and even to RAML, so this doesn't really bother me.

What we'll build on this part

Part 1 will only consist of the set up of the project, a status endpoint and the implementation of a servlet to retrieve swagger documentation.

The endpoints to manage professionals, the authentication filter, the standalone build (jar deployment) and a UI to see the API documentation on an interactive manner will be approached on the second part.

Preconditions

I'll assume you are familiar with the Scala language, SBT and REST.

To work through this tutorial you'll need to install JDK 8 (open jdk is fine) and SBT.

All the commands I'll provide to install, run and do stuff were tested on Linux Mint 18. They'll probably work on Mac OS without any issue. If you're working on windows I'm really sorry.

Set Up

I'll show how to create this project by hand, but there are project templates and stuff you can use. I think SBT has a command to generate empty projects, but also lighbend activator is a fine tool to start from a template.

First of all, let's create our project directory running mkdir scala-scalatra-swagger-example.

Now, this are the directories you'll need to have:

  • project
  • src/main/resources
  • src/main/scala
  • src/main/webapp/WEB-INF

Under project directory, you need to create a file called build.properties with the following content:

sbt.version=0.13.13

Enter fullscreen mode Exit fullscreen mode

We are defining our project to be built using sbt 0.13.13.

Also, under project directory, you'll need a file called plugins.sbt with the following content:

addSbtPlugin("org.scalatra.sbt" % "scalatra-sbt" % "0.5.1")

addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.4")

addSbtPlugin("org.scalariform" % "sbt-scalariform" % "1.6.0")
Enter fullscreen mode Exit fullscreen mode

Here we are importing scalatra SBT plugin, needed to work with scalatra (it will provide some tools to work with Jetty and stuff). Then, we import assembly, which we'll use to build our standalone jar. Finally, we are adding scalariform, so our code gets formated on compile time.

Now, under src/main/webapp/WEB-INF we'll need a file called web.xml with the following content:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
      version="3.0">

  <!--
    This listener loads a class in the default package called ScalatraBootstrap.
    That class should implement org.scalatra.LifeCycle.  Your app can be
    configured in Scala code there.
  -->
  <listener>
    <listener-class>org.scalatra.servlet.ScalatraListener</listener-class>
  </listener>
</web-app>

Enter fullscreen mode Exit fullscreen mode

This basically tells jetty that our only listener is the one provided by Scalatra.

Now, under src/main/resources we'll need our application configuration file, called application.conf.

application.port=8080

Enter fullscreen mode Exit fullscreen mode

Yes, it only contains the port where the API will listen, but I wanted to show how configuration will work on an application like this, as it is a real need in production.

Also, logging is a necessity in production, so, under src/main/resources too, we'll need our logback.xml.

<configuration>

  <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <pattern>[%d] [%level] [tid: %X{tid:-none}] [%t] [%logger{39}] : %m%n%rEx{10}</pattern>
      <charset>utf8</charset>
    </encoder>
  </appender>

  <root level="INFO">
    <appender-ref ref="CONSOLE"/>
  </root>

</configuration>

Enter fullscreen mode Exit fullscreen mode

This only defines a console appender (sends logs to the standard output), with a really standard pattern (the only weird thing is the tid thingy, we'll talk about it later).

Now we'll glue everything together with a build.sbt file.

import com.typesafe.sbt.SbtScalariform
import com.typesafe.sbt.SbtScalariform.ScalariformKeys
import org.scalatra.sbt._

import scalariform.formatter.preferences._

val ScalatraVersion = "2.5.0"

ScalatraPlugin.scalatraSettings

organization := "com.svinci.professionals"

name := "api"

version := "0.0.1-SNAPSHOT"

scalaVersion := "2.11.8"

resolvers += Classpaths.typesafeReleases

SbtScalariform.scalariformSettings

ScalariformKeys.preferences := ScalariformKeys.preferences.value
  .setPreference(AlignSingleLineCaseStatements, true)
  .setPreference(DoubleIndentClassDeclaration, true)
  .setPreference(AlignParameters, true)
  .setPreference(AlignArguments, true)

libraryDependencies ++= Seq(
  "org.scalatra"        %%  "scalatra"          % ScalatraVersion,
  "org.scalatra"        %%  "scalatra-json"     % ScalatraVersion,
  "org.scalatra"        %%  "scalatra-swagger"  % ScalatraVersion,
  "org.json4s"          %%  "json4s-native"     % "3.5.0",
  "org.json4s"          %%  "json4s-jackson"    % "3.5.0",
  "com.typesafe"        %   "config"            % "1.3.1",
  "ch.qos.logback"      %   "logback-classic"   % "1.1.5"             % "runtime",
  "org.eclipse.jetty"   %   "jetty-webapp"      % "9.2.15.v20160210"  % "container;compile",
  "javax.servlet"       %   "javax.servlet-api" % "3.1.0"             % "provided"
)

assemblyJarName in assembly := "professionals-api.jar"

enablePlugins(JettyPlugin)

Enter fullscreen mode Exit fullscreen mode

What's this build.sbt doing?

  • At the top of the file we are importing our plugins and the things we need to set up the build of our project.
  • Then we define a value containing scalatra version (2.5.0) and applying ScalatraPlugin.scalatraSettings.
  • After that, we are defining some artifact information (organization, name, version, scala version), adding typesafe repository (lightbend used to be called typesafe) to our resolvers and then we configure scalariform.
  • Then, our dependencies are being defined. Notice scalatra dependencies, json4s, config, logback and jetty.
  • Then, we are configuring assembly, so everything is packaged under a jar called professionals-api.jar
  • Finally, we are enabling jetty plugin (it comes with the scalatra plugin).

At this point, we can run sbt on the root of the project to see that it's loaded correctly, and even compile it (although there is no code to compile).

Now it would be the time to open this project with an IDE (yes, I've been using vim up to this point).

Utility objects and traits

We will create now some infrastructure for our code. First, let's create our package com.svinci.professionals.api.infrastructure. Under this package, let's write the following (the code is explained with comments):

  • A file called Configuration.scala with the following content:
package com.svinci.professionals.api.infrastructure

import com.typesafe.config.ConfigFactory

/**
  * This will provide a singleton instance of our configuration.
  * Also, it will encapsulate typesafe.config, so the rest of the application doesn't need to know about the configuration library we are using.
  */
object Configuration {

  /**
    * This is our configuration instance. Private and immutable.
    */
  private[this] val configuration = ConfigFactory.load()

  /**
    * Methods like this one should be defined to access any type of configuration from its key.
    * The reason we do it is to define an interface that makes sense for our application, and make the rest of the code
    * agnostic to what library we are using. Just a good practice.
    * @param key Configuration key.
    * @return The configured Int.
    */
  def getInt(key: String): Int = configuration.getInt(key)

}

Enter fullscreen mode Exit fullscreen mode
  • A file called ApiInformation.scala with the following content:
package com.svinci.professionals.api.infrastructure

import org.scalatra.swagger.{ApiInfo, Swagger}

/**
  * Information of our API as a whole.
  */
object ProfessionalsApiInfo extends ApiInfo(
  title = "professionals-api",
  description = "Professionals CRUD operations.",
  termsOfServiceUrl = "some terms of service URL",
  contact = "some contact information",
  license = "MIT",
  licenseUrl = "http://opensource.org/licenses/MIT"
)

/**
  * Swagger instance for our API. It's defined  as an object so we have only one instance for all our resources.
  */
object ProfessionalsApiSwagger extends Swagger(swaggerVersion = Swagger.SpecVersion, apiVersion = "1.0.0", apiInfo = ProfessionalsApiInfo)
Enter fullscreen mode Exit fullscreen mode
  • A file called ServletSupport.scala with the following content:
package com.svinci.professionals.api.infrastructure

import java.util.{Date, UUID}

import org.json4s.{DefaultFormats, Formats}
import org.scalatra._
import org.scalatra.json._
import org.scalatra.swagger.SwaggerSupport
import org.slf4j.{LoggerFactory, MDC}

/**
  * We'll have a couple servlets probably (a status endpoint, the CRUD servlet for our professionals, and if we deploy this to production we'll probably add some more),
  * so it's convenient to have the things every servlet will need to define in one trait to extend it.
  * 
  * This trait extends ScalatraServlet and adds json and swagger support.
  */
trait ServletSupport extends ScalatraServlet with JacksonJsonSupport with SwaggerSupport {

  /**
    * As we are going to document every endpoint of our API, we'll need our swagger instance in everyone of our servlets.
    */
  override protected implicit def swagger = ProfessionalsApiSwagger

  /**
    * This is a logger... to log stuff.
    */
  private[this] val logger = LoggerFactory.getLogger(getClass)
  /**
    * Scalatra requires us to define an implicit Formats instance for it to know how we want JSONs to be serialized/deserialized.
    * It provides a DefaultFormats that fill all our needs today, so we'll use it. 
    */
  protected implicit lazy val jsonFormats: Formats = DefaultFormats

  /**
    * Before every request made to a servlet that extends this trait, the function passed to `before()` will be executed.
    * We are using this to :
    *   - Set the Content-Type header for every request, as we are always going to return JSON.
    *   - Set the date to the request, so we can calculate spent time afterwards.
    *   - Generate a transaction identifier, an add it to the MDC, so we know which lines of logs were triggered by which request.
    *   - Log that a request arrived.
    */
  before() {

    contentType = "application/json"
    request.setAttribute("startTime", new Date().getTime)
    MDC.put("tid", UUID.randomUUID().toString.substring(0, 8))

    logger.info(s"Received request ${request.getMethod} at ${request.getRequestURI}")

  }

  /**
    * NotFound handler. We just want to set the status code, and avoid the huge stack traces scalatra returns in the body.
    */
  notFound {

    response.setStatus(404)

  }

  /**
    * After every request made to a servlet that extends this trait, the function passed to `after()` will be executed.
    * We are using this to:
    *   - Retrieve the start time added in the `before()` handler an calculate how much time the API took to respond.
    *   - Log that the request handling finished, with how much time it took.
    */
  after() {

    val startTime: Long = request.getAttribute("startTime").asInstanceOf[Long]
    val spentTime: Long = new Date().getTime - startTime
    logger.info(s"Request ${request.getMethod} at ${request.getRequestURI} took ${spentTime}ms")

  }

}

Enter fullscreen mode Exit fullscreen mode

Status Endpoint

We'll code now a status endpoint that will always return a JSON with the following content:

{
  "healthy": true
}
Enter fullscreen mode Exit fullscreen mode

If you had a database, or an API you depend on, you could add their statuses there. First of all, let's create our package com.svinci.professionals.api.domain.status and, under this package, write the following (the code is explained with comments):

  • A file called Status.scala with the following content:
package com.svinci.professionals.api.domain.status

/**
  * This is the object our status endpoint will convert to JSON and return.
  */
case class Status(healthy: Boolean)


Enter fullscreen mode Exit fullscreen mode
  • A file called StatusService.scala with the following content:
package com.svinci.professionals.api.domain.status

/**
  * We are using cake pattern to solve dependency injection without using any library. You can find a really good explanation of this pattern at http://www.cakesolutions.net/teamblogs/2011/12/19/cake-pattern-in-depth.
  * 
  * This is the component that defines a StatusService interface (trait, actually), and names an instance of it.
  */
trait StatusServiceComponent {

  /**
    * This is the definition of the instance of StatusService an implementation of this component will hold.
    */
  def statusServiceInstance: StatusService

  /**
    * StatusService interface definition.
    */
  trait StatusService {

    /**
      * Retrieve the application status.
      * @return The application status.
      */
    def status: Status

  }

}

/**
  * Default StatusServiceComponent implementation.
  */
trait DefaultStatusServiceComponent extends StatusServiceComponent {

  /**
    * Here, we define how the DefaultStatusService is created.
    */
  override def statusServiceInstance: StatusService = new DefaultStatusService

  /**
    * Default StatusService implementation.
    */
  class DefaultStatusService extends StatusService {

    /**
      * @inheritdoc
      */
    override def status: Status = Status(healthy = true)

  }

}
Enter fullscreen mode Exit fullscreen mode
  • A file called StatusServlet.scala with the following content:
package com.svinci.professionals.api.domain.status

import com.svinci.professionals.api.infrastructure.ServletSupport

/**
  * We are using cake pattern to solve dependency injection without using any library. You can find a really good explanation of this pattern at http://www.cakesolutions.net/teamblogs/2011/12/19/cake-pattern-in-depth.
  *
  * As this is an entry point to our application, there is no need to create an interface (it's a servlet after all, so there are no functions exposed).
  */
trait StatusServletComponent {

  /**
    * As defined by cake pattern, with self type annotations we are defining that any class that extends this trait, needs to extend StatusServiceComponent too.
    * This makes the interface and instance defined by StatusServiceComponent available in this trait.
    */
  this: StatusServiceComponent =>

  /**
    * This is the StatusServlet instance held by this component. Notice that we are instantiating StatusServlet passing the statusServiceInstance provided by StatusServiceComponent.
    */
  def statusServletInstance: StatusServlet = new StatusServlet(statusService = statusServiceInstance)

  /**
    * This is the scalatra servlet that will serve our status endpoint.
    */
  class StatusServlet(val statusService: StatusService) extends ServletSupport {

    /**
      * This value defines the documentation for this endpoint. We are giving the endpoint a name, the return type and a description/summary.
      */
    private[this] val getStatus = apiOperation[Status]("status") summary "Retrieve API status."

    /**
      * We are routing our status endpoint to the root of this servlet, and passing to scalatra our apiOperation.
      */
    get("/", operation(getStatus)) {
      statusService.status
    }

    /**
      * This is the description of this servlet, requested by swagger.
      */
    override protected def applicationDescription: String = "API Status."

  }

}

/**
  * This is the default instance of StatusServletComponent. Here we define that the StatusServletComponent will use the DefaultStatusServiceComponent.
  */
object DefaultStatusServletComponent extends StatusServletComponent with DefaultStatusServiceComponent

Enter fullscreen mode Exit fullscreen mode

Now we go back to the package called com.svinci.professionals.api.infrastructure and create a file called Module.scala with the following content:

package com.svinci.professionals.api.infrastructure

import com.svinci.professionals.api.domain.status.DefaultStatusServletComponent

/**
  * We are using cake pattern to solve dependency injection without using any library. You can find a really good explanation of this pattern at http://www.cakesolutions.net/teamblogs/2011/12/19/cake-pattern-in-depth.
  *
  * In this object we'll hold all the instances required by our application.
  */
object Module {

  /**
    * Default instance of StatusServlet.
    */
  def statusServlet: DefaultStatusServletComponent.StatusServlet = DefaultStatusServletComponent.statusServletInstance

}

Enter fullscreen mode Exit fullscreen mode

Now, for our API to run we'll need an application to run, just as any scala application. Under a package called com.svinci.professionals.api we'll write a file called JettyLauncher.scala with the following content:

package com.svinci.professionals.api

import com.svinci.professionals.api.infrastructure.Configuration
import org.eclipse.jetty.server.Server
import org.eclipse.jetty.servlet.DefaultServlet
import org.eclipse.jetty.webapp.WebAppContext
import org.scalatra.servlet.ScalatraListener

/**
  * This is the MainClass of our application.
  */
object JettyLauncher extends App {

  /**
    * Server instantiation. We are retrieving the port we need to listen at from our Configuration object.
    */
  val server = new Server(Configuration.getInt("application.port"))

  /**
    * Web application context instantiation.
    * This class will hold the context path, the resource base, the event listener and one servlet.
    */
  val context = new WebAppContext()

  context setContextPath "/"
  context.setResourceBase("src/main/webapp")
  context.addEventListener(new ScalatraListener)  // We use scalatra listener as event listener.
  context.addServlet(classOf[DefaultServlet], "/")  // We don't need to add our servlets here, we're going to add them using the scalatra life cycle.

  server.setHandler(context)  // We add the WebAppContext to the server.

  server.start()  // Start the server.
  server.join()  // Join the server's thread pool so the application doesn't quit.

}
Enter fullscreen mode Exit fullscreen mode

And now we need to add our servlets to the scalatra lyfe cycle, so we write a file called ScalatraBootstrap.scala under the src/main/scala directory (outside any package) with the following content:

package com.svinci.professionals.api

import javax.servlet.ServletContext

import com.svinci.professionals.api.infrastructure.Module
import org.scalatra._
import org.slf4j.LoggerFactory

/**
  * This class in looked up by scalatra and automatically instantiated. Here we need to code all the start up code of our API.
  */
class ScalatraBootstrap extends LifeCycle {

  private[this] val logger = LoggerFactory.getLogger(getClass)

  /**
    * In the method we need to mount our servlets to the ServletContext provided as parameter.
    * Any additional startup code should be wrote here too (warm up, scheduled tasks initialization, etc.).
    */
  override def init(context: ServletContext) {

    logger.info(context.getContextPath)

    logger.info("Mounting Changas API servlets.")
    context.mount(Module.statusServlet, "/professionals-api/status", "status")
    logger.info(s"API started.")

  }

}

Enter fullscreen mode Exit fullscreen mode

As you can see there, we are mounting our status servlet to the path /professionals-api/status, so all the routing we do inside the servlet will be relative to that path. The third parameter we pass to the mount method is a name we are assigning to that servlet.

This class needs to be in that location so scalatra can find it. If you place it anywhere else you'll see an assertion error: java.lang.AssertionError: assertion failed: No lifecycle class found!.

Now it's time to test our server and our status endpoint. Boot the sbt console by running sbt on the root of the project, and once inside run jetty:start. You'll have control over sbt console after executing that command, and to stop the API you can run jetty:stop. Of course, CTRL + C will work too, but that will close the sbt console too.

You can test the API now:

$ curl http://localhost:8080/professionals-api/status
{"healthy":true}
Enter fullscreen mode Exit fullscreen mode

Great, stop the server now and we'll get back to coding.

Swagger Documentation

Now we'll create the servlet that will return our endpoints documentation.

First, we need to create a package called com.svinci.professionals.api.domain.docs, and under that package we'll write a file called DocsServlet.scala with the following content:

package com.svinci.professionals.api.domain.docs

import com.svinci.professionals.api.infrastructure.ProfessionalsApiSwagger
import org.scalatra.ScalatraServlet
import org.scalatra.swagger.NativeSwaggerBase

/**
  * This servlet, as is, will be able to return swagger v1 JSONs. This is the entry point to our documentation.
  */
class DocsServlet extends ScalatraServlet with NativeSwaggerBase {

  /**
    * Application swagger global instance.
    */
  override protected implicit def swagger = ProfessionalsApiSwagger

}
Enter fullscreen mode Exit fullscreen mode

This is a really simple servlet, so I didn't feel like doing cake pattern here, it didn't make sense. Now, in the Module.scala object we'll need to add a new function to retrieve an instance of this servlet:

  /**
    * Swagger documentation servlet instance.
    */
  def docsServlet: DocsServlet = new DocsServlet
Enter fullscreen mode Exit fullscreen mode

Once we have everything in place, we mount the new servlet to the ServletContext on ScalatraBootstrap.

context.mount(Module.docsServlet, "/docs", "docs")
Enter fullscreen mode Exit fullscreen mode

We can now start our server again and test:


$ curl http://localhost:8080/docs
{"apiVersion":"1.0.0","swaggerVersion":"1.2","apis":[{"path":"/professionals-api/status","description":"API Status."}],"authorizations":{},"info":{}}

$ curl http://localhost:8080/docs/professionals-api/status
{"apiVersion":"1.0.0","swaggerVersion":"1.2","resourcePath":"/professionals-api/status","produces":["application/json"],"consumes":["application/json"],"protocols":["http"],"apis":[{"path":"/professionals-api/status/","operations":[{"method":"GET","summary":"Retrieve API status.","position":0,"notes":"","deprecated":false,"nickname":"status","parameters":[],"type":"Status"}]}],"models":{"Status":{"id":"Status","name":"Status","qualifiedType":"com.svinci.professionals.api.domain.status.Status","required":["healthy"],"properties":{"healthy":{"position":0,"type":"boolean"}}}},"basePath":"http://localhost:8080"}
Enter fullscreen mode Exit fullscreen mode

Notice that general information of our API is found at /docs, and then at /docs/professionals-api/status you'll find documentation for the servlet that was mounted on /professionals-api/status.

Conclusion

That's it in this part of the post series. We have an API working, with a status endpoint and swagger documentation.

Right now the second part hasn't been written, but in a couple days I'll have it done and I will put the link here.

The code to this example can be found here.

See you in the comments!

Top comments (0)