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]).