Building REST APIs with Smithy4s
Smithy4s is a code generation tool that creates Scala server and client code from Smithy specifications. It works with http4s and gives you type-safety across your API.
Why Smithy4s?
When you build APIs the traditional way, you often need to:
- Write a lot of boilerplate code for serialization
- Keep server, client, and docs in sync manually
- Deal with runtime errors when the API changes
Smithy4s fixes this by:
- Code generation: Creates server and client code from one definition
- Type-safety: The compiler catches errors before runtime
- Protocol support: Works with REST JSON and other formats
- Good integration: Works well with http4s and Cats Effect
Getting Started
Project Setup
Add the Smithy4s plugin to your project/plugins.sbt:
addSbtPlugin("com.disneystreaming.smithy4s" % "smithy4s-sbt-codegen" % "0.18.15")
Enable the plugin in build.sbt:
lazy val root = project
.in(file("."))
.enablePlugins(Smithy4sCodegenPlugin)
.settings(
name := "smithy4s-demo",
scalaVersion := "3.3.1",
libraryDependencies ++= Seq(
"com.disneystreaming.smithy4s" %% "smithy4s-http4s" % "0.18.15",
"org.http4s" %% "http4s-ember-server" % "0.23.23",
"org.http4s" %% "http4s-ember-client" % "0.23.23"
)
)
Define Your API
Create a Smithy specification in src/main/smithy/bookstore.smithy:
$version: "2"
namespace demo.bookstore
use alloy#simpleRestJson
@simpleRestJson
service BookstoreService {
operations: [GetBook, ListBooks, CreateBook]
}
@readonly
@http(method: "GET", uri: "/books/{isbn}")
operation GetBook {
input := {
@required
@httpLabel
isbn: String
}
output := {
@required
book: Book
}
errors: [BookNotFound]
}
@readonly
@http(method: "GET", uri: "/books")
operation ListBooks {
output := {
@required
books: Books
}
}
@http(method: "POST", uri: "/books")
operation CreateBook {
input := {
@required
book: Book
}
output := {
@required
book: Book
}
}
structure Book {
@required
isbn: String
@required
title: String
@required
author: String
year: Integer
}
list Books {
member: Book
}
@error("client")
@httpError(404)
structure BookNotFound {
@required
message: String
}
Implement the Service
The code generation creates a trait BookstoreService[F[_]] that you implement:
import cats.effect.*
import cats.syntax.all.*
import demo.bookstore.*
class BookstoreServiceImpl[F[_]: Sync] extends BookstoreService[F] {
private var books = Map(
"978-0134685991" -> Book(
isbn = "978-0134685991",
title = "Effective Java",
author = "Joshua Bloch",
year = Some(2018)
)
)
def getBook(isbn: String): F[GetBookOutput] =
books.get(isbn) match {
case Some(book) => GetBookOutput(book).pure[F]
case None =>
Sync[F].raiseError(
BookNotFound(s"Book with ISBN $isbn not found")
)
}
def listBooks(): F[ListBooksOutput] =
ListBooksOutput(books.values.toList).pure[F]
def createBook(book: Book): F[CreateBookOutput] =
Sync[F].delay {
books = books + (book.isbn -> book)
CreateBookOutput(book)
}
}
Create the Server
import cats.effect.*
import com.comcast.ip4s.*
import org.http4s.ember.server.EmberServerBuilder
import smithy4s.http4s.SimpleRestJsonBuilder
object Main extends IOApp.Simple {
def run: IO[Unit] = {
val bookstoreImpl = new BookstoreServiceImpl[IO]
SimpleRestJsonBuilder
.routes(bookstoreImpl)
.resource
.flatMap { routes =>
EmberServerBuilder
.default[IO]
.withHost(host"0.0.0.0")
.withPort(port"9000")
.withHttpApp(routes.orNotFound)
.build
}
.use(_ => IO.println("Server started on port 9000") *> IO.never)
}
}
Use the Generated Client
Smithy4s also generates a type-safe client:
import cats.effect.*
import org.http4s.ember.client.EmberClientBuilder
import smithy4s.http4s.SimpleRestJsonBuilder
object ClientDemo extends IOApp.Simple {
def run: IO[Unit] =
EmberClientBuilder.default[IO].build.use { httpClient =>
SimpleRestJsonBuilder(BookstoreService)
.client(httpClient)
.uri(uri"http://localhost:9000")
.resource
.use { client =>
for {
// List all books
allBooks <- client.listBooks()
_ <- IO.println(s"Books: ${allBooks.books}")
// Get a specific book
book <- client.getBook("978-0134685991")
_ <- IO.println(s"Book: ${book.book}")
// Create a new book
newBook = Book(
isbn = "978-1617295027",
title = "Functional Programming in Scala",
author = "Paul Chiusano",
year = Some(2014)
)
created <- client.createBook(newBook)
_ <- IO.println(s"Created: ${created.book}")
} yield ()
}
}
}
Key Benefits
Single Source of Truth: Your Smithy spec defines both server and client behavior.
Type-Safety: The compiler catches contract violations at build time.
Documentation: You can generate OpenAPI specs from your Smithy definitions.
Protocol Flexibility: Support REST JSON, AWS protocols, or custom formats.
Scala 3 Ready: Full support for Scala 3 with good type inference.
Advanced Features
Custom Protocols
Smithy4s supports custom protocols beyond REST JSON. You can define your own serialization formats.
Middleware Integration
Since Smithy4s generates http4s routes, you can use standard http4s middleware:
import org.http4s.server.middleware.*
val routesWithMiddleware = Logger.httpRoutes(true, true)(routes)
Error Handling
Errors defined in Smithy become typed exceptions that the framework handles automatically, returning proper HTTP status codes.
Resources
- Smithy4s Documentation
- Building a Fullstack App with Smithy4s
- Creating REST Services with Smithy4s
- Smithy4s CLI with Scala Native
Conclusion
Smithy4s brings contract-first API development to Scala with good type-safety and developer experience. By defining your API once in Smithy, you get server stubs, clients, and documentation that stay in sync automatically.
The combination of Smithy4s with http4s and Cats Effect gives you a solid way to build REST APIs in Scala.