Skip to main content

Installation

To use Baklava, you need to choose:

  1. HTTP Integration: Pekko HTTP, HTTP4s (for routes you serve), or sttp (for remote APIs you consume)
  2. Test Framework: Specs2, ScalaTest, or MUnit
  3. Output Format: Simple, OpenAPI, or TS-REST

Quick Setup Guide​

1. Add the SBT Plugin​

Add the Baklava plugin to your project/plugins.sbt:

addSbtPlugin("pl.iterators" % "baklava-sbt-plugin" % "VERSION")

Enable the plugin in your build.sbt:

enablePlugins(BaklavaSbtPlugin)

2. Choose Your Dependencies​

Add the required dependencies to your build.sbt based on your choices:

Core Dependencies (Required)​

HTTP Integration - Choose one:

// For Pekko HTTP
libraryDependencies += "pl.iterators" %% "baklava-pekko-http" % "VERSION" % Test

// For HTTP4s
libraryDependencies += "pl.iterators" %% "baklava-http4s" % "VERSION" % Test

// For remote/third-party APIs via sttp-client
libraryDependencies += "pl.iterators" %% "baklava-sttp" % "VERSION" % Test

Test Framework Integration - Choose one:

// For Specs2
libraryDependencies += "pl.iterators" %% "baklava-specs2" % "VERSION" % Test

// For ScalaTest
libraryDependencies += "pl.iterators" %% "baklava-scalatest" % "VERSION" % Test

// For MUnit
libraryDependencies += "pl.iterators" %% "baklava-munit" % "VERSION" % Test

Output Format Dependencies (Required)​

Choose one or more output formats:

// Simple format (no additional configuration required)
libraryDependencies += "pl.iterators" %% "baklava-simple" % "VERSION" % Test

// OpenAPI format (requires additional configuration - see below)
libraryDependencies += "pl.iterators" %% "baklava-openapi" % "VERSION" % Test

// TS-REST format (requires additional configuration - see below)
libraryDependencies += "pl.iterators" %% "baklava-tsrest" % "VERSION" % Test

// oRPC contract format
libraryDependencies += "pl.iterators" %% "baklava-orpc" % "VERSION" % Test

Optional: SwaggerUI Support​

SwaggerUI is available for both Pekko HTTP and http4s (combined with the OpenAPI output format). Add the matching routes module:

// Pekko HTTP
libraryDependencies += "pl.iterators" %% "baklava-pekko-http-routes" % "VERSION"

// http4s
libraryDependencies += "pl.iterators" %% "baklava-http4s-routes" % "VERSION"

Configuration​

Basic Configuration​

The minimal configuration required in your build.sbt:

inConfig(Test)(
BaklavaSbtPlugin.settings(Test) ++ Seq(
fork := false
)
)

Format-Specific Configuration​

OpenAPI Configuration (Required for OpenAPI format)​

If you're using the OpenAPI format, add this configuration:

inConfig(Test)(
BaklavaSbtPlugin.settings(Test) ++ Seq(
fork := false,
baklavaGenerateConfigs := Map(
"openapi-info" ->
s"""
|{
| "openapi" : "3.0.1",
| "info" : {
| "title" : "My API",
| "version" : "1.0.0"
| }
|}
|""".stripMargin
)
)
)

TS-REST Configuration (Required for TS-REST format)​

If you're using the TS-REST format, add this to your baklavaGenerateConfigs map:

baklavaGenerateConfigs := Map(
"ts-rest-package-contract-json" ->
"""
|{
| "name": "your api package e.g.: @company/backend-contracts",
| "version": "VERSION",
| "main": "index.js",
| "types": "index.d.ts"
|}
|""".stripMargin
)

Combined Configuration Example​

If you're using both OpenAPI and TS-REST formats:

inConfig(Test)(
BaklavaSbtPlugin.settings(Test) ++ Seq(
fork := false,
baklavaGenerateConfigs := Map(
"openapi-info" ->
s"""
|{
| "openapi" : "3.0.1",
| "info" : {
| "title" : "My API",
| "version" : "1.0.0"
| }
|}
|""".stripMargin,
"ts-rest-package-contract-json" ->
"""
|{
| "name": "@company/backend-contracts",
| "version": "1.0.0",
| "main": "index.js",
| "types": "index.d.ts"
|}
|""".stripMargin
)
)
)

Complete Example Configurations​

Example 1: Pekko HTTP + ScalaTest + OpenAPI with SwaggerUI​

// build.sbt
enablePlugins(BaklavaSbtPlugin)

libraryDependencies ++= Seq(
"pl.iterators" %% "baklava-pekko-http" % "VERSION" % Test,
"pl.iterators" %% "baklava-scalatest" % "VERSION" % Test,
"pl.iterators" %% "baklava-openapi" % "VERSION" % Test,
"pl.iterators" %% "baklava-pekko-http-routes" % "VERSION" // For SwaggerUI
)

inConfig(Test)(
BaklavaSbtPlugin.settings(Test) ++ Seq(
fork := false,
baklavaGenerateConfigs := Map(
"openapi-info" ->
s"""
|{
| "openapi" : "3.0.1",
| "info" : {
| "title" : "My API",
| "version" : "1.0.0"
| }
|}
|""".stripMargin
)
)
)

Example 2: HTTP4s + MUnit + Simple Format​

// build.sbt
enablePlugins(BaklavaSbtPlugin)

libraryDependencies ++= Seq(
"pl.iterators" %% "baklava-http4s" % "VERSION" % Test,
"pl.iterators" %% "baklava-munit" % "VERSION" % Test,
"pl.iterators" %% "baklava-simple" % "VERSION" % Test
)

inConfig(Test)(
BaklavaSbtPlugin.settings(Test) ++ Seq(
fork := false
)
)

To serve SwaggerUI from an http4s app, add baklava-http4s-routes and the OpenAPI format:

libraryDependencies ++= Seq(
"pl.iterators" %% "baklava-http4s" % "VERSION" % Test,
"pl.iterators" %% "baklava-openapi" % "VERSION" % Test,
"pl.iterators" %% "baklava-http4s-routes" % "VERSION"
)

Example 3: Pekko HTTP + Specs2 + Multiple Formats​

// build.sbt
enablePlugins(BaklavaSbtPlugin)

libraryDependencies ++= Seq(
"pl.iterators" %% "baklava-pekko-http" % "VERSION" % Test,
"pl.iterators" %% "baklava-specs2" % "VERSION" % Test,
"pl.iterators" %% "baklava-simple" % "VERSION" % Test,
"pl.iterators" %% "baklava-openapi" % "VERSION" % Test,
"pl.iterators" %% "baklava-tsrest" % "VERSION" % Test,
"pl.iterators" %% "baklava-orpc" % "VERSION" % Test,
"pl.iterators" %% "baklava-pekko-http-routes" % "VERSION" // For SwaggerUI
)

inConfig(Test)(
BaklavaSbtPlugin.settings(Test) ++ Seq(
fork := false,
baklavaGenerateConfigs := Map(
"openapi-info" ->
s"""
|{
| "openapi" : "3.0.1",
| "info" : {
| "title" : "My API",
| "version" : "1.0.0"
| }
|}
|""".stripMargin,
"ts-rest-package-contract-json" ->
"""
|{
| "name": "@company/backend-contracts",
| "version": "1.0.0",
| "main": "index.js",
| "types": "index.d.ts"
|}
|""".stripMargin
)
)
)

SBT Tasks​

The plugin provides several SBT tasks:

  • baklavaGenerate - Generate API documentation from the stored calls (runs automatically after tests)
  • baklavaClean - Delete generated documentation and the stored calls
Incremental runs are fine

Captured calls are kept in a persistent store under target/baklava/store/, one directory per test suite. Every run replaces the entries of the suites that captured at least one call and keeps the rest, then regenerates the documentation from the whole store. Plain sbt test therefore produces complete documentation even on sbt 2, whose test is incremental and may run only some suites, or none. The store cannot help when sbt 2 skips a suite that has no entry in it: on a checkout without target/ on a machine whose sbt cache already holds the results (a second clone, or CI restoring ~/.cache/sbt but not target/), after baklavaClean (or clean on sbt 1, which deletes target/), or when a suite that had been removed comes back unchanged, for example after switching branches, because sbt 2 caches test results by content. Run sbt testFull once in that case, and prefer testFull over test in CI on sbt 2. Baklava prints a message when the store is empty.

The documentation will be generated in target/baklava/ directory after running your tests:

  • Simple format: target/baklava/simple/
  • OpenAPI format: target/baklava/openapi/openapi.yml
  • TS-REST format: target/baklava/tsrest/
  • oRPC contract format: target/baklava/orpc/

Incremental runs and testOnly​

  • sbt testOnly com.acme.UsersRouterSpec refreshes the documentation of that suite and keeps every other suite's documentation as it was. The same holds for testQuick and for sbt 2's incremental test.
  • Filtering inside a suite (for example testOnly com.acme.UsersRouterSpec -- -z "GET /users") replaces that suite's stored calls with the subset that ran. Run the suite unfiltered afterwards.
  • A suite that still exists but no longer makes any Baklava call keeps its last stored calls. Rename or delete the suite, or run baklavaClean followed by a full test run, to drop them.
  • Only sbt's default single test group is supported. With one JVM per suite (testGrouping), sbt may start the next group's JVM while the previous group's generation is still merging and clearing the shared staging directory, so a suite's stored calls can end up incomplete; that holds even with the default sequential group limit.
  • sbt baklavaGenerate on its own regenerates all output from the store without re-running tests, for example after changing baklavaGenerateConfigs.
  • A suite that was removed from the store (deleted, then restored unchanged, for example by switching branches) is served from sbt 2's test cache without running, so it stays undocumented until testFull or until it changes.
  • sbt baklavaClean deletes target/baklava, including the store; rebuild it with testFull on sbt 2 or test on sbt 1. On sbt 2, clean leaves target/baklava in place.

Runtime Configuration​

SwaggerUI and Routes Configuration​

If you're using SwaggerUI (Pekko HTTP or http4s + OpenAPI), you can configure the routes behavior at runtime. Each module exposes its own BaklavaRoutesConfig case class with identical fields — pl.iterators.baklava.http4s.routes.BaklavaRoutesConfig for the http4s module and pl.iterators.baklava.routes.BaklavaRoutesConfig for the Pekko HTTP module. They are not interchangeable types; import the one that matches the module you're wiring up. The Pekko HTTP module additionally accepts a Typesafe com.typesafe.config.Config, since HOCON is the idiomatic config format on that stack.

Configuration options at a glance​

FieldDefaultEnvironment variable
enabledtrueBAKLAVA_ROUTES_ENABLED
basicAuthUserNoneBAKLAVA_ROUTES_BASIC_AUTH_USER
basicAuthPasswordNoneBAKLAVA_ROUTES_BASIC_AUTH_PASSWORD
fileSystemPath./target/baklavaBAKLAVA_ROUTES_FILESYSTEM_PATH
publicPathPrefix/BAKLAVA_ROUTES_PUBLIC_PATH_PREFIX
apiPublicPathPrefix/v1BAKLAVA_ROUTES_API_PUBLIC_PATH_PREFIX

Configuration via case class (both modules)​

Calling BaklavaRoutes.routes() with no arguments uses defaults overridden by the BAKLAVA_ROUTES_* environment variables:

// http4s
import pl.iterators.baklava.http4s.routes.BaklavaRoutes
val docs: HttpRoutes[IO] = BaklavaRoutes.routes()

// Pekko HTTP
import pl.iterators.baklava.routes.BaklavaRoutes
val docs: Route = BaklavaRoutes.routes()

To configure explicitly, build a BaklavaRoutesConfig yourself (works well with Ciris, PureConfig, or anything else that produces a case class):

// http4s
import pl.iterators.baklava.http4s.routes.{BaklavaRoutes, BaklavaRoutesConfig}
val docs: HttpRoutes[IO] = BaklavaRoutes.routes(
BaklavaRoutesConfig(
basicAuthUser = Some("admin"),
basicAuthPassword = Some("secret"),
publicPathPrefix = "/internal/docs"
)
)

// Pekko HTTP — same shape, different package
import pl.iterators.baklava.routes.{BaklavaRoutes, BaklavaRoutesConfig}
val docs: Route = BaklavaRoutes.routes(BaklavaRoutesConfig(enabled = false))

Configuration via application.conf (Pekko HTTP only)​

The Pekko HTTP module also accepts a Typesafe Config. Add a baklava-routes section to src/main/resources/application.conf:

baklava-routes {
# Enable/disable baklava routes (default: true)
# Can be overridden with environment variable: BAKLAVA_ROUTES_ENABLED
enabled = true

# HTTP Basic Auth credentials for protecting SwaggerUI (optional)
# Can be overridden with environment variables: BAKLAVA_ROUTES_BASIC_AUTH_USER, BAKLAVA_ROUTES_BASIC_AUTH_PASSWORD
# basic-auth-user = "admin"
# basic-auth-password = "secret"

# Directory where generated documentation files are stored (default: "./target/baklava")
# Can be overridden with environment variable: BAKLAVA_ROUTES_FILESYSTEM_PATH
filesystem-path = "./target/baklava"

# URL prefix for serving baklava resources (default: "/")
# Can be overridden with environment variable: BAKLAVA_ROUTES_PUBLIC_PATH_PREFIX
public-path-prefix = "/"

# URL prefix for API endpoints in documentation (default: "/v1")
# Can be overridden with environment variable: BAKLAVA_ROUTES_API_PUBLIC_PATH_PREFIX
api-public-path-prefix = "/v1"
}
import com.typesafe.config.ConfigFactory
import pl.iterators.baklava.routes.BaklavaRoutes

val docs: Route = BaklavaRoutes.routes(ConfigFactory.load())

If you're on http4s and want HOCON, build the case class from your Config:

import com.typesafe.config.{Config, ConfigFactory}
import pl.iterators.baklava.http4s.routes.{BaklavaRoutes, BaklavaRoutesConfig}
import scala.util.Try

val c: Config = ConfigFactory.load().getConfig("baklava-routes")
val docs = BaklavaRoutes.routes(
BaklavaRoutesConfig(
enabled = c.getBoolean("enabled"),
basicAuthUser = Try(c.getString("basic-auth-user")).toOption,
basicAuthPassword = Try(c.getString("basic-auth-password")).toOption,
fileSystemPath = c.getString("filesystem-path"),
publicPathPrefix = c.getString("public-path-prefix"),
apiPublicPathPrefix = c.getString("api-public-path-prefix")
)
)

Configuration via Environment Variables​

Either module's no-arg BaklavaRoutes.routes() reads these variables on startup:

# Enable/disable baklava routes
export BAKLAVA_ROUTES_ENABLED=true

# Set basic auth credentials (optional)
export BAKLAVA_ROUTES_BASIC_AUTH_USER=admin
export BAKLAVA_ROUTES_BASIC_AUTH_PASSWORD=secret

# Set custom filesystem path
export BAKLAVA_ROUTES_FILESYSTEM_PATH=/custom/path/to/docs

# Set custom URL prefixes
export BAKLAVA_ROUTES_PUBLIC_PATH_PREFIX=/docs
export BAKLAVA_ROUTES_API_PUBLIC_PATH_PREFIX=/api/v2

Common Configuration Examples​

Example 1: Development setup with custom paths

BaklavaRoutesConfig(
enabled = true,
fileSystemPath = "./docs/generated",
publicPathPrefix = "/api-docs",
apiPublicPathPrefix = "/api/v1"
)

Example 2: Production setup with basic auth

BaklavaRoutesConfig(
enabled = true,
basicAuthUser = Some("api-docs"),
basicAuthPassword = Some("secure-password"),
publicPathPrefix = "/internal/docs"
)

Example 3: Disabled in production (using environment variable)

export BAKLAVA_ROUTES_ENABLED=false