Creating a REST Service with Smithy4s
Smithy4s is a library that provides tooling for the Smithy IDL in Scala. One of the things Smithy4s does is allow you to easily create a REST service based on a Smithy specification. In general, it will eliminate your need for maintaining the API layer of your service altogether. Smithy4s will take care of HTTP routes, JSON codecs, and more.
Contract-First and Smithy
Smithy is an Interface Definition Language (IDL) that AWS created several years ago. If you’ve ever come in contact with OpenAPI (formerly Swagger), you can consider Smithy to be an alternative to it. Here’s an example of a simple Hello World service defined with the Smithy IDL.
$version: "2"
namespace hello
use alloy#simpleRestJson
@simpleRestJson
service HelloWorldService {
operations: [HelloWorld]
}
@http(method: "GET", uri: "/hello")
operation HelloWorld {
input := {
@httpQuery("name")
name: String
}
output := {
@required
message: String
}
}
This is a service, called HelloWorldService
, that has a single operation (endpoint) called HelloWorld
. The HelloWorld
operation defines an HTTP GET
endpoint that takes a name
as an input from a query parameter and returns a structure as JSON.
The simpleRestJson
trait annotation on the HelloWorldService
is defining what protocol the service uses.
Smithy4s Setup
Below we will walk through everything you’ll need to get Smithy4s setup, generating code and serving your API. This guide was written for Smithy4s 0.18.x. You'll want to check for the latest version here before getting started on your own application.
Note that there is a quick way to get your entire smithy4s application set up using the giter8 template. This would be my recommendation, but the guide below exists to break it down and talk through the individual pieces.
project/plugins.sbt
This will add the smithy4s code generation plugin to your build. This is the piece that allows smithy4s to find your Smithy files and generate code from them.
addSbtPlugin("com.disneystreaming.smithy4s" % "smithy4s-sbt-codegen" % "0.18.27")
build.sbt
Here we create a basic build, enable the Smithy4sCodegenPlugin
, and add the dependencies needed to use Smithy4s to create a REST service with http4s. The smithy4s-http4s-swagger
dependency is optional. It is only used if you wish to host a SwaggerUI from your service. Smithy4s has the ability to convert your Smithy specifications into OpenAPI such that a SwaggerUI can be served. This is all done with only a single line of code in your actual application (you'll see later in this post).
Note that we use smithy4sVersion.value
to get the version for the various Smithy4s dependencies. This will automatically keep these versions in sync with the one defined in your plugins.sbt
file. Then when you update your Smithy4s version, you'll only need to do so in a single place.
ThisBuild / scalaVersion := "2.13.12"
lazy val root = (project in file("."))
.enablePlugins(Smithy4sCodegenPlugin)
.settings(
name := "example",
libraryDependencies ++= Seq(
"com.disneystreaming.smithy4s" %% "smithy4s-http4s" % smithy4sVersion.value,
"com.disneystreaming.smithy4s" %% "smithy4s-http4s-swagger" % smithy4sVersion.value,
"org.http4s" %% "http4s-ember-server" % "0.23.30"
),
Compile / run / fork := true
)
src/main/smithy
This is the same smithy file we defined above. You can customize the location of your Smithy files, but by default the Smithy4s plugin will look for them in src/main/smithy
.
$version: "2"
namespace hello
use alloy#simpleRestJson
@simpleRestJson
service HelloWorldService {
operations: [HelloWorld]
}
@http(method: "GET", uri: "/hello")
operation HelloWorld {
input := {
@httpQuery("name")
name: String
}
output := {
@required
message: String
}
}
src/main/scala/example/HelloWorldServiceImpl.scala
This is where we put our business logic to handle incoming requests. Smithy4s will handle all the mechanics around deserializing incoming requests and calling the appropriate handler function. From there it will take the output provided by the handler function and will serialize and send the response.
package example
import hello._
import cats.effect.IO
object HelloWorldServiceImpl extends HelloWorldService[IO] {
override def helloWorld(name: Option[String]): IO[HelloWorldOutput] = {
val n = name.getOrElse("World")
IO.pure(HelloWorldOutput(s"Hello, $n!"))
}
}
src/main/scala/example/Routes.scala
Here we use the SimpleRestJsonBuilder
provided by the smithy4s-http4s
module. This is what will give us an instance of HttpRoutes
that we can use to bootstrap our application.
Additionally, we are using the swagger docs add-on brought in from the smithy4s-http4s-swagger
module. This gives us the ability to host a SwaggerUI for our api at the path /docs
(the path is customizable).
package example
import hello._
import org.http4s._
import cats.syntax.all._
import cats.effect.{IO, Resource}
import smithy4s.http4s.SimpleRestJsonBuilder
object Routes {
private val hello: Resource[IO, HttpRoutes[IO]] =
SimpleRestJsonBuilder.routes(HelloWorldServiceImpl).resource
private val docs: HttpRoutes[IO] =
smithy4s.http4s.swagger.docs[IO](HelloWorldService)
val all: Resource[IO, HttpRoutes[IO]] = hello.map(_ <+> docs)
}
src/main/scala/example/Main.scala
Finally, we get to the entry point of our application. There is nothing specific to Smithy4s here, rather we are just taking the HttpRoutes
that Smithy4s helped us create and passing them into the EmberServerBuilder
(we could use Blaze or a different http4s backend just the same).
package example
import cats.effect.{IOApp, IO}
import cats.syntax.all._
import com.comcast.ip4s._
import org.http4s.ember.server.EmberServerBuilder
import scala.concurrent.duration._
object Main extends IOApp.Simple {
val run = Routes.all.flatMap { routes =>
EmberServerBuilder
.default[IO]
.withPort(port"9000")
.withHost(host"localhost")
.withHttpApp(routes.orNotFound)
.withShutdownTimeout(1.second)
.build
}.useForever
}
Running
Now that we have our application built out, you can run it with sbt run
and then navigate to http://localhost:9000/docs in your browser to check it out.
Note that you can find the generated code in your project by looking inside target/scala-2.13/src_managed
(the directory will change if you’re using a different Scala version).
Conclusion
Smithy4s makes application development more pleasant by taking away the need to worry about the API layer so you can focus on your business logic. It also helps you use a contract-first paradigm in building your application. This means your API documentation will never be out of date again. If you would like to learn more, take a look at the smithy4s documentation.