Skip to main content

Pekko HTTP Integration

Baklava provides seamless integration with Pekko HTTP server, route documentation in unit tests and serving Open API via Swagger UI.

Documenting routes in unit tests

Spec2

Most convenient way to use Baklava with Spec2 is to define base class for all tests. This can look like below:

import org.apache.pekko.http.scaladsl.marshalling.ToEntityMarshaller
import org.apache.pekko.http.scaladsl.server.Route
import org.apache.pekko.http.scaladsl.testkit.Specs2RouteTest
import org.apache.pekko.http.scaladsl.unmarshalling.FromEntityUnmarshaller
import org.specs2.mutable.SpecificationLike
import org.specs2.specification.core.{AsExecution, Fragment, Fragments}
import pl.iterators.baklava.pekkohttp.BaklavaPekkoHttp
import pl.iterators.baklava.specs2.BaklavaSpecs2

trait BaseRouteSpec
extends Specs2RouteTest
with SpecificationLike
with BaklavaPekkoHttp[Fragment, Fragments, AsExecution]
with BaklavaSpecs2[Route, ToEntityMarshaller, FromEntityUnmarshaller] {

// Define the routes to test
def allRoutes: Route = ??? // All application routes

// Required implementations for Baklava framework
override implicit val executionContext: scala.concurrent.ExecutionContext =
system.dispatcher

override def strictHeaderCheckDefault: Boolean = false

override def performRequest(
routes: Route,
request: HttpRequest
): HttpResponse =
request ~> routes ~> check {
response
}
}

Then use above as base class for your tests as below:

import org.apache.pekko.http.scaladsl.marshallers.sprayjson.SprayJsonSupport.*
import org.apache.pekko.http.scaladsl.model.HttpMethods.*
import org.apache.pekko.http.scaladsl.model.StatusCodes.*
import pl.iterators.example.baklava.UserApiServer.*

class GetUsersUserIdRouteSpec extends BaseRouteSpec {

path(path = "/users/{userId}")(
supports(
GET,
pathParameters = p[Long]("userId"),
description = "Get a specific user by ID",
summary = "Retrieve a specific user"
)(
onRequest(pathParameters = (1L))
.respondsWith[User](OK, description = "Return user with ID 1")
.assert { ctx =>
val response = ctx.performRequest(allRoutes)

response.body.id should beEqualTo(1L)
},
onRequest(pathParameters = (999L))
.respondsWith[ErrorResponse](NotFound, description = "Return 404 for non-existent user")
.assert { ctx =>
val response = ctx.performRequest(allRoutes)

response.body should beEqualTo {
ErrorResponse("User with the specified ID does not exist", "USER_NOT_FOUND")
}
}
)
)

}

MUnit

Most convenient way to use Baklava with MUnit is to define base class for all tests. Note that this requires the pekko-http-testkit-munit dependency in addition to the standard pekko-http-testkit:

libraryDependencies += "org.apache.pekko" %% "pekko-http-testkit-munit" % pekkoHttpVersion % Test

This can look like below:

import org.apache.pekko.http.scaladsl.marshalling.ToEntityMarshaller
import org.apache.pekko.http.scaladsl.server.Route
import org.apache.pekko.http.scaladsl.testkit.RouteTestTimeout
import org.apache.pekko.http.scaladsl.testkit.munit.MunitRouteTest
import org.apache.pekko.http.scaladsl.unmarshalling.FromEntityUnmarshaller
import pl.iterators.baklava.munit.{BaklavaMunit, MunitAsExecution}
import pl.iterators.baklava.pekkohttp.BaklavaPekkoHttp

trait BaseRouteTest
extends MunitRouteTest
with BaklavaPekkoHttp[Unit, Unit, MunitAsExecution]
with BaklavaMunit[Route, ToEntityMarshaller, FromEntityUnmarshaller] {

// Define the routes to test
def allRoutes: Route = ??? // All application routes

// Required implementations for Baklava framework
override implicit val executionContext: scala.concurrent.ExecutionContext =
system.dispatcher

override def strictHeaderCheckDefault: Boolean = false

override def performRequest(
routes: Route,
request: HttpRequest
): HttpResponse =
request ~> routes ~> check {
response
}
}

Then use above as base class for your tests as below:

import org.apache.pekko.http.scaladsl.marshallers.sprayjson.SprayJsonSupport.*
import org.apache.pekko.http.scaladsl.model.HttpMethods.*
import org.apache.pekko.http.scaladsl.model.StatusCodes.*
import pl.iterators.example.baklava.UserApiServer.*

class GetUsersUserIdRouteTest extends BaseRouteTest {

path(path = "/users/{userId}")(
supports(
GET,
pathParameters = p[Long]("userId"),
description = "Get a specific user by ID",
summary = "Retrieve a specific user"
)(
onRequest(pathParameters = (1L))
.respondsWith[User](OK, description = "Return user with ID 1")
.assert { ctx =>
val response = ctx.performRequest(allRoutes)

assertEquals(response.body.id, 1L)
},
onRequest(pathParameters = (999L))
.respondsWith[ErrorResponse](NotFound, description = "Return 404 for non-existent user")
.assert { ctx =>
val response = ctx.performRequest(allRoutes)

assertEquals(
response.body,
ErrorResponse("User with the specified ID does not exist", "USER_NOT_FOUND")
)
}
)
)

}

ScalaTest

Most convenient way to use Baklava with ScalaTest is to define base class for all tests. This can look like below:

import org.apache.pekko.http.scaladsl.marshalling.ToEntityMarshaller
import org.apache.pekko.http.scaladsl.server.Route
import org.apache.pekko.http.scaladsl.testkit.{RouteTestTimeout, ScalatestRouteTest}
import org.apache.pekko.http.scaladsl.unmarshalling.FromEntityUnmarshaller
import org.scalatest.funspec.AnyFunSpec
import org.scalatest.matchers.should.Matchers
import pl.iterators.baklava.pekkohttp.BaklavaPekkoHttp
import pl.iterators.baklava.scalatest.{BaklavaScalatest, ScalatestAsExecution}
import pl.iterators.example.baklava.UserApiServer

trait BaseRouteSpec
extends AnyFunSpec
with ScalatestRouteTest
with Matchers
with BaklavaPekkoHttp[Unit, Unit, ScalatestAsExecution]
with BaklavaScalatest[Route, ToEntityMarshaller, FromEntityUnmarshaller] {

// Define the routes to test
def allRoutes: Route = UserApiServer.userRoutes

// Required implementations for Baklava framework
override implicit val executionContext: scala.concurrent.ExecutionContext =
system.dispatcher

override def strictHeaderCheckDefault: Boolean = false

override def performRequest(
routes: Route,
request: HttpRequest
): HttpResponse =
request ~> routes ~> check {
response
}
}

Then use above as base class for your tests as below:

import org.apache.pekko.http.scaladsl.marshallers.sprayjson.SprayJsonSupport.*
import org.apache.pekko.http.scaladsl.model.HttpMethods.*
import org.apache.pekko.http.scaladsl.model.StatusCodes.*
import pl.iterators.example.baklava.UserApiServer.*

class GetUsersUserIdRouteSpec extends BaseRouteSpec {

path(path = "/users/{userId}")(
supports(
GET,
pathParameters = p[Long]("userId"),
description = "Get a specific user by ID",
summary = "Retrieve a specific user"
)(
onRequest(pathParameters = (1L))
.respondsWith[User](OK, description = "Return user with ID 1")
.assert { ctx =>
val response = ctx.performRequest(allRoutes)

response.body.id shouldBe 1L
},
onRequest(pathParameters = (999L))
.respondsWith[ErrorResponse](NotFound, description = "Return 404 for non-existent user")
.assert { ctx =>
val response = ctx.performRequest(allRoutes)

response.body shouldBe ErrorResponse("User with the specified ID does not exist", "USER_NOT_FOUND")
}
)
)

}

Documenting file uploads

To document a binary upload (e.g. an avatar PNG), declare Content-Type among the request headers and pass the matching value on the onRequest(...) call — the pekko-http adapter honors that declared value, overriding the content type the implicit marshaller would otherwise bake into the request. Use Array[Byte] for the body so a byte-array marshaller is in scope.

import org.apache.pekko.http.scaladsl.marshalling.{PredefinedToEntityMarshallers, ToEntityMarshaller}

import java.nio.charset.StandardCharsets

class PutUsersUserIdAvatarRouteSpec extends BaseRouteSpec {

implicit val byteArrayMarshaller: ToEntityMarshaller[Array[Byte]] =
PredefinedToEntityMarshallers.ByteArrayMarshaller

path(path = "/users/{userId}/avatar")(
supports(
PUT,
pathParameters = p[Long]("userId"),
headers = h[String]("Content-Type"),
description = "Upload or update a user's avatar",
summary = "Upload or update a user's avatar",
tags = List("Users")
)(
onRequest(
pathParameters = 1L,
headers = "image/png",
body = "\u0089PNG\r\n...".getBytes(StandardCharsets.UTF_8)
).respondsWith[EmptyBody](NoContent, description = "User avatar updated successfully")
.assert { ctx =>
val response = ctx.performRequest(allRoutes)
response.status.code shouldBe 204
}
)
)
}

The generator detects that Content-Type is a request-body media type rather than a free header, so the generated OpenAPI spec emits it as requestBody.content["image/png"] with a schema: { type: string, format: binary } instead of rendering it as a header parameter. The declared value is also used as the actual wire Content-Type of the test request, so server routes that pattern-match on it (e.g. entity(as[Array[Byte]])) run under the right conditions.

Downloads

Binary downloads work with respondsWith[Array[Byte]]. The pekko-http test needs a byte-array entity unmarshaller in scope; the test stub's response must carry the right Content-Type — whatever the server serves becomes the OpenAPI responseContentType:

import org.apache.pekko.http.scaladsl.unmarshalling.{FromEntityUnmarshaller, PredefinedFromEntityUnmarshallers}

implicit val byteArrayUnmarshaller: FromEntityUnmarshaller[Array[Byte]] =
PredefinedFromEntityUnmarshallers.byteArrayUnmarshaller

supports(
GET,
pathParameters = p[Long]("userId"),
description = "Download the user's avatar as raw image bytes",
tags = List("Users")
)(
onRequest(pathParameters = 1L)
.respondsWith[Array[Byte]](OK, description = "Avatar bytes")
.assert { ctx =>
val response = ctx.performRequest(allRoutes)
response.status.code shouldBe 200
}
)

Schema[Array[Byte]] is a default on the classpath, so the generated OpenAPI renders responses[code].content["<content-type>"].schema = { type: string, format: binary }.

Serving Open API and Swagger UI

Adding baklava-pekko-http-routes dependency to your project you can easily serve Open API and Swagger UI:

import org.apache.pekko.actor.typed.ActorSystem
import org.apache.pekko.actor.typed.scaladsl.Behaviors
import org.apache.pekko.http.scaladsl.Http
import org.apache.pekko.http.scaladsl.server.Route
import com.typesafe.config.Config
import pl.iterators.baklava.routes.BaklavaRoutes

import scala.concurrent.ExecutionContext
import scala.io.StdIn

def main(args: Array[String]): Unit = {
implicit val system: ActorSystem[Nothing] = ActorSystem(Behaviors.empty, "api-actor-system")
implicit val executionContext: ExecutionContext = system.executionContext
val config: Config = system.settings.config

val apiRoute: Route = ??? // all your api routes
val apiAndSwaggerRoute: Route = apiRoute ~ BaklavaRoutes.routes(config)

val bindingFuture = Http().newServerAt("localhost", 8080).bind(apiAndSwaggerRoute)

println(s"Server online at http://localhost:8080/")
println("Press RETURN to stop...")
StdIn.readLine()
bindingFuture
.flatMap(_.unbind())
.onComplete(_ => system.terminate())
}

For detailed configuration options check [installation.md#swaggerui-and-routes-configuration]

Documenting multipart/form-data uploads

Beyond raw binary bodies (above), Multipart bundles one or more named parts — typed as FilePart (binary data with its own MIME type + optional filename) or TextPart (a plain form field) — into a single request. The pekko-http adapter reassembles them into a native Multipart.FormData body with a deterministic boundary.

import pl.iterators.baklava.{FilePart, Multipart, TextPart}
import java.nio.charset.StandardCharsets

class PostUsersUserIdPhotoSpec extends BaseRouteSpec {

path(path = "/users/{userId}/photo")(
supports(
POST,
pathParameters = p[Long]("userId"),
description = "Upload a profile photo with a caption",
summary = "Upload photo",
tags = List("Users")
)(
onRequest(
pathParameters = 1L,
body = Multipart(
FilePart("photo", "image/png", "photo.png",
"\u0089PNG\r\n...".getBytes(StandardCharsets.UTF_8)),
TextPart("caption", "profile photo")
)
).respondsWith[EmptyBody](NoContent, description = "Photo uploaded")
.assert { ctx =>
val response = ctx.performRequest(allRoutes)
response.status.status should beEqualTo(NoContent.status)
}
)
)
}

The generated OpenAPI emits requestBody.content["multipart/form-data"] with a free-form type: object schema (describing each per-part schema would need a runtime-derived Schema; users who need richer docs can replace the implicit Schema[Multipart]).