Note
The tutorial is also available as a video.
We already know how to expose an endpoint as an HTTP server. Let’s now generate documentation for the API in theOpenAPI format, and expose it using the Swagger UI.
OpenAPI is a widely used format for describing HTTP APIs, which can be serialized to JSON or YAML. Such a description canbe later used to generate client code in a given programming language, server stubs or programmer-friendlydocumentation. In our case, we’ll use the last option, with the help of Swagger UI, which allows both browsing andinvoking the endpoints. There are also alternative UIs, such as Redoc.
Generating the OpenAPI specification and exposing the Swagger UI might be done separately. However, we’lluse a bundle, which first interprets the provided tapir endpoints into OpenAPI and then returns another set ofendpoints, which expose the UI together with the generated specification. We’ll need to add a dependency:
//> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.10.13
We’ll also define and expose two endpoints as an HTTP server, as described in the previous tutorial. Hence, ourstarting setup of docs.scala
is as follows:
//> using dep com.softwaremill.sttp.tapir::tapir-core:1.10.13//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.10.13//> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.10.13import sttp.tapir.*import sttp.tapir.server.netty.sync.NettySyncServer@main def tapirDocs(): Unit = val e1 = endpoint .get.in("hello" / "world").in(query[String]("name")) .out(stringBody) .handleSuccess(name => s"Hello, $name!") val e2 = endpoint .post.in("double").in(stringBody) .errorOut(stringBody) .out(stringBody) .handle { s => s.toIntOption.fold(Left(s"$s is not a number"))(n => Right((n*2).toString)) } NettySyncServer().port(8080) .addEndpoints(List(e1, e2)) .startAndWait()
We’ve got two endpoints, one corresponding to GET /hello/world
, the other a POST /double
. We can test them fromthe command line as before:
# first console% scala-cli docs.scala# another console% curl -XPOST "http://localhost:8080/double" -d "21"42% curl -XPOST "http://localhost:8080/double" -d "XYZ"XYZ is not a number
NettySyncServer
is a server interpreter: it takes a list of endpoints (in our case, List(e1, e2)
), and, basing ontheir description and the server logic, exposes them as an HTTP server. Similarly, a documentation interpretertakes a list of endpoints and, based on their description only (server logic is not needed here), generates the OpenAPIdocumentation.
One possibility is to generate the YAML or JSON file, save it to disk, share it with other projects, etc. But in ourcase, as mentioned at the beginning, we’ll use the rendered specification to expose a UI, allowing browsingour API. The UI itself needs to be exposed using HTTP. Hence, we’ll need to generate tapir endpoints, which will servethe appropriate resources (such as the UI’s index.html
, the CSS files, and the generated OpenAPI specification).
This amounts to invoking the SwaggerInterpreter
:
val swaggerEndpoints = SwaggerInterpreter() .fromEndpoints[Identity](List(e1, e2), "My App", "1.0")
By default, the generated endpoints will expose the UI using the /docs
path, but this can be customized using theoptions.
Note
You might wonder what is the purpose and meaning of the Identity
type parameter, which is used when calling thefromEndpoints
method.
Tapir integrates with multiple Scala stacks, including Pekko, Akka, cats-effect, or ZIO. We’ll learn how to use themin subsequent tutorials. They all use different types to represent effectful or asynchronous computations. So far, we’veused direct-style, synchronous code, which doesn’t use any “wrapper” types to represent computations.
In some cases, tapir has dedicated APIs to work with direct-style, such as providing the server logic for an endpointusing .handle
(when using the IO effect from cats-effect, server logic is provided using the serverLogic[IO]
method). In other cases, there’s a single set of APIs for direct and “wrapped” style - such as the API to generatethe documentation & swagger endpoints above.
The Identity
type constructor is a simple type alias: type Identity[X] = X
. It can be used whenever a typeconstructor parameter (typically called F[_]
) is required, and when we’re using direct-style, meaning thatcomputations run synchronously in a blocking way.
And that’s almost all the code changes that we need to introduce! We only need to additionally expose theswaggerEndpoints
using our HTTP server:
//> using dep com.softwaremill.sttp.tapir::tapir-core:1.10.13//> using dep com.softwaremill.sttp.tapir::tapir-netty-server-sync:1.10.13//> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.10.13import sttp.shared.Identityimport sttp.tapir.*import sttp.tapir.server.netty.sync.NettySyncServerimport sttp.tapir.swagger.bundle.SwaggerInterpreter@main def tapirDocs(): Unit = val e1 = endpoint .get.in("hello" / "world").in(query[String]("name")) .out(stringBody) .handleSuccess(name => s"Hello, $name!") val e2 = endpoint .post.in("double").in(stringBody) .out(stringBody) .errorOut(stringBody) .handle { s => s.toIntOption.fold(Left(s"$s is not a number"))(n => Right((n*2).toString)) } val swaggerEndpoints = SwaggerInterpreter() .fromServerEndpoints[Identity](List(e1, e2), "My App", "1.0") NettySyncServer().port(8080) .addEndpoints(List(e1, e2)) .addEndpoints(swaggerEndpoints) .startAndWait()
Try running the code, and opening http://localhost:8080/docs
in your browser:
% scala-cli docs.scala# Now open http://localhost:8080/docs in your browser
Browse the Swagger UI and invoke the endpoints. The generated OpenAPI specification should be available athttp://localhost:8080/docs/docs.yaml
:
openapi: 3.1.0info: title: My App version: '1.0'paths: /hello/world: get: operationId: getHelloWorld parameters: - name: name in: query required: true schema: type: string responses: '200': description: '' content: text/plain: schema: type: string '400': description: 'Invalid value for: query parameter name' content: text/plain: schema: type: string /double: post: operationId: postDouble requestBody: content: text/plain: schema: type: string required: true responses: '200': description: '' content: text/plain: schema: type: string default: description: '' content: text/plain: schema: type: string
This wraps up the tutorial on generating and exposing OpenAPI documentation. If you’d like to customize some of theoptions, tapir’s OpenAPI reference documentation should help.