Skip to main content
Version: 2.x

Endpoint

The Endpoint API in ZIO HTTP, is an alternative way to describe the endpoints but in a declarative way. It is a high-level API that allows us to describe the endpoints and their inputs, outputs, and how they should look. So we can think of it as a DSL for just describing the endpoints, and then we can implement them separately.

Using Endpoint API enables us to generate OpenAPI documentation, and also to generate clients for the endpoints.

Overview

Before delving into the detailed description of the Endpoint API, let's begin with a simple example to demonstrate how we can define an endpoint using the Endpoint API:

import zio._
import zio.http._
import zio.http.codec._
import zio.http.endpoint.Endpoint
import zio.schema.DeriveSchema

case class Book(title: String, authors: List[String])
object Book {
implicit val schema = DeriveSchema.gen[Book]
}

val endpoint =
Endpoint(RoutePattern.GET / "books")
.query(HttpCodec.query[String]("q") examples (("example1", "scala"), ("example2", "zio")))
.out[List[Book]]

In the above example, we defined an endpoint on the path /books that accepts a query parameter q of type String and returns a list of Book.

After defining the endpoint, we are ready to implement it. We can implement it using the Endpoint#implement method, which takes a proper handler function that will be called when the endpoint is invoked and returns a Route:

val booksRoute = endpoint.implement(query => BookRepo.find(query))

We can also generate OpenAPI documentation for our endpoint using the OpenAPIGen.fromEndpoints constructor:

val openAPI       = OpenAPIGen.fromEndpoints(title = "Library API", version = "1.0", endpoint)
val swaggerRoutes = SwaggerUI.routes("docs" / "openapi", openAPI)

And finally we are ready to serve all the routes. Let's see the complete example:

zio-http-example/src/main/scala/example/endpoint/BooksEndpointExample.scala
package example.endpoint

import zio._

import zio.schema.annotation.description
import zio.schema.{DeriveSchema, Schema}

import zio.http._
import zio.http.codec.PathCodec._
import zio.http.codec._
import zio.http.endpoint._
import zio.http.endpoint.openapi._

object BooksEndpointExample extends ZIOAppDefault {
case class Book(
@description("Title of the book")
title: String,
@description("List of the authors of the book")
authors: List[String],
)
object Book {
implicit val schema: Schema[Book] = DeriveSchema.gen
}

object BookRepo {
val book1 = Book("Programming in Scala", List("Martin Odersky", "Lex Spoon", "Bill Venners", "Frank Sommers"))
val book2 = Book("Zionomicon", List("John A. De Goes", "Adam Fraser"))
val book3 = Book("Effect-Oriented Programming", List("Bill Frasure", "Bruce Eckel", "James Ward"))
def find(q: String): List[Book] = {
if (q.toLowerCase == "scala") List(book1, book2, book3)
else if (q.toLowerCase == "zio") List(book2, book3)
else List.empty
}
}

val endpoint =
Endpoint((RoutePattern.GET / "books") ?? Doc.p("Route for querying books"))
.query(
HttpCodec.query[String]("q").examples(("example1", "scala"), ("example2", "zio")) ?? Doc.p(
"Query parameter for searching books",
),
)
.out[List[Book]](Doc.p("List of books matching the query")) ?? Doc.p(
"Endpoint to query books based on a search query",
)

val booksRoute = endpoint.implementHandler(handler((query: String) => BookRepo.find(query)))
val openAPI = OpenAPIGen.fromEndpoints(title = "Library API", version = "1.0", endpoint)
val swaggerRoutes = SwaggerUI.routes("docs" / "openapi", openAPI)
val routes = Routes(booksRoute) ++ swaggerRoutes

def run = Server.serve(routes).provide(Server.default)
}

By running the above example, other than the main /books route, we can also access the OpenAPI documentation using the SwaggerUI at the /docs/openapi route.

This was an overview of the Endpoint API in ZIO HTTP. Next, we will dive deeper into the Endpoint API and see how we can describe the endpoints in more detail.

Describing Endpoints

Each endpoint is described by a set of properties, such as the path, query parameters, headers, and response type. The Endpoint API provides a set of methods to describe these properties. We can think of an endpoint as a function that takes some input and returns some output:

  • Input Properties— They can be the HTTP method, path parameters, query parameters, request headers, and the request body.
  • Output Properties­— They can be success or failure! Both success and failure can have a response body, media type, and status code.

Also, we can provide metadata for each property, such as documentation, examples, etc.

Describing Input Properties

Method and Path Parameters

We start describing an endpoint by specifying the HTTP method and the path. The default constructor of the Endpoint class takes a RoutePattern which is a combination of the HTTP method and the path:

import zio._
import zio.http._
import zio.http.endpoint._
import zio.http.endpoint.AuthType._
import zio.http.codec.PathCodec

val endpoint1: Endpoint[Unit, Unit, ZNothing, ZNothing, None] =
Endpoint(RoutePattern.GET / "users")

val endpoint2: Endpoint[String, String, ZNothing, ZNothing, None] =
Endpoint(RoutePattern.GET / "users" / PathCodec.string("user_name"))

val endpoint3: Endpoint[(String, Int), (String, Int), ZNothing, ZNothing, None] =
Endpoint(RoutePattern.GET / "users" / PathCodec.string("user_name") / "posts" / PathCodec.int("post_id"))

In the above examples, we defined three endpoints. The first one is a simple endpoint that matches the GET method on the /users path. The second one matches the GET method on the /users/:user_name path, where :user_name is a path parameter of type String. The third one matches the GET method on the /users/:user_name/posts/:post_id path, where :user_name and :post_id are path parameters of type String and Int, respectively.

The Endpoint is a type-safe way to describe the endpoints. For example, if we try to implement the endpoint3 with a handler that takes a different input type other than (String, Int), the compiler will give us an error.

Query Parameters

Query parameters can be described using the Endpoint#query method which takes a QueryCodec[A]:

val endpoint: Endpoint[Unit, String, ZNothing, ZNothing, AuthType.None] =
Endpoint(RoutePattern.GET / "books")
.query(HttpCodec.query[String]("q"))

QueryCodecs are composable, so we can combine multiple query parameters:

val endpoint: Endpoint[Unit, (String, Int), ZNothing, ZNothing, AuthType.None] =
Endpoint(RoutePattern.GET / "books")
.query(HttpCodec.query[String]("q") ++ HttpCodec.query[Int]("limit"))

Or we can use the query method multiple times:

val endpoint: Endpoint[Unit, (String, Int), ZNothing, ZNothing, AuthType.None] =
Endpoint(RoutePattern.GET / "books")
.query(HttpCodec.query[String]("q"))
.query(HttpCodec.query[Int]("limit"))

Please note that as we add more properties to the endpoint, the input and output types of the endpoint change accordingly. For example, in the following example, we have an endpoint with a path parameter of type String and two query parameters of type String and Int. So the input type of the endpoint is (String, String, Int):

val endpoint: Endpoint[String, (String, String, Int), ZNothing, ZNothing, AuthType.None] =
Endpoint(RoutePattern.GET / "books" / PathCodec.string("genre"))
.query(HttpCodec.query[String]("q"))
.query(HttpCodec.query[Int]("limit"))

When we implement the endpoint, the handler function should take the input type of a tuple that the first element is the "genre" path parameter, and the second and third elements are the query parameters "q" and "limit" respectively.

Headers

Headers can be described using the Endpoint#header method which takes a HeaderCodec[A] and specifies that the given header is required, for example:

val endpoint: Endpoint[String, (String, Header.Authorization), ZNothing, ZNothing, AuthType.None] =
Endpoint(RoutePattern.GET / "books" / PathCodec.string("genre"))
.header(HeaderCodec.authorization)

Request Body

The request body can be described using the Endpoint#in method:

import zio.schema._

case class Book(title: String, author: String)

object Book {
implicit val schema: Schema[Book] = DeriveSchema.gen[Book]
}

val endpoint: Endpoint[Unit, Book, ZNothing, ZNothing, AuthType.None] =
Endpoint(RoutePattern.POST / "books" )
.in[Book]

The above example describes an endpoint that accepts a Book object as the request body.

By default, the request body is not named and its media type is determined by the Content-Type header. But for multipart form data, we can have multiple request bodies, called parts:

val endpoint =
Endpoint(RoutePattern.POST / "submit-form")
.header(HeaderCodec.contentType.expect(Header.ContentType(MediaType.multipart.`form-data`)))
.in[String]("title")
.in[String]("author")

In the above example, we have defined an endpoint that describes a multipart form data request body with two parts: title and author. Let's see what the request body might look like:

POST /submit-form HTTP/1.1
Content-Type: multipart/form-data; boundary=boundary1234567890

--boundary1234567890
Content-Disposition: form-data; name="title"

The Title of the Book
--boundary1234567890
Content-Disposition: form-data; name="author"

John Doe
--boundary1234567890--

The Endpoint#in method has multiple overloads that can be used to describe other properties of the request body, such as the media type and documentation.

Describing Output Properties of Success Responses

The Endpoint#out method is used to describe the output properties of the success response:

import zio.http._
import zio.schema._

case class Book(title: String, author: String)

object Book {
implicit val schema: Schema[Book] = DeriveSchema.gen[Book]
}

val endpoint: Endpoint[Unit, String, ZNothing, List[Book], AuthType.None] =
Endpoint(RoutePattern.GET / "books")
.query(HttpCodec.query[String]("q"))
.out[List[Book]]

In the above example, we defined an endpoint that describes a query parameter q as input and returns a list of Book as output. The Endpoint#out method has multiple overloads that can be used to describe other properties of the output, such as the status code, media type, and documentation.

We can also add custom headers to the output using the Endpoint#outHeader method:

import zio.http._
import zio.schema._

case class Book(title: String, author: String)

object Book {
implicit val schema: Schema[Book] = DeriveSchema.gen[Book]
}

val endpoint: Endpoint[Unit, String, ZNothing, (List[Book], Header.Date), AuthType.None] =
Endpoint(RoutePattern.GET / "books")
.query(HttpCodec.query[String]("q"))
.out[List[Book]]
.outHeader(HttpCodec.date)

Sometimes based on the condition, we might want to return different types of responses. We can use the Endpoint#out method multiple times to describe different output types:

import zio._
import zio.http.{RoutePattern, _}
import zio.http.endpoint.Endpoint
import zio.schema.DeriveSchema.gen
import zio.schema._

case class Book(title: String, author: String)

object Book {
implicit val schema: Schema[Book] = DeriveSchema.gen
}

case class Article(title: String, author: String)

object Article {
implicit val schema: Schema[Article] = DeriveSchema.gen
}

case class Course(title: String, price: Double)
object Course {
implicit val schema = DeriveSchema.gen[Course]
}

case class Quiz(question: String, level: Int)
object Quiz {
implicit val schema = DeriveSchema.gen[Quiz]
}

object EndpointWithMultipleOutputTypes extends ZIOAppDefault {
val endpoint: Endpoint[Unit, Unit, ZNothing, Either[Quiz, Course], AuthType.None] =
Endpoint(RoutePattern.GET / "resources")
.out[Course]
.out[Quiz]

def run = Server.serve(
endpoint.implement(_ =>
ZIO.randomWith(_.nextBoolean)
.map(r =>
if (r) Right(Course("Introduction to Programming", 49.99))
else Left(Quiz("What is the boiling point of water in Celsius?", 2)),
)
)
.toRoutes).provide(Server.default)
}

In the above example, we defined an endpoint that describes a path parameter id as input and returns either a Book or an Article as output.

With multiple outputs, we can define if all of them or just some should add an output header, by the order of calling out and outHeader methods:

import zio._
import zio.http._
import zio.http.endpoint._
import zio.schema._

case class Book(title: String, author: String)

object Book {
implicit val schema: Schema[Book] = DeriveSchema.gen
}

case class Article(title: String, author: String)

object Article {
implicit val schema: Schema[Article] = DeriveSchema.gen
}
// header will be added to the first output
val endpoint: Endpoint[Unit, Unit, ZNothing, Either[Article, (Book, Header.Date)], AuthType.None] =
Endpoint(RoutePattern.GET / "resources")
.out[Book]
.outHeader(HttpCodec.date)
.out[Article]

// header will be added to all outputs
val endpoint2: Endpoint[Unit, Unit, ZNothing, (Either[Article, Book], Header.Date), AuthType.None] =
Endpoint(RoutePattern.GET / "resources")
.out[Book]
.out[Article]
.outHeader(HttpCodec.date)

A call to outHeder will require to provide the header together with all outputs defined before it.

Sometimes we might want more control over the output properties, in such cases, we can provide a custom HttpCodec that describes the output properties using the Endpoint#outCodec method. This can be very useful when we only want to add headers to a subset of outputs for example:

import zio._
import zio.http._
import zio.http.endpoint._
import zio.schema._

case class Book(title: String, author: String)

object Book {
implicit val schema: Schema[Book] = DeriveSchema.gen
}

case class Article(title: String, author: String)

object Article {
implicit val schema: Schema[Article] = DeriveSchema.gen
}
val endpoint: Endpoint[Unit, Unit, ZNothing, Either[(Article, Header.Date), Book], AuthType.None] =
Endpoint(RoutePattern.GET / "resources")
.out[Book]
.outCodec(HttpCodec.content[Article] ++ HttpCodec.date)

Or when we want to reuse the same codec for multiple endpoints:

import zio._
import zio.http._
import zio.http.endpoint._
import zio.schema._

case class Book(title: String, author: String)

object Book {
implicit val schema: Schema[Book] = DeriveSchema.gen
}

case class Article(title: String, author: String)

object Article {
implicit val schema: Schema[Article] = DeriveSchema.gen
}
val bookCodec = HttpCodec.content[Book] ++ HttpCodec.date
val articleCodec = HttpCodec.content[Article] ++ HttpCodec.date

val endpoint1: Endpoint[Unit, Unit, ZNothing, (Book, Header.Date), AuthType.None] =
Endpoint(RoutePattern.GET / "books")
.outCodec(bookCodec)

val endpoint2: Endpoint[Unit, Unit, ZNothing, (Article, Header.Date), AuthType.None] =
Endpoint(RoutePattern.GET / "articles")
.outCodec(articleCodec)

val endpoint3: Endpoint[Unit, Unit, ZNothing, Either[(Article, Header.Date), (Book, Header.Date)], AuthType.None] =
Endpoint(RoutePattern.GET / "resources")
.outCodec(articleCodec | bookCodec)

Describing Failures

For failure outputs, we can describe the output properties using the Endpoint#outError* methods. Let's see an example:

zio-http-example/src/main/scala/example/endpoint/style/DeclarativeProgrammingExample.scala
package example.endpoint.style

import zio._

import zio.schema.{DeriveSchema, Schema}

import zio.http._
import zio.http.codec._
import zio.http.endpoint.{AuthType, Endpoint}

object DeclarativeProgrammingExample extends ZIOAppDefault {

case class Book(title: String, authors: List[String])

object Book {
implicit val schema: Schema[Book] = DeriveSchema.gen
}
case class NotFoundError(message: String)

object NotFoundError {
implicit val schema: Schema[NotFoundError] = DeriveSchema.gen
}

object BookRepo {
def find(id: String): ZIO[Any, NotFoundError, Book] = {
if (id == "1")
ZIO.succeed(Book("Zionomicon", List("John A. De Goes", "Adam Fraser")))
else
ZIO.fail(NotFoundError("The requested book was not found!"))
}
}

val endpoint: Endpoint[Unit, String, NotFoundError, Book, AuthType.None] =
Endpoint(RoutePattern.GET / "books")
.query(HttpCodec.query[String]("id"))
.out[Book]
.outError[NotFoundError](Status.NotFound)

val getBookHandler: Handler[Any, NotFoundError, String, Book] =
handler(BookRepo.find(_))

val routes = endpoint.implementHandler(getBookHandler).toRoutes @@ Middleware.debug

def run = Server.serve(routes).provide(Server.default)
}

In the above example, we defined an endpoint that describes a query parameter id as input and returns a Book as output. If the book is not found, the endpoint returns a NotFound status code with a custom error message.

Multiple Failure Outputs Using Endpoint#outError

If we have multiple failure outputs, we can use the Endpoint#outError method multiple times to describe different error types. By specifying more error types, the type of the endpoint will be the union of all the error types (e.g., Either[Error1, Error2]):

import zio._
import zio.http._
import zio.schema._
import zio.schema.DeriveSchema

case class Book(title: String, authors: List[String])
case class BookNotFound(message: String, bookId: Int)
case class AuthenticationError(message: String, userId: Int)

implicit val bookSchema = DeriveSchema.gen[Book]
implicit val notFoundSchema = DeriveSchema.gen[BookNotFound]
implicit val authSchema = DeriveSchema.gen[AuthenticationError]

val endpoint: Endpoint[Int, (Int, Header.Authorization), Either[AuthenticationError, BookNotFound], Book, AuthType.None] =
Endpoint(RoutePattern.GET / "books" / PathCodec.int("id"))
.header(HeaderCodec.authorization)
.out[Book]
.outError[BookNotFound](Status.NotFound)
.outError[AuthenticationError](Status.Unauthorized)
Full Implementation Showcase
zio-http-example/src/main/scala/example/endpoint/EndpointWithMultipleErrorsUsingEither.scala
package example.endpoint

import zio._

import zio.schema.{DeriveSchema, Schema}

import zio.http._
import zio.http.codec._
import zio.http.endpoint.{AuthType, Endpoint}

object EndpointWithMultipleErrorsUsingEither extends ZIOAppDefault {

case class Book(title: String, authors: List[String])

object Book {
implicit val schema: Schema[Book] = DeriveSchema.gen
}

case class BookNotFound(message: String, bookId: Int)

object BookNotFound {
implicit val schema: Schema[BookNotFound] = DeriveSchema.gen
}

case class AuthenticationError(message: String, userId: Int)

object AuthenticationError {
implicit val schema: Schema[AuthenticationError] = DeriveSchema.gen
}

object BookRepo {
def find(id: Int): ZIO[Any, BookNotFound, Book] = {
if (id == 1)
ZIO.succeed(Book("Zionomicon", List("John A. De Goes", "Adam Fraser")))
else
ZIO.fail(BookNotFound("The requested book was not found.", id))
}
}

val endpoint
: Endpoint[Int, (Int, Header.Authorization), Either[AuthenticationError, BookNotFound], Book, AuthType.None] =
Endpoint(RoutePattern.GET / "books" / PathCodec.int("id"))
.header(HeaderCodec.authorization)
.out[Book]
.outError[BookNotFound](Status.NotFound)
.outError[AuthenticationError](Status.Unauthorized)

def isUserAuthorized(authHeader: Header.Authorization) = false

val getBookHandler
: Handler[Any, Either[AuthenticationError, BookNotFound], (RuntimeFlags, Header.Authorization), Book] =
handler { (id: Int, authHeader: Header.Authorization) =>
if (isUserAuthorized(authHeader))
BookRepo.find(id).mapError(Right(_))
else
ZIO.fail(Left(AuthenticationError("User is not authenticated", 123)))
}

val routes = endpoint.implementHandler(getBookHandler).toRoutes @@ Middleware.debug

def run = Server.serve(routes).provide(Server.default)
}

Multiple Failure Outputs Endpoint#outErrors

Alternatively, the idiomatic way to describe multiple failure outputs is to unify all the error types into a single error type using a sealed trait or an enum, and then describe the output properties using the Endpoint#outErrors method:

import zio.schema.DeriveSchema

case class Book(title: String, authors: List[String])
implicit val bookSchema = DeriveSchema.gen[Book]

abstract class AppError(message: String)
case class BookNotFound(message: String, bookId: Int) extends AppError(message)
case class AuthenticationError(message: String, userId: Int) extends AppError(message)

implicit val notFoundSchema = DeriveSchema.gen[BookNotFound]
implicit val authSchema = DeriveSchema.gen[AuthenticationError]

val endpoint: Endpoint[Int, (Int, Header.Authorization), AppError, Book, AuthType.None] =
Endpoint(RoutePattern.GET / "books" / PathCodec.int("id"))
.header(HeaderCodec.authorization)
.out[Book]
.outErrors[AppError](
HttpCodec.error[BookNotFound](Status.NotFound),
HttpCodec.error[AuthenticationError](Status.Unauthorized),
)

The Endpoint#outErrors method takes a list of HttpCodec that describes the error types and their corresponding status codes.

Full Implementation Showcase
zio-http-example/src/main/scala/example/endpoint/EndpointWithMultipleUnifiedErrors.scala
package example.endpoint

import scala.annotation.nowarn

import zio._

import zio.schema.{DeriveSchema, Schema}

import zio.http._
import zio.http.codec._
import zio.http.endpoint.{AuthType, Endpoint}

object EndpointWithMultipleUnifiedErrors extends ZIOAppDefault {

case class Book(title: String, authors: List[String])

object Book {
implicit val schema: Schema[Book] = DeriveSchema.gen
}

@nowarn("msg=parameter .* never used")
abstract class AppError(message: String)

case class BookNotFound(message: String, bookId: Int) extends AppError(message)

object BookNotFound {
implicit val schema: Schema[BookNotFound] = DeriveSchema.gen
}

case class AuthenticationError(message: String, userId: Int) extends AppError(message)

object AuthenticationError {
implicit val schema: Schema[AuthenticationError] = DeriveSchema.gen
}

object BookRepo {
def find(id: Int): ZIO[Any, BookNotFound, Book] = {
if (id == 1)
ZIO.succeed(Book("Zionomicon", List("John A. De Goes", "Adam Fraser")))
else
ZIO.fail(BookNotFound("The requested book was not found.", id))
}
}

val endpoint: Endpoint[Int, (Int, Header.Authorization), AppError, Book, AuthType.None] =
Endpoint(RoutePattern.GET / "books" / PathCodec.int("id"))
.header(HeaderCodec.authorization)
.out[Book]
.outErrors[AppError](
HttpCodec.error[BookNotFound](Status.NotFound),
HttpCodec.error[AuthenticationError](Status.Unauthorized),
)

def isUserAuthorized(authHeader: Header.Authorization) = false

val getBookHandler: Handler[Any, AppError, (Int, Header.Authorization), Book] =
handler { (id: Int, authHeader: Header.Authorization) =>
if (isUserAuthorized(authHeader))
BookRepo.find(id)
else
ZIO.fail(AuthenticationError("User is not authenticated", 123))
}

val routes = endpoint.implementHandler(getBookHandler).toRoutes @@ Middleware.debug

def run = Server.serve(routes).provide(Server.default)
}

Transforming Endpoint Input/Output and Error Types

To transform the input, output, and error types of an endpoint, we can use the Endpoint#transformIn, Endpoint#transformOut, and Endpoint#transformError methods, respectively. Let's see an example:

case class BookQuery(query: String, genre: String, title: String)

val endpoint: Endpoint[String, (String, String, String), ZNothing, ZNothing, AuthType.None] =
Endpoint(RoutePattern.POST / "books" / PathCodec.string("genre"))
.query(HttpCodec.query[String]("q"))
.query(HttpCodec.query[String]("title"))

val mappedEndpoint: Endpoint[String, BookQuery, ZNothing, ZNothing, AuthType.None] =
endpoint.transformIn[BookQuery] { case (genre, q, title) => BookQuery(q, genre, title) } { i =>
(i.genre, i.query, i.title)
}

In the above example, we mapped over the input type of the endpoint and transformed it into a single BookQuery object. The Endpoint#transformIn method takes two functions, the first one is used to map the input type to the new input type, and the second one is responsible for mapping the new input type back to the original input type.

The transformOut and transformError methods work similarly to the transformIn method.

OpenAPI Documentation

Every property of an Endpoint API can be annotated with documentation, may be examples using methods like ?? and example*. We can use these metadata to generate OpenAPI documentation:

val endpoint =
Endpoint((RoutePattern.GET / "books") ?? Doc.p("Route for querying books"))
.query(
HttpCodec.query[String]("q").examples(("example1", "scala"), ("example2", "zio")) ?? Doc.p(
"Query parameter for searching books",
),
)
.out[List[Book]](Doc.p("List of books matching the query")) ?? Doc.p(
"Endpoint to query books based on a search query",
)

Also, we can use the @description annotation from the zio.schema.annotation package to annotate data models, which will enrich the OpenAPI documentation:

import zio.schema.annotation.description

case class Book(
@description("Title of the book")
title: String,
@description("List of the authors of the book")
authors: List[String],
)

The OpenAPIGen.fromEndpoints constructor generates OpenAPI documentation from the endpoints. By having the OpenAPI documentation, we can easily generate Swagger UI routes using the SwaggerUI.routes constructor:

val booksRoute = endpoint.implement(query => BookRepo.find(query))
val openAPI = OpenAPIGen.fromEndpoints(title = "Library API", version = "1.0", endpoint)
val swaggerRoutes = SwaggerUI.routes("docs" / "openapi", openAPI)
val routes = Routes(booksRoute) ++ swaggerRoutes
Full Implementation Showcase
zio-http-example/src/main/scala/example/endpoint/BooksEndpointExample.scala
package example.endpoint

import zio._

import zio.schema.annotation.description
import zio.schema.{DeriveSchema, Schema}

import zio.http._
import zio.http.codec.PathCodec._
import zio.http.codec._
import zio.http.endpoint._
import zio.http.endpoint.openapi._

object BooksEndpointExample extends ZIOAppDefault {
case class Book(
@description("Title of the book")
title: String,
@description("List of the authors of the book")
authors: List[String],
)
object Book {
implicit val schema: Schema[Book] = DeriveSchema.gen
}

object BookRepo {
val book1 = Book("Programming in Scala", List("Martin Odersky", "Lex Spoon", "Bill Venners", "Frank Sommers"))
val book2 = Book("Zionomicon", List("John A. De Goes", "Adam Fraser"))
val book3 = Book("Effect-Oriented Programming", List("Bill Frasure", "Bruce Eckel", "James Ward"))
def find(q: String): List[Book] = {
if (q.toLowerCase == "scala") List(book1, book2, book3)
else if (q.toLowerCase == "zio") List(book2, book3)
else List.empty
}
}

val endpoint =
Endpoint((RoutePattern.GET / "books") ?? Doc.p("Route for querying books"))
.query(
HttpCodec.query[String]("q").examples(("example1", "scala"), ("example2", "zio")) ?? Doc.p(
"Query parameter for searching books",
),
)
.out[List[Book]](Doc.p("List of books matching the query")) ?? Doc.p(
"Endpoint to query books based on a search query",
)

val booksRoute = endpoint.implementHandler(handler((query: String) => BookRepo.find(query)))
val openAPI = OpenAPIGen.fromEndpoints(title = "Library API", version = "1.0", endpoint)
val swaggerRoutes = SwaggerUI.routes("docs" / "openapi", openAPI)
val routes = Routes(booksRoute) ++ swaggerRoutes

def run = Server.serve(routes).provide(Server.default)
}

Generating Endpoint from OpenAPI Spec

With ZIO HTTP, we can generate endpoints from an OpenAPI specification. To do this, first, we need to add the following line to the build.sbt file:

libraryDependencies += "dev.zio" %% "zio-http-gen" % "3.0.0"

Then we can generate the endpoints from the OpenAPI specification using the EndpointGen.fromOpenAPI constructor:

zio-http-example/src/main/scala/example/endpoint/GenerateEndpointFromOpenAPIExample.scala
package example.endpoint

import java.nio.file._

import zio.http.endpoint.openapi.OpenAPI
import zio.http.gen.openapi.EndpointGen
import zio.http.gen.scala.CodeGen

object GenerateEndpointFromOpenAPIExample extends App {
val userOpenAPI = OpenAPI.fromJson(
"""|{
| "openapi": "3.0.0",
| "info": {
| "title": "User API",
| "version": "1.0.0"
| },
| "paths": {
| "/users/{userId}": {
| "get": {
| "parameters": [
| {
| "name": "userId",
| "in": "path",
| "required": true,
| "schema": {
| "type": "integer"
| }
| }
| ],
| "responses": {
| "200": {
| "content": {
| "application/json": {
| "schema": {
| "type": "object",
| "properties": {
| "userId": {
| "type": "integer"
| },
| "username": {
| "type": "string"
| }
| }
| }
| }
| }
| },
| "404": {
| "description": "User not found"
| }
| }
| }
| }
| }
|}
|""".stripMargin,
)

CodeGen.writeFiles(
EndpointGen.fromOpenAPI(userOpenAPI.toOption.get),
basePath = Paths.get("./users/src/main/scala"),
basePackage = "org.example",
scalafmtPath = None,
)
}

Generating ZIO CLI App from Endpoint API

The ZIO CLI is a ZIO library that provides a way to build command-line applications using ZIO facilities. With ZIO HTTP, we can generate a ZIO CLI client from the Endpoint API.

To do this, first, we need to add the following line to the build.sbt file:

libraryDependencies += "dev.zio" %% "zio-http-cli" % "3.0.0"

Then we can generate the ZIO CLI client from the Endpoint API using the HttpCliApp.fromEndpoints constructor:

object TestCliApp extends zio.cli.ZIOCliDefault with TestCliEndpoints {
val cliApp =
HttpCliApp
.fromEndpoints(
name = "users-mgmt",
version = "0.0.1",
summary = HelpDoc.Span.text("Users management CLI"),
footer = HelpDoc.p("Copyright 2023"),
host = "localhost",
port = 8080,
endpoints = Chunk(getUser, getUserPosts, createUser),
cliStyle = true,
)
.cliApp
}

Using the above code, we can create the users-mgmt CLI application that can be used to interact with the getUser, getUserPosts, and createUser endpoints:

                                                             __ 
__ __________ __________ ____ ___ ____ _____ ___ / /_
/ / / / ___/ _ \/ ___/ ___/_____/ __ `__ \/ __ `/ __ `__ \/ __/
/ /_/ (__ ) __/ / (__ )_____/ / / / / / /_/ / / / / / / /_
\__,_/____/\___/_/ /____/ /_/ /_/ /_/\__, /_/ /_/ /_/\__/
/____/

users-mgmt v0.0.1 -- Users management CLI

USAGE

$ users-mgmt <command>

COMMANDS

- get-users --userId integer --location text Get a user by ID

- get-users-posts --postId integer --userId integer --user-name text Get a user's posts by userId and postId

- create-users -f file|-u text|--.id integer --.name text [--.email text] Create a new user

Copyright 2023
Full Implementation Showcase
zio-http-example/src/main/scala/example/endpoint/CliExamples.scala
package example.endpoint

import zio._
import zio.cli._

import zio.schema._
import zio.schema.annotation.description

import zio.http.Header.Location
import zio.http._
import zio.http.codec._
import zio.http.endpoint.cli._
import zio.http.endpoint.{Endpoint, EndpointExecutor}

final case class User(
@description("The unique identifier of the User")
id: Int,
@description("The user's name")
name: String,
@description("The user's email")
email: Option[String],
)
object User {
implicit val schema: Schema[User] = DeriveSchema.gen[User]
}
final case class Post(
@description("The unique identifier of the User")
userId: Int,
@description("The unique identifier of the Post")
postId: Int,
@description("The post's contents")
contents: String,
)
object Post {
implicit val schema: Schema[Post] = DeriveSchema.gen[Post]
}

trait TestCliEndpoints {

val getUser =
Endpoint(Method.GET / "users" / int("userId") ?? Doc.p("The unique identifier of the user"))
.header(HeaderCodec.location ?? Doc.p("The user's location"))
.out[User] ?? Doc.p("Get a user by ID")

val getUserPosts =
Endpoint(
Method.GET /
"users" / int("userId") ?? Doc.p("The unique identifier of the user") /
"posts" / int("postId") ?? Doc.p("The unique identifier of the post"),
)
.query(
HttpCodec.query[String]("user-name") ?? Doc.p(
"The user's name",
),
)
.out[List[Post]] ?? Doc.p("Get a user's posts by userId and postId")

val createUser =
Endpoint(Method.POST / "users")
.in[User]
.out[String] ?? Doc.p("Create a new user")
}

object TestCliApp extends zio.cli.ZIOCliDefault with TestCliEndpoints {
val cliApp =
HttpCliApp
.fromEndpoints(
name = "users-mgmt",
version = "0.0.1",
summary = HelpDoc.Span.text("Users management CLI"),
footer = HelpDoc.p("Copyright 2023"),
host = "localhost",
port = 8080,
endpoints = Chunk(getUser, getUserPosts, createUser),
cliStyle = true,
)
.cliApp
}

object TestCliServer extends zio.ZIOAppDefault with TestCliEndpoints {
val getUserRoute =
getUser.implementHandler {
Handler.fromFunctionZIO { case (id, _) =>
ZIO.succeed(User(id, "Juanito", Some("juanito@test.com"))).debug("Hello")
}
}

val getUserPostsRoute =
getUserPosts.implementHandler {
Handler.fromFunction { case (userId, postId, name) =>
List(Post(userId, postId, name))
}
}

val createUserRoute =
createUser.implementHandler {
Handler.fromFunction { user =>
user.name
}
}

val routes = Routes(getUserRoute, getUserPostsRoute, createUserRoute) @@ Middleware.debug

val run = Server.serve(routes).provide(Server.default)
}

object TestCliClient extends zio.ZIOAppDefault with TestCliEndpoints {
val run =
clientExample
.provide(
EndpointExecutor.make(serviceName = "test"),
Client.default,
)

def clientExample: URIO[EndpointExecutor[Any, Unit], Unit] =
for {
executor <- ZIO.service[EndpointExecutor[Any, Unit]]
_ <- ZIO.scoped(executor(getUser(42, Location.parse("some-location").toOption.get))).debug("result1")
_ <- ZIO.scoped(executor(getUserPosts(42, 200, "adam")).debug("result2"))
_ <- ZIO.scoped(executor(createUser(User(2, "john", Some("john@test.com"))))).debug("result3")
} yield ()

}