Running as a Play server

Tapir supports both Play 2.9, which still ships with Akka, and Play 3.0, which replaces Akka with Pekko. See the Play framework documentation for differences between these versions.

To expose an endpoint as a play-server, using Play 2.9 with Akka, add the following dependencies:

"com.softwaremill.sttp.tapir" %% "tapir-play29-server" % "1.10.1"

and (if you don’t already depend on Play)

"org.playframework" %% "play-akka-http-server" % "2.9.2"


"org.playframework" %% "play-netty-server" % "2.9.2"

depending on whether you want to use netty or Akka based http-server under the hood. Please note that Play 2.9 server is available only for Scala 2.13.

To expose an endpoint as a play-server, using Play 3.0 with Pekko, add the following dependencies:

"com.softwaremill.sttp.tapir" %% "tapir-play-server" % "1.10.1"

and (if you don’t already depend on Play)

"org.playframework" %% "play-pekko-http-server" % "3.0.2"


"org.playframework" %% "play-netty-server" % "3.0.2"

depending on whether you want to use netty or Pekko based http-server under the hood.

The following code samples use Play 3.0 with Pekko. If you are using Play 2.9 with Akka, simply replace the import of with import

Then import the object:


The toRoutes method requires a single, or a list of ServerEndpoints, which can be created by adding server logic to an endpoint. For example:

import play.api.routing.Router.Routes
import sttp.tapir._

import scala.concurrent.Future

implicit val materializer: Materializer = ???

def countCharacters(s: String): Future[Either[Unit, Int]] =
  Future(Right[Unit, Int](s.length))

val countCharactersEndpoint: PublicEndpoint[String, Unit, Int, Any] =[Int])
val countCharactersRoutes: Routes =
  PlayServerInterpreter().toRoutes(countCharactersEndpoint.serverLogic(countCharacters _))


A single Play application can contain both tapir-managed and Play-managed routes. However, because of the routing implementation in Play, the shape of the paths that tapir and other Play handlers serve should not overlap. The shape of the path includes exact path segments, single- and multi-wildcards. Otherwise, request handling will throw an exception. We don’t expect users to encounter this as a problem, however the implementation here diverges a bit comparing to other interpreters.

Bind the routes

Creating the HTTP server manually

An HTTP server can then be started as in the following example:

import play.core.server._
import play.api.routing.Router.Routes

val aRoute: Routes = ???

object Main {
  // JVM entry point that starts the HTTP server
  def main(args: Array[String]): Unit = {
    val playConfig = ServerConfig(port =
    NettyServer.fromRouterWithComponents(playConfig) { components =>

As part of an existing Play application

Or, if you already have an existing Play application, you can create a Router class and bind it to the application.

First, add a line like following in the routes files:

->      /api                        api.ApiRouter

Then create a class like this:

class ApiRouter @Inject() () extends SimpleRouter {
  override def routes: Routes = {

Find more details about how to bind a Router to your application in the Play framework documentation.

Web sockets

The interpreter supports web sockets, with pipes of type Flow[REQ, RESP, Any]. See web sockets for more details.

The interpreter does not expose control frames (Ping, Pong and Close), so any setting regarding them are discarded, however those that are emitted are sent to the client.


The interpreter can be configured by providing a PlayServerOptions value, see server options for details.