Inputs/outputs¶
An input is described by an instance of the EndpointInput
trait, and an output by an instance of the EndpointOutput
trait. Some inputs can be used both as inputs and outputs; then, they additionally implement the EndpointIO
trait.
Each input or output can yield/accept a value (but doesn’t have to).
For example, 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 Int
.
The 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 typeT
- 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 typeEndpointInput[Unit]
) paths
, which maps to the whole remaining path as aList[String]
query[T](name)
captures a query parameter with the given namequeryParams
captures all query parameters, represented asQueryParams
cookie[T](name)
captures a cookie from theCookie
header with the given nameextractFromRequest
extracts 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 nameheaders
captures all headers, represented asList[Header]
cookies
captures cookies from theCookie
header and represents them asList[Cookie]
setCookie(name)
captures the value & metadata of the aSet-Cookie
header with a matching namesetCookies
captures cookies from theSet-Cookie
header and represents them asList[SetCookie]
stringBody
,plainBody[T]
,jsonBody[T]
,rawBinaryBody[R]
,binaryBody[R, T]
,formBody[T]
,multipartBody[T]
captures the bodystreamBody[S]
captures the body as a stream: only a client/server interpreter supporting streams of typeS
can be used with such an endpoint
For outputs:
statusCode
maps to the status code of the responsestatusCode(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
parameters, 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 in
, out
and 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 api/v1.0/status
path.
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
of the DecodeResult
trait.
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).
The Endpoint.mapIn
, Endpoint.mapInTo
etc. have the same signatures are the ones above.
Path matching¶
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
, /api/
, but won’t match
/
, /api/users
.
To match only the root path, use an empty string: endpoint.in("")
will match http://server.com/
and
http://server.com
.
To match a path prefix, first define inputs which match the path prefix, and then capture any remaining part using
paths
, e.g.: endpoint.in("api" / "download").in(paths)"
.
Next¶
Read on about status codes.