Guardrail with http4s tutorial

6 minute read

This article is an introduction on how to use Twilio’s Guardrail to safely generate and maintain a http4s REST API server. I wanted to write this article as a reference for my future self and others who are interested in this technology.

real guardrail in its natural habitat

tl;dr - I just want the code!

Find the finished project at guardrail-http4s-tutorial

The case for Guardrail

To quote from Guardrail’s website:

Guardrail is a code generation tool, capable of reading from OpenAPI/Swagger specification files and generating Scala source code, primarily targeting the akka-http and http4s web frameworks, using circe for JSON encoding/decoding.

That’s nice and all, but Swagger is already capable of generating Scala code, why does Guardrail exist at all?

In two words type safety, as we will see it is impossible to do the wrong thing when working with Guardrail’s generated code because the compiler’s type checker will prevent us from making mistakes.

More important, when using Guardrail, we are forced to develop API first and our OpenAPI/Swagger specification functions as the single source of truth for our API’s.

Now, without further ado, let’s build a http4s server using Guardrail!

Prerequisites and setup

For this tutorial we will need sbt. To save some time we are going to use the http4s g8 template, in your terminal do the following:

> sbt new http4s/http4s.g8

I’m going to use the defaults. If you want to have different names, the paths and filenames in this tutorial might be different for you. When sbt is done open the newly created directory. Start with deleting the standard routes since we are not going to use them. Delete the following files and directories:

  • src/main/scala/com/example/quickstart/HelloWorld.scala
  • src/main/scala/com/example/quickstart/Jokes.scala
  • src/main/scala/com/example/quickstart/QuickstartRoutes.scala
  • src/test

In QuickstartServer.scala, remove the references to files you just deleted so that we are left with the following file:

package com.example.quickstart

import cats.effect.{ConcurrentEffect, Effect, ExitCode, IO, IOApp, Timer, ContextShift}
import cats.implicits._
import fs2.Stream
import org.http4s.client.blaze.BlazeClientBuilder
import org.http4s.HttpRoutes
import org.http4s.implicits._
import org.http4s.server.blaze.BlazeServerBuilder
import org.http4s.server.middleware.Logger
import scala.concurrent.ExecutionContext.global

object QuickstartServer {

  def stream[F[_]: ConcurrentEffect](implicit T: Timer[F], C: ContextShift[F]): Stream[F, Nothing] = {
    for {
      client <- BlazeClientBuilder[F](global).stream

      httpApp = (
        HttpRoutes.empty[F] // We will add our own routes here later
      ).orNotFound

      finalHttpApp = Logger.httpApp(true, true)(httpApp)

      exitCode <- BlazeServerBuilder[F]
        .bindHttp(8080, "0.0.0.0")
        .withHttpApp(finalHttpApp)
        .serve
    } yield exitCode
  }.drain
}

Now let’s see if we can still run the application:

> sbt run

The application should compile and we should see some output like this:

[scala-execution-context-global-92] INFO  o.h.s.b.BlazeServerBuilder - http4s v0.20.0 on blaze v0.14.0 started at http://[0:0:0:0:0:0:0:0]:8080/

Congratulations! We have a working http4s application. Right now it just sits there doing nothing, so let us create some endpoints!

API specifications and the Guardrail sbt plugin

For this tutorial, we are going to recreate the Hello World endpoint that we deleted earlier. Save the following specification as src/main/resources/api.yaml:

openapi: "3.0.0"
info:
  title: http4s Guardrail example
  version: 0.0.1
tags:
  - name: hello
paths:
  /hello:
    get:
      tags: [hello]
      x-scala-package: hello
      operationId: getHello
      summary: Returns a hello message
      responses:
        200:
          description: Hello message
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HelloResponse'
components:
  schemas:
    HelloResponse:
      type: object
      properties:
        message:
          type: string
      required:
        - message

Two things to notice in the specification are:

  1. We must provide an operationId for our operation
  2. It is good practice to provide an x-scala-package value to group related operations together.

For the code generation, we are going to use the sbt-guardrail plugin, to project.plugins.sbt add the following line:

addSbtPlugin("com.twilio" % "sbt-guardrail" % "0.46.0")

To build.sbt, append the following lines:

guardrailTasks in Compile := List(
  ScalaServer(
    specPath = (Compile / resourceDirectory).value / "api.yaml",
    pkg = "com.example.quickstart.endpoints",
    framework = "http4s",
    tracing = false
  )
)

And add the following dependency to the existing libraryDependencies:

"io.circe" %% "circe-java8" % CirceVersion,

To see the code generator in action, run:

> sbt compile

And take a look in the target/scala-2.12/src_managed/main/com/example/quickstart directory, this is where our generated code lives, lets see what is there:

The definitions directory contains the case classes that are used as request and response bodies and helper code for serialization and deserialization. For example, we defined a HelloResponse schema in the API specification we got a corresponding HelloResponse.scala file.

The hello package got its name from the x-scala-package value. hello/Routes.scala contains a trait with methods that we must implement. The methods in this trait correspond the operations / operationIds in the API specification.

Http4sImplicits.scala and Implicits.scala contain, well, implicits. They are there to glue everything together.

So far so good, now we must actually implement the endpoint we generated.

Implementing the generated endpoint

Create a new file at src/main/scala/com/example/quickstart/endpoints/hello/HelloHandlerImpl.scala with the following content:

package com.example.quickstart.endpoints.hello

import cats.Applicative
import cats.implicits._
import com.example.quickstart.endpoints.definitions.HelloResponse

class HelloHandlerImpl[F[_] : Applicative]() extends HelloHandler[F] {
  override def getHello(respond: GetHelloResponse.type)(): F[GetHelloResponse] = {
    for {
      message <- "Hello, world".pure[F]
    } yield respond.Ok(HelloResponse(message))
  }
}

What we’ve done here is implement the generated HelloHandler. Looking at the signature of the getHello method we can see Guardrail genius, everything is typed! If this still doesn’t click with you, try to rewrite the change the code to respond with something else than a 200 OK and have it compile (hint, you can’t).

Before we forget, lets add our hello routes to the application, open src/main/scala/com/example/quickstart/QuickstartServer.scala and replace it with:

package com.example.quickstart

import cats.effect.{ConcurrentEffect, Effect, ExitCode, IO, IOApp, Timer, ContextShift}
import cats.implicits._
import com.example.quickstart.endpoints.hello.{HelloHandlerImpl, HelloResource}
import fs2.Stream
import org.http4s.client.blaze.BlazeClientBuilder
import org.http4s.HttpRoutes
import org.http4s.implicits._
import org.http4s.server.blaze.BlazeServerBuilder
import org.http4s.server.middleware.Logger
import scala.concurrent.ExecutionContext.global

object QuickstartServer {

  def stream[F[_]: ConcurrentEffect](implicit T: Timer[F], C: ContextShift[F]): Stream[F, Nothing] = {
    for {
      client <- BlazeClientBuilder[F](global).stream

      httpApp = (
        new HelloResource().routes(new HelloHandlerImpl())
      ).orNotFound

      finalHttpApp = Logger.httpApp(true, true)(httpApp)

      exitCode <- BlazeServerBuilder[F]
        .bindHttp(8080, "0.0.0.0")
        .withHttpApp(finalHttpApp)
        .serve
    } yield exitCode
  }.drain
}

What changed is that we added the line new HelloResource().routes(new HelloHandlerImpl()) to the httpApp.

Now we can run the application again:

> sbt run

And once it’s running we can test our endpoint using curl:

> curl http://localhost:8080/hello
{"message":"Hello, world"}%

🎉 Success! Everything is well in the world now. That is, until the API requirements change…

API specification changes

Guardrail makes changing the API specification a breeze. Earlier I said that we were recreating the standard hello world routes provided by the g8 http4s template. But we are missing something, namely, we want the /hello endpoint to respond with any given name. Let’s change the API specification at src/main/resources/api.yaml to

openapi: "3.0.0"
info:
  title: http4s Guardrail example
  version: 0.0.1
tags:
  - name: hello
paths:
  /hello:
    get:
      tags: [hello]
      x-scala-package: hello
      operationId: getHello
      summary: Returns a hello message
      parameters:
        - $ref: '#/components/parameters/NameParam'
      responses:
        200:
          description: Hello message
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HelloResponse'
components:
  parameters:
    NameParam:
      name: name
      in: query
      description: Name to greet
      schema:
        type: string
  schemas:
    HelloResponse:
      type: object
      properties:
        message:
          type: string
      required:
        - message

What changed is that we added a parameter to the /hello endpoint.

If we trigger the code generator again by calling:

> sbt compile

Guardrail will inform us that we are changing an existing file:

Warning:
  The file ~/Developer/quickstart/target/scala-2.12/src_managed/main/com/example/quickstart/endpoints/hello/Routes.scala contained different content than was expected.

  Existing file: ): F[GetHelloResponse] }\nclass HelloResource[F[_]]
  New file     : name: Option[String] = None): F[GetHelloResponse]

Followed by a bunch of compiler errors. This is actually the compiler telling us that we need to change our implementation because it is out of sync with the generated code. Nice. Open src/main/scala/com/example/quickstart/endpoints/HelloHandlerImpl.scala and replace it with the following:

package com.example.quickstart.endpoints.hello

import cats.Applicative
import cats.implicits._
import com.example.quickstart.endpoints.definitions.HelloResponse

class HelloHandlerImpl[F[_] : Applicative]() extends HelloHandler[F] {
  override def getHello(respond: GetHelloResponse.type)(name: Option[String] = None): F[GetHelloResponse] = {
    for {
      message <- s"Hello, ${name.getOrElse("world")}".pure[F]
    } yield respond.Ok(HelloResponse(message))
  }
}

Now run the application again:

> sbt run

And once it is running we can try to get a personalized greeting:

> curl http://localhost:8080/hello\?name\=Kay
{"message":"Hello, Kay"}%

And that is how easy it is to update your API specification!

Conclusion

In a few minutes we were able to create a simple REST API server with safely typed endpoints generated from an API specification. Better yet, we now have a basis to build our application on. As we have seen Guardrail makes our lives easier by forcing us to stay true to our API specification.

Guardrail is production ready IMO but can be rough around the edges sometimes. If you like Guardrail, they are looking for contributions.

You can find the finished project at guardrail-http4s-tutorial.

Thank you for reading.