[go: up one dir, main page]

DEV Community

Sebastian G. Vinci
Sebastian G. Vinci

Posted on

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

This is the continuation of a post you can find here.

Where we left off...

So, we have an API that has a status endpoint and the code in place to document it with swagger.

Starting the service, we'll see that our documentation is available at http://localhost:8080/docs, and we have a status endpoint at http://localhost:8080/professionals-api/status.

So, let's keep going!

Professionals API

Let's create our model at a package called com.svinci.professionals.api.domain.professional:

  • First, professionals have jobs where they excel, right? And they may have more than one, so we'll need a case class for a job:
case class Job(name: String, description: String)
Enter fullscreen mode Exit fullscreen mode
  • Now, contact information will be displayed or not according whether the client is authenticated or not, so we'll need a case class for the contact information:
case class ContactInformation(phone: String, eMail: String, address: String)
Enter fullscreen mode Exit fullscreen mode
  • And then, we just need a professional. It will have an id, a name, an age, a list of jobs and, optionally, contact information:
case class Professional(id: String, name: String, age: Int, jobs: List[Job], contactInformation: Option[ContactInformation])
Enter fullscreen mode Exit fullscreen mode
  • Also, when a client creates a professional, the id should be generated by the API, and contact information should be mandatory, so we'll need a create request, and it would be nice if it knew how to transform itself to a full professional:
case class ProfessionalCreationRequest(name: String, age: Int, jobs: List[Job], contactInformation: ContactInformation) {

  def toProfessional: Professional = Professional(
    id = UUID.randomUUID().toString,
    name = name,
    age = age,
    jobs = jobs,
    contactInformation = Option(contactInformation)
  )

}
Enter fullscreen mode Exit fullscreen mode

That should do it, let's move on to the repository. Create a file called ProfessionalRepository.scala with the following content, the code is explained with comments:

package com.svinci.professionals.api.domain.professional

import scala.collection.concurrent.TrieMap

/**
  * 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 component holds the interface for professionals repository.
  */
trait ProfessionalRepositoryComponent {

  /**
    * Professional repository instance definition.
    */
  def professionalRepositoryInstance: ProfessionalRepository

  /**
    * Professional repository interface.
    */
  trait ProfessionalRepository {

    /**
      * Receives a creation request, transforms it to a Professional and stores it. Returns the created professional.
      */
    def create(professionalCreationRequest: ProfessionalCreationRequest): Professional

    /**
      * Removes the professional that has the given id. Returns the removed professional if any.
      */
    def remove(professionalId: String): Option[Professional]

    /**
      * Returns the professional that has the given id if any.
      */
    def findById(professionalId: String, displayContactInformation: Boolean): Option[Professional]

    /**
      * Returns a list with all the stored professionals.
      */
    def all(displayContactInformation: Boolean): List[Professional]

  }

}

/**
  * This component defines an implementation of ProfessionalRepository which holds the data in memory.
  */
trait InMemoryProfessionalRepositoryComponent extends ProfessionalRepositoryComponent {

  /**
    * In memory professional repository instantiation.
    */
  override def professionalRepositoryInstance: ProfessionalRepository = new InMemoryProfessionalRepository

  /**
    * In memory implementation of a professional repository.
    */
  class InMemoryProfessionalRepository extends ProfessionalRepository {

    /**
      * This is the data structure chosen to store the professionals.
      * In this Map you'll find professionals indexed by their ids.
      * TrieMap was chosen to make this class thread safe.
      */
    private[this] val storage: TrieMap[String, Professional] = TrieMap()

    /**
      * @inheritdoc
      */
    override def create(professionalCreationRequest: ProfessionalCreationRequest): Professional = {
      val professional = professionalCreationRequest.toProfessional
      storage.put(professional.id, professional)
      professional
    }

    /**
      * @inheritdoc
      */
    override def remove(professionalId: String): Option[Professional] = storage.remove(professionalId)

    /**
      * @inheritdoc
      */
    override def findById(professionalId: String, displayContactInformation: Boolean): Option[Professional] = {
      val maybeProfessional = storage.get(professionalId)

      if (displayContactInformation) {
        return maybeProfessional
      }

      maybeProfessional.map(professional => professional.copy(contactInformation = None))
    }

    /**
      * @inheritdoc
      */
    override def all(displayContactInformation: Boolean): List[Professional] = {
      val allProfessionals = storage.values.toList

      if (displayContactInformation) {
        return allProfessionals
      }

      allProfessionals.map(professional => professional.copy(contactInformation = None))
    }

  }

}
Enter fullscreen mode Exit fullscreen mode

Notice that the read operations receive a flag called displayContactInformation, if it's true, they just return what they have, but if it's false they copy the professionals omitting the contact information.

I wouldn't normally put that logic in the repository, I would have a service layer or something, but as this is a really small API, I prefer keeping it simple.

Now we just need our servlet, we'll need a file called ProfessionalServlet.scala with the following content:

package com.svinci.professionals.api.domain.professional

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 ProfessionalServletComponent {

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

  /**
    * Professional servlet instantiation.
    */
  def professionalServletInstance: ProfessionalServlet = new ProfessionalServlet(professionalRepositoryInstance)

  /**
    * This is the scalatra servlet that will serve our professionals management endpoints.
    */
  class ProfessionalServlet(val repository: ProfessionalRepository) extends ServletSupport {

    /**
      * This value defines the documentation for this endpoint. We are giving the endpoint a name, the return type, a description/summary and the schema for the expected boy.
      */
    private[this] val createProfessional = apiOperation[Professional]("create") summary "Insert a professional in the system" parameter bodyParam[ProfessionalCreationRequest]
    /**
      * We are routing our creation endpoint to the root of this servlet, passing the api operation for swagger to document.
      */
    post("/", operation(createProfessional)) {
      val creationRequest = parsedBody.extract[ProfessionalCreationRequest]
      repository.create(creationRequest)
    }

    /**
      * This value defines the documentation for this endpoint. We are giving the endpoint a name, the return type, a description/summary and the specifications for the id path param.
      */
    private[this] val removeProfessional = apiOperation[Option[Professional]]("remove") summary "Remove a professional by its id." parameter pathParam[String]("Id of the professional to remove")
    /**
      * We are routing our removal endpoint to the root of this servlet with the id as a path param, passing the api operation for swagger to document.
      */
    delete("/:id", operation(removeProfessional)) {
      val professionalId = params('id)
      repository.remove(professionalId)
    }

    /**
      * This value defines the documentation for this endpoint. We are giving the endpoint a name, the return type, a description/summary and the specifications for the id path parameter.
      */
    private[this] val findById = apiOperation[Option[Professional]]("findById") summary "Find a professional by its id." parameter pathParam[String]("id of the professional you are looking for.")
    /**
      * We are routing our findById endpoint to the root of this servlet with the id as a path param, passing the api operation for swagger to document.
      */
    get("/:id", operation(findById)) {
      val professionalId = params('id)
      val authenticated = request.getAttribute("authenticated").asInstanceOf[Boolean]
      repository.findById(professionalId, authenticated)
    }

    /**
      * 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 all = apiOperation[List[Professional]]("allProfessionals") summary "Retrieve all professionals."
    /**
      * We are routing our all endpoint to the root of this servlet, passing the api operation for swagger to document.
      */
    get("/", operation(all)) {
      val authenticated = request.getAttribute("authenticated").asInstanceOf[Boolean]
      repository.all(authenticated)
    }

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

  }

}

/**
  * This is the default instance of ProfessionalServletComponent. Here we define that the ProfessionalServletComponent will use the InMemoryProfessionalRepositoryComponent.
  */
object DefaultProfessionalServletComponent extends ProfessionalServletComponent with InMemoryProfessionalRepositoryComponent

Enter fullscreen mode Exit fullscreen mode

One thing to notice about this servlet is that we are retrieving an attribute from the request to check if the client is authenticated: request.getAttribute("authenticated").asInstanceOf[Boolean]. This attribute should be populated by a filter or a before handler. Let's add it to the ServletSupport trait, in the before execution.

   /**
    * From the servlet request, check if the client is authenticated.
    *
    * This function can be exposed as protected as is, and let the servlets that extend ServletSupport use it,
    * but that wouldn't be convenient in a real situation, as checking if an authentication token is actually valid
    * would have performance implications (query to a database, REST API call, etc.).
    */
  private[this] def isAuthenticated()(implicit request: HttpServletRequest): Boolean = {
    val authenticationToken: Option[String] = Option(request.getHeader("X-PROFESSIONALS-API-TOKEN"))

    // Here you may perform token validation calling an authentication service or something.
    // We'll keep it simple in this example, and we'll assume the client is authenticated if the header is present.
    authenticationToken.isDefined
  }

  /**
   * 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.
   *   - Check if the client is authenticated and add the flag to the request attributes.
   *   - Log that a request arrived.
   */
  before() {

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

    val authenticated = isAuthenticated()
    request.setAttribute("authenticated", authenticated)

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

  }
Enter fullscreen mode Exit fullscreen mode

There, we are creating the function isAuthenticated and calling it from the function we pass to before() that was already written from the previous post.

Now, we need to add the professional servlet to our Module object.

  /**
    * Default instance of ProfessionalServlet
    */
  def professionalServlet: DefaultProfessionalServletComponent.ProfessionalServlet = DefaultProfessionalServletComponent.professionalServletInstance

Enter fullscreen mode Exit fullscreen mode

And now we can mount it in our ScalatraBootstrap class.

    context.mount(Module.professionalServlet, "/professionals-api/professionals", "professionals")
Enter fullscreen mode Exit fullscreen mode

It's time to test it, let's boot the sbt console running sbt at the root of the project and then running jetty:start.

Let's create a professional:

$ curl -v localhost:8080/professionals-api/professionals -X POST -H "Content-Type: application/json" -d '{"name": "Mario", "age": 45, "jobs": [{"name": "Plumber", "description": "Fixing your pipes broh!"}], "contactInformation": {"phone": "555-1234", "eMail": "mario@gmail.com", "address": "Check your pipes ;)"}}'

{"id":"f967b053-68c1-41af-a8b9-27135516c0fc","name":"Mario","age":45,"jobs":[{"name":"Plumber","description":"Fixing your pipes broh!"}],"contactInformation":{"phone":"555-1234","eMail":"mario@gmail.com","address":"Check your pipes ;)"}}
Enter fullscreen mode Exit fullscreen mode

As you see there, the API received the request, and returned the created Mario. Let's retrieve all professionals now, to check the API is actually storing stuff.

$ curl localhost:8080/professionals-api/professionals

[{"id":"f967b053-68c1-41af-a8b9-27135516c0fc","name":"Mario","age":45,"jobs":[{"name":"Plumber","description":"Fixing your pipes broh!"}]}]
Enter fullscreen mode Exit fullscreen mode

There it is! You can see Mario there, yet it hasn't contact information, shall we check that the authentication works?

$ curl localhost:8080/professionals-api/professionals -H "X-PROFESSIONALS-API-TOKEN: 1234"

[{"id":"f967b053-68c1-41af-a8b9-27135516c0fc","name":"Mario","age":45,"jobs":[{"name":"Plumber","description":"Fixing your pipes broh!"}],"contactInformation":{"phone":"555-1234","eMail":"mario@gmail.com","address":"Check your pipes ;)"}}]
Enter fullscreen mode Exit fullscreen mode

Awesome! We can see his mail and phone now. Let's retrieve Mario by his id f967b053-68c1-41af-a8b9-27135516c0fc.

$ curl localhost:8080/professionals-api/professionals/f967b053-68c1-41af-a8b9-27135516c0fc -H "X-PROFESSIONALS-API-TOKEN: 1234"

{"id":"f967b053-68c1-41af-a8b9-27135516c0fc","name":"Mario","age":45,"jobs":[{"name":"Plumber","description":"Fixing your pipes broh!"}],"contactInformation":{"phone":"555-1234","eMail":"mario@gmail.com","address":"Check your pipes ;)"}}
Enter fullscreen mode Exit fullscreen mode

This is great, let's remove Mario now, and move on to check the documentation.

$ curl localhost:8080/professionals-api/professionals/f967b053-68c1-41af-a8b9-27135516c0fc -H "X-PROFESSIONALS-API-TOKEN: 1234" -X DELETE

{"id":"f967b053-68c1-41af-a8b9-27135516c0fc","name":"Mario","age":45,"jobs":[{"name":"Plumber","description":"Fixing your pipes broh!"}],"contactInformation":{"phone":"555-1234","eMail":"mario@gmail.com","address":"Check your pipes ;)"}}

$ curl localhost:8080/professionals-api/professionals -H "X-PROFESSIONALS-API-TOKEN: 1234"
[]
Enter fullscreen mode Exit fullscreen mode

Great, we removed it and then the API is no longer retrieving it. Now, it's time to see how the documentation is working, but I don't want to keep reading JSON, I mean, JSON is a good format, and it's easy to read... but wouldn't a UI be great for this? It shouldn't be that hard to make...

Swagger UI

Well it isn't, and they actually did it. You'll need docker to keep following this example btw. Installation instructions can be found here.

Execute the following command one you have docker installed:

docker run -p 8081:8080 --name swagger-ui-instance -d swaggerapi/swagger-ui:v2.2.9
Enter fullscreen mode Exit fullscreen mode

This will boot a docker container that serves swagger ui with an NGINX. It doesn't matter anyway. You just visit http://localhost:8081 in your browser, find the text input in the top navigation bad, type http://localhost:8080/docs there and hit Explore.

There you have it! An interactive graphic interface to see your API documentation.

See the version tag v2.2.9 we are setting when we run the docker instance? That's important. As I said on the previous post, scalatra swagger provides Swagger V1 JSONS, which are deprecated, and v2.2.9 of swagger UI is the latest version that support Swagger V1.

Packaging a standalone JAR

This is a quick step, we just need to add two lines to the build.sbt file, as most of the setup was already done when we started the project:

mainClass in Compile := Some("com.svinci.professionals.api.JettyLauncher")
mainClass in assembly := Some("com.svinci.professionals.api.JettyLauncher")
Enter fullscreen mode Exit fullscreen mode

This way, we are telling assembly which is our main class. That JettyLauncher object was written in the previous post and it's an object that's capable of starting the JettyServer as the scalatra plugin does.

Now we'll run some commands:

  • First, to assembly the JAR we run sbt assembly. There are going to be a couple WARN lines of log, but they aren't important.
  • Second, we go to the directory where the JAR is located doing cd target/scala-2.11.
  • There you'll find the JAR called professionals-api.jar. By doing java -jar professionals-api.jar you can run the JAR with the default configuration.
  • The configuration present in application.conf can be overridden wit java options as in java -Dapplication.port=8082 -jar professionals-api.jar <- this command will run the server but it will listen in the port 8082.

Conclusion

Scala, SBT, Scalatra and Swagger are awesome. Yes, I'd like to move to Swagger V2, but it isn't that problematic. APIs written with these technologies work just fine and perform very well.

You can find the code to this post series here.

Thanks a lot for reading!

See you in the comments!

Top comments (0)