An input is described by an instance of the
EndpointInput trait, and an output by an instance of the
trait. Some inputs can be used both as inputs and outputs; then, they additionally implement the
Each input or output can yield/accept a value (but doesn’t have to).
query[Int]("age"): EndpointInput[Int] describes an input, which is the
age parameter from the URI’s
query, and which should be coded (using the string-to-integer codec) as an
tapir package contains a number of convenience methods to define an input or an output for an endpoint.
For inputs, these are:
path[T], which captures a path segment as an input parameter of type
- any string, which will be implicitly converted to a fixed path segment. Path segments can be combined with the
/method, and don’t map to any values (have type
paths, which maps to the whole remaining path as a
query[T](name)captures a query parameter with the given name
queryParamscaptures all query parameters, represented as
cookie[T](name)captures a cookie from the
Cookieheader with the given name
extractFromRequestextracts a value from the request. This input is only used by server interpreters, ignored by documentation interpreters. Client interpreters ignore the provided value.
For both inputs/outputs:
header[T](name)captures a header with the given name
headerscaptures all headers, represented as
cookiescaptures cookies from the
Cookieheader and represents them as
setCookie(name)captures the value & metadata of the a
Set-Cookieheader with a matching name
setCookiescaptures cookies from the
Set-Cookieheader and represents them as
multipartBody[T]captures the body
streamBody[S]captures the body as a stream: only a client/server interpreter supporting streams of type
Scan be used with such an endpoint
statusCodemaps to the status code of the response
statusCode(code)maps to a fixed status code of the response
Combining inputs and outputs¶
Endpoint inputs/outputs can be combined in two ways. However they are combined, the values they represent always accumulate into tuples of values.
First, inputs/outputs can be combined using the
.and method. Such a combination results in an input/output, which maps
to a tuple of the given types. This combination can be assigned to a value and re-used in multiple endpoints. As all
other values in tapir, endpoint input/output descriptions are immutable. For example, an input specifying two query
start (mandatory) and
limit (optional) can be written down as:
import sttp.tapir._ import sttp.tapir.json.circe._ import io.circe.generic.auto._ import java.util.UUID case class User(name: String) val paging: EndpointInput[(UUID, Option[Int])] = query[UUID]("start").and(query[Option[Int]]("limit")) // we can now use the value in multiple endpoints, e.g.: val listUsersEndpoint: Endpoint[(UUID, Option[Int]), Unit, List[User], Any] = endpoint.in("user" / "list").in(paging).out(jsonBody[List[User]])
Second, inputs can be combined by calling the
errorOut methods on
Endpoint multiple times. Each time
such a method is invoked, it extends the list of inputs/outputs. This can be useful to separate different groups of
parameters, but also to define template-endpoints, which can then be further specialized. For example, we can define a
base endpoint for our API, where all paths always start with
/api/v1.0, and errors are always returned as a json:
import sttp.tapir._ import sttp.tapir.json.circe._ import io.circe.generic.auto._ case class ErrorInfo(message: String) val baseEndpoint: Endpoint[Unit, ErrorInfo, Unit, Any] = endpoint.in("api" / "v1.0").errorOut(jsonBody[ErrorInfo])
Thanks to the fact that inputs/outputs accumulate, we can use the base endpoint to define more inputs, for example:
case class Status(uptime: Long) val statusEndpoint: Endpoint[Unit, ErrorInfo, Status, Any] = baseEndpoint.in("status").out(jsonBody[Status])
The above endpoint will correspond to the
Mapping over input/output values¶
Inputs/outputs can also be mapped over. As noted before, all mappings are bi-directional, so that they can be used both when interpreting an endpoint as a server, and as a client, as well as both in input and output contexts.
There’s a couple of ways to map over an input/output. First, there’s the
map[II](f: I => II)(g: II => I) method,
which accepts functions which provide the mapping in both directions. For example:
import sttp.tapir._ import java.util.UUID case class Paging(from: UUID, limit: Option[Int]) val paging: EndpointInput[Paging] = query[UUID]("start").and(query[Option[Int]]("limit")) .map(input => Paging(input._1, input._2))(paging => (paging.from, paging.limit))
Next, you can use
mapDecode[II](f: I => DecodeResult[II])(g: II => I), to handle cases where decoding (mapping a
low-level value to a higher-value one) can fail. There’s a couple of failure reasons, captured by the alternatives
Mappings can also be done given an
Mapping[I, II] instance. More on that in the secion on codecs.
Creating a mapping between a tuple and a case class is a common operation, hence there’s also a
mapTo(CaseClassCompanion) method, which automatically provides the functions to construct/deconstruct the case class:
val paging: EndpointInput[Paging] = query[UUID]("start").and(query[Option[Int]]("limit")) .mapTo(Paging)
Mapping methods can also be called on an endpoint (which is useful if inputs/outputs are accumulated, for example).
Endpoint.mapInTo etc. have the same signatures are the ones above.
By default (as with all other types of inputs), if no path input/path segments are defined, any path will match.
If any path input/path segment is defined, the path must match exactly - any remaining path segments will cause the
endpoint not to match the request. For example,
endpoint.in("api") will match
/api/, but won’t match
To match only the root path, use an empty string:
endpoint.in("") will match
To match a path prefix, first define inputs which match the path prefix, and then capture any remaining part using
endpoint.in("api" / "download").in(paths)".