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")
}
)
)

}

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]