Construyendo APIs REST con Smithy4s
Smithy4s es una herramienta de generación de código que crea código Scala de servidor y cliente desde especificaciones Smithy. Funciona con http4s y te da seguridad de tipos en tu API.
¿Por qué Smithy4s?
Cuando construyes APIs de forma tradicional, a menudo necesitas:
- Escribir mucho código repetitivo para serialización
- Mantener servidor, cliente y documentación sincronizados manualmente
- Lidiar con errores en tiempo de ejecución cuando la API cambia
Smithy4s soluciona esto con:
- Generación de código: Crea código de servidor y cliente desde una definición
- Seguridad de tipos: El compilador detecta errores antes de ejecutar
- Soporte de protocolos: Funciona con REST JSON y otros formatos
- Buena integración: Funciona bien con http4s y Cats Effect
Comenzando
Configuración del Proyecto
Agrega el plugin Smithy4s a tu project/plugins.sbt:
addSbtPlugin("com.disneystreaming.smithy4s" % "smithy4s-sbt-codegen" % "0.18.15")
Habilita el plugin en 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 tu API
Crea una especificación Smithy en 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
}
Implementa el Servicio
La generación de código crea un trait BookstoreService[F[_]] que implementas:
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)
}
}
Crea el Servidor
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("Servidor iniciado en puerto 9000") *> IO.never)
}
}
Usa el Cliente Generado
Smithy4s también genera un cliente con seguridad de tipos:
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 {
// Listar todos los libros
allBooks <- client.listBooks()
_ <- IO.println(s"Libros: ${allBooks.books}")
// Obtener un libro específico
book <- client.getBook("978-0134685991")
_ <- IO.println(s"Libro: ${book.book}")
// Crear un nuevo libro
newBook = Book(
isbn = "978-1617295027",
title = "Functional Programming in Scala",
author = "Paul Chiusano",
year = Some(2014)
)
created <- client.createBook(newBook)
_ <- IO.println(s"Creado: ${created.book}")
} yield ()
}
}
}
Beneficios Principales
Única Fuente de Verdad: Tu especificación Smithy define tanto el servidor como el cliente.
Seguridad de Tipos: El compilador detecta violaciones de contrato en tiempo de compilación.
Documentación: Puedes generar especificaciones OpenAPI desde tus definiciones Smithy.
Flexibilidad de Protocolo: Soporta REST JSON, protocolos AWS, o formatos personalizados.
Compatible con Scala 3: Soporte completo para Scala 3 con buena inferencia de tipos.
Características Avanzadas
Protocolos Personalizados
Smithy4s soporta protocolos personalizados más allá de REST JSON. Puedes definir tus propios formatos de serialización.
Integración de Middleware
Como Smithy4s genera rutas http4s, puedes usar middleware estándar de http4s:
import org.http4s.server.middleware.*
val routesWithMiddleware = Logger.httpRoutes(true, true)(routes)
Manejo de Errores
Los errores definidos en Smithy se convierten en excepciones tipadas que el framework maneja automáticamente, devolviendo códigos de estado HTTP apropiados.
Recursos
- Documentación de Smithy4s
- Construyendo una App Fullstack con Smithy4s
- Creando Servicios REST con Smithy4s
- Smithy4s CLI con Scala Native
Conclusión
Smithy4s trae desarrollo de APIs contract-first a Scala con buena seguridad de tipos y experiencia del desarrollador. Al definir tu API una vez en Smithy, obtienes stubs de servidor, clientes y documentación que se mantienen sincronizados automáticamente.
La combinación de Smithy4s con http4s y Cats Effect te da una forma sólida de construir APIs REST en Scala.