Building REST APIs with Smithy4s

#scala#smithy4s#rest-api#http4s

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

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.