Endpoints
This algebra, at the top of the hierarchy, provides the base vocabulary to describe HTTP endpoints, requests and responses.
"org.julienrf" %% "endpoints-algebra" % "0.15.0"
Endpoint
The algebra introduces the concept of Endpoint[A, B]
: an HTTP endpoint whose request carries an information of type A
and whose response carries an information of type B
. For instance, an endpoint of type Endpoint[Long, Option[User]]
has a request containing a Long
(for instance, a user id), and a response optionally containing a User
.
You can define an endpoint by using the endpoint
constructor:
// An endpoint whose requests use the HTTP verb “GET” and the URL
// path “/some-resource”, and whose responses have an entity of
// type “text/plain”
val someResource: Endpoint[Unit, String] =
endpoint(get(path / "some-resource"), ok(textResponse))
The endpoint
constructor takes two parameters, the request description (of type Request[A]
) and the response description (of type Response[B]
), which are documented in the following sections.
It also takes optional parameters carrying documentation information:
endpoint(
get(path / "some-resource"),
ok(textResponse),
docs = EndpointDocs(description = Some("The contents of some resource"))
)
Request
The Request[A]
type models an HTTP request carrying some information of type A
. For instance, a Request[Long]
value is a request containing a Long
value.
A request is defined in terms of an HTTP verb, an URL, an entity and headers:
// A request that uses the verb “GET”, the URL path “/foo”,
// no entity, no documentation, and no headers
request(Get, path / "foo", emptyRequest, None, emptyRequestHeaders)
For convenience, get
, post
, put
and delete
methods are provided:
get(path / "foo") // Same as above
The next sections document how to describe URLs, request headers and request entities.
URL
The Url[A]
type models an URL carrying some information of type A
. For instance, an Url[Long]
value is an URL containing a Long
value.
An URL is defined by a path and a query string. Here are some self-explanatory examples of URLs:
path // the root path “/”
path / "users" // “/users”
path / "users" / segment[Long]() // “/users/1234”, “/users/5678”, …
path / "assets" / remainingSegments() // “/assets/images/logo.png”
path / "articles" /? qs[Int](
"page"
) // “/articles?page=2”, “/articles?page=5”, …
// Optional parameter
path / "articles" /? qs[Option[Int]](
"page"
) // “/articles”, “/articles?page=2”, …
// Repeated parameter
path / "articles" /? qs[List[String]](
"kinds"
) // “/articles?kinds=garden&kinds=woodworking”, …
// Several parameters
path /? (qs[String]("q") & qs[String]("lang")) // “/?q=foo&lang=en”, …
The examples above show that basic types (e.g., Int
, String
, etc.) are supported out of the box as query and path parameters. A user-defined type T
can be supported either by - defining implicit instances of Segment[T]
(for path parameters) or QueryStringParam[T]
(for query string parameters), - transforming or refining already supported types by using xmap
or xmapPartial
(see next section).
Path segments and query string parameters can take additional parameters containing documentation:
// “/users/{id}”
path / "users" / segment[Long]("id", docs = Some("A user id"))
// “/?q=foo&lang=en”, …
val query = qs[String]("q", docs = Some("Query"))
val lang = qs[String]("lang", docs = Some("Language"))
path /? (query & lang)
Transforming and Refining URL Constituents
All the data types involved in a URL description (Path[A]
, Segment[A]
, QueryString[A]
, etc.) have an xmap
and an xmapPartial
operations, for transforming or refining their carried type.
For instance, consider the following user-defined Location
type, containing a longitude
and a latitude
:
case class Location(longitude: Double, latitude: Double)
The QueryString[Location]
type means “a query string that carries a Location
”. We can define a value of type QueryString[Location]
by transforming a query string that carries the longitude and latitude parameters as follows:
val locationQueryString: QueryString[Location] =
(qs[Double]("lon") & qs[Double]("lat")).xmap {
case (lon, lat) => Location(lon, lat)
} { location => (location.longitude, location.latitude) }
The xmap
operation requires the source type and the target type to be equivalent (in the above case, the source type is (Double, Double)
and the target type is Location
).
In case the target type is smaller than the source type, you can use the xmapPartial
operation, which refines the carried type. As an example, here is how you can define a Segment[LocalDate]
:
import java.time.LocalDate
import endpoints.{Invalid, Valid}
implicit def localDateSegment(
implicit string: Segment[String]
): Segment[LocalDate] =
string.xmapPartial { s =>
Try(LocalDate.parse(s)) match {
case Failure(_) => Invalid(s"Invalid date value '$s'")
case Success(date) => Valid(date)
}
}(_.toString)
The first function passed to the xmapPartial
operation returns a Validated[LocalDate]
value. Returning an Invalid
value means that there is no representation of the source type in the target type.
Request Headers
The type RequestHeaders[A]
models request headers carrying some information of type A
. For instance, a value of type RequestHeaders[Credentials]
describes request headers containing credentials.
Please refer to the API documentation for details about constructors and operations for the type RequestHeaders
.
Request Entity
The type RequestEntity[A]
models a request entity carrying some information of type A
. For instance, a value of type RequestEntity[Command]
describes a request entity containing a command.
The Endpoints
algebra provides a few RequestEntity
constructors and operations, which can be extended to support more content types. For instance, the JsonEntities algebra adds support for requests with JSON entities.
Response
The Response[A]
type models an HTTP response carrying some information of type A
. For instance, a Response[User]
value describes an HTTP response containing a user: client interpreters decode a User
from the response entity, server interpreters encode a User
as a response entity, and documentation interpreters render the serialization schema of a User
.
Constructing Responses
A response is defined in terms of a status, headers and an entity. Here is an example of a simple OK response with no entity and no headers:
// An HTTP response with status code 200 (Ok) and no entity
val nothing: Response[Unit] = ok(emptyResponse)
There is a more general response constructor taking the status as parameter:
// An HTTP response with status code 200 (Ok) and a text entity
val aTextResponse: Response[String] = response(OK, textResponse)
Additional documentation about the response can be passed as an extra parameter:
ok(
emptyResponse,
docs = Some("A response with an OK status code and no entity")
)
Response Headers
The type ResponseHeaders[A]
models response headers carrying some information of type A
. For instance, a value of type ResponseHeaders[Origin]
describes response headers containing an origin (e.g., an Access-Control-Allow-Origin
header).
Refer to the API documentation for details about constructors and operations for the type ResponseHeaders
.
Response Entity
The type ResponseEntity[A]
models a response entity carrying some information of type A
. For instance, a value of type ResponseEntity[Event]
describes a response entity containing an event.
The Endpoints
algebra provides a few ResponseEntity
constructors and operations, which can be extended to support more content-types. For instance, the JsonEntities algebra adds support for responses with JSON entities.
Transforming Responses
Responses have methods provided by the ResponseSyntax
and the InvariantFunctorSyntax
implicit classes, whose usage is illustrated in the remaining of this section.
The orNotFound
operation is useful to handle resources that may not be found:
val getUser: Endpoint[Long, Option[User]] =
endpoint(
get(path / "user" / segment[Long]("id")),
ok(jsonResponse[User]).orNotFound()
)
In this example, servers can produce a Not Found (404) response by returning None
, and an OK (200) response containing a user by returning a Some[User]
value. Conversely, clients interpret a Not Found response as a None
value, and an OK response (with a valid user entity) as a Some[User]
value.
More generally, you can describe an alternative between two possible responses by using the orElse
operation:
val maybeUserResponse: Response[Either[Unit, User]] =
response(NotImplemented, emptyResponse).orElse(ok(jsonResponse[User]))
In this example, servers can produce a Not Implemented (501) response by returning Left(())
, and an OK (200) response containing a user by returning Right(user)
. Conversely, clients interpret a Not Implemented response as a Left(())
value, and an OK response (with a valid user entity) as a Right(user)
value.
You can also transform the type produced by the alternative responses into a more convenient type to work with, by using the xmap
operation. For instance, here is how to transform a Response[Either[Unit, User]]
into a Response[Option[User]]
:
val maybeUserResponse: Response[Option[User]] =
response(NotImplemented, emptyResponse)
.orElse(ok(jsonResponse[User]))
.xmap {
case Left(()) => None
case Right(user) => Some(user)
}(_.toRight(()))
Error Responses
endpoints server interpreters handle two kinds of errors:
- when the server is unable to decode an incoming request (because, for instance, a query parameter is missing, or the request entity has the wrong format). In this case it is a “client error” ;
- when the provided business logic throws an exception, or the server is unable to serialize the result into a proper HTTP response. In this case it is a “server error”.
By default, client errors are reported as an Invalid value, serialized into a Bad Request (400) response, as a JSON array containing string messages. You can change the provided serialization format by overriding the clientErrorsResponseEntity operation.
Similarly, by default server errors are reported as a Throwable
value, serialized into an Internal Server Error (500) response, as a JSON array containing string messages. You can change the provided serialization format by overriding the serverErrorResponseEntity operation.
Next Step
See how you can describe endpoints with JSON entities.