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
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")
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>
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
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>
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)
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)
}
- 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)
- 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")
}
}
Status Endpoint
We'll code now a status endpoint that will always return a JSON with the following content:
{
"healthy": true
}
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)
- 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)
}
}
- 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
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
}
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.
}
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.")
}
}
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}
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
}
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
Once we have everything in place, we mount the new servlet to the ServletContext
on ScalatraBootstrap
.
context.mount(Module.docsServlet, "/docs", "docs")
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"}
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)