tapir, or Typed API descRiptions
With tapir you can describe HTTP API endpoints as immutable Scala values. Each endpoint can contain a number of input parameters, error-output parameters, and normal-output parameters. An endpoint specification can then be translated to:
- a server, given the "business logic": a function, which computes output parameters based on input parameters. Currently supported:
- a client, which is a function from input parameters to output parameters. Currently supported: sttp.
- documentation. Currently supported: OpenAPI.
import tapir._ import tapir.json.circe._ import io.circe.generic.auto._ type Limit = Int type AuthToken = String case class BooksFromYear(genre: String, year: Int) case class Book(title: String) val booksListing: Endpoint[(BooksFromYear, Limit, AuthToken), String, List[Book], Nothing] = endpoint .get .in(("books" / path[String]("genre") / path[Int]("year")).mapTo(BooksFromYear)) .in(query[Int]("limit").description("Maximum number of books to retrieve")) .in(header[String]("X-Auth-Token")) .errorOut(stringBody) .out(jsonBody[List[Book]]) // import tapir.docs.openapi._ import tapir.openapi.circe.yaml._ val docs = booksListing.toOpenAPI("My Bookshop", "1.0") println(docs.toYaml) // import tapir.server.akkahttp._ import akka.http.scaladsl.server.Route import scala.concurrent.Future def bookListingLogic(bfy: BooksFromYear, limit: Limit, at: AuthToken): Future[Either[String, List[Book]]] = Future.successful(Right(List(Book("The Sorrows of Young Werther")))) val booksListingRoute: Route = booksListing.toRoute(bookListingLogic _) // import tapir.client.sttp._ import com.softwaremill.sttp._ val booksListingRequest: Request[Either[String, List[Book]], Nothing] = booksListing .toSttpRequest(uri"http://localhost:8080") .apply(BooksFromYear("SF", 2016), 20, "xyz-abc-123")
Also check out the runnable example available in the repository.
Goals of the project
- programmer-friendly, human-comprehensible types, that you are not afraid to write down
- (also inferencable by IntelliJ)
- discoverable API through standard auto-complete
- separate "business logic" from endpoint definition & documentation
- as simple as possible to generate a server, client & docs
- based purely on case class-based, immutable and reusable data structures
- first-class OpenAPI support. Provide as much or as little detail as needed.
- reasonably type safe: only, and as much types to safely generate the server/client/docs
Working with tapir
To use tapir, add the following dependency to your project:
"com.softwaremill.tapir" %% "tapir-core" % "0.0.9"
This will import only the core classes. To generate a server or a client, you will need to add further dependencies.
Most of tapir functionalities use package objects which provide builder and extensions methods, hence it's easiest to work with tapir if you import whole packages, e.g.:
If you don't have it already, you'll also need partial unification enabled in the compiler (alternatively, you'll need to manually provide type arguments in some cases). In sbt, this is:
scalacOptions += "-Ypartial-unification"
To see an example project using Tapir, check out this Todo-Backend using Tapir and Http4s.
Anatomy an endpoint
An endpoint is represented as a value of type
Endpoint[I, E, O, S], where:
Iis the type of the input parameters
Eis the type of the error-output parameters
Ois the type of the output parameters
Sis the type of streams that are used by the endpoint's inputs/outputs
Output parameters can be:
- of type
Unit, when there's no input/ouput of the given type
- a single type
- a tuple of types
You can think of an endpoint as a function, which takes input parameters of type
I and returns a result of type
Defining an endpoint
The description of an endpoint is an immutable case class, which includes a number of methods:
description, etc. methods allow modifying the endpoint information, which will then be included in the endpoint documentation
postetc. methods specify the method using which the endpoint should support
outmethods allow adding a new input/output parameter
mapInTo, ... methods allow mapping the current input/output parameters to another value or to a case class
An important note on mapping: in tapir, all mappings are bi-directional (they specify an isomorphism between two values). That's because each mapping can be used to generate a server or a client, as well as in many cases can be used both for input and for output.
Defining an endpoint input/output
An input is represented as an instance of the
EndpointInput trait, and an output as an instance of the
EndpointIO trait (all outputs can also be used as inputs). 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
- any string, which will be implicitly converted to a constant path segment. Path segments can be combined with the
query[T](name)captures a query parameter with the given name
header[T](name)captures a header with the given name
jsonBody[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
For outputs, you can use the
body family of methods.
Endpoint inputs/outputs can be combined using the
.and method. Such a combination results in an input/output represented as a tuple of the given types. Inputs/outputs can also be mapped over, either using the
.map method and providing mappings in both directions, or the
.mapTo method for mapping into a case class.
A codec specifies how to map from and to raw textual values that are sent over the network. There are some built-in codecs for most common types such as
Int etc. Codecs are usually defined as implicit values and resolved implicitly when they are referenced.
For example, a
query[Int]("quantity") specifies an input parameter which will be read from the
quantity query parameter and decoded into an
Int. If the value cannot be parsed to an int, the endpoint won't match the request.
Optional parameters are represented as
Option values, e.g.
Codecs carry an additional type parameter, which specifies the media type. There are two built-in media types for now:
Hence, it is possible to have a
Codec[MyCaseClass, Text] which specified how to serialize a case class to plain text, and a different
Codec[MyCaseClass, Json], which specifies how to serialize a case class to json.
When defining a path, query or header parameter, only a codec with the
Text media type can be used.
A codec also contains the schema of the mapped type. This schema information is used when generating documentation. For primitive types, the schema values are built-in. For complex types, it is possible to define the schema by hand (by creating an implicit value of type
SchemaFor[T]), however usually this will be automatically derived for case classes using Magnolia.
It is possible to configure generic derivation to use snake-case, kebab-case or custom field naming policy.
implicit val customConfiguration = Configuration.defaults.snakeCaseTransformation
Working with json
"com.softwaremill.tapir" %% "tapir-json-circe" % "0.0.9"
contains codecs which, given a circe
Decoder in scope, will generate a codec using the json media type.
Running as an akka-http server
"com.softwaremill.tapir" %% "tapir-akka-http-server" % "0.0.9"
To expose an endpoint as an akka-http server, import the package:
This adds two extension methods to the
toRoute. Both require the logic of the endpoint to be given as a function of type:
[I as function arguments] => Future[Either[E, O]]
Note that the function doesn't take the tuple
I directly as input, but instead this is converted to a function of the appropriate arity. The created
Directive can then be further combined with other akka-http directives.
As an endpoint can be interpreted to an akka-http directive or route, it is possible to nest and combine it with other routes. It's completely feasible that some part of the input is read using akka-http directives, and the rest using Tapir endpoint descriptions; or, that the Tapir-generated route is wrapped in e.g. a metrics route. Moreover, "edge-case endpoints", which require some special logic not expressible using Tapir, can be always implemented directly using akka-http.
Using as an sttp client
"com.softwaremill.tapir" %% "tapir-sttp-client" % "0.0.9"
To make requests using an endpoint definition using sttp, import:
This adds the
toRequest(Uri) extension method to any
Endpoint instance which, given the given base URI returns a function:
[I as function arguments] => Request[Either[E, O], Nothing]
After providing the input parameters, the result is a description of the request to be made, which can be further customised and sent using any sttp backend.
"com.softwaremill.tapir" %% "tapir-openapi-docs" % "0.0.9" "com.softwaremill.tapir" %% "tapir-openapi-circe-yaml" % "0.0.9"
Tapir contains a case class-based model of the openapi data structures in the
openapi/openapi-model subproject. An endpoint can be converted to an instance of the model by importing the package and calling an extension method:
import tapir.docs.openapi._ val docs = booksListing.toOpenAPI("My Bookshop", "1.0")
Such a model can then be refined, by adding details which are not auto-generated, or by adding whole new endpoints which are exposed by the service not through Tapir, but e.g. using a hand-written akka-http route. Working with a deeply nested case class structure such as the
OpenAPI one can be made easier by using a lens library, e.g. Quicklens.
The openapi case classes can then be serialised, either to JSON or YAML using Circe:
import tapir.openapi.circe.yaml._ println(docs.toYaml)
There's a number of similar projects from which Tapir draws inspiration:
- streaming support
- multi-part bodies
- file support, non-string-based bodies, form-fields bodies
- coproducts/sealed trait families/discriminators in object hierarchies
- support for OpenAPI's formats (base64, binary, email, ...)
- ... and much more :)
See the list of issues and pick one!
Creating your own Tapir
Tapir uses a number of packages which contain either the data classes for describing endpoints or interpreters of this data (turning endpoints into a server or a client). Importing these packages every time you want to use Tapir may be tedious, that's why each package object inherits all of its functionality from a trait.
Hence, it is possible to create your own object which combines all of the required functionalities and provides a single-import whenever you want to use Tapir. For example:
object MyTapir extends Tapir with AkkaHttpServer with SttpClient with CirceJson with OpenAPICirceYaml
Then, a single
import MyTapir._ and all Tapir data types and extensions methods will be in scope!
Tuple-concatenating code is copied from akka-http
Generic derivation configuration is copied from circe
Tapir is an early stage project. Everything might change. Be warned: the code is often ugly, uses mutable state, imperative loops and type casts. Luckily, there's no reflection. All suggestions welcome :)