Documenting a Scala API shouldn't feel like archaeology. Yet nearly a quarter of developers still reverse-engineer code because the docs aren't there when they need them, according to recent maintenance surveys. This reality check reveals the gap between shipping code and shipping usable documentation.
The solution isn't more tools. It's the right workflow. Generate HTML docs with
a single sbt doc
or scala-cli doc .
, automate the process in GitHub Actions
so docs never drift, choose between Scaladoc, Guardrail, OpenAPI Generator, and
OpenApi4s based on your team's needs, and apply a practical checklist that keeps
everything clear and current. Copy the workflow, ship faster, and get back to
building.
- One-Command Documentation with Scaladoc
- Automating Documentation in CI/CD Pipelines
- Choosing the Right Tool: Scaladoc vs Guardrail vs OpenAPI Generator vs OpenApi4s
- Scaladoc
- OpenAPI Generator
- Bridging Code-First and API-First Workflows
- Best-Practice Checklist for Rock-Solid Scala API Docs
- Troubleshooting & Common Pitfalls
- Why Most Scala Teams Are Moving Beyond Traditional Documentation
- Publishing Docs & Making Your Scala APIs AI-Ready with Zuplo
One-Command Documentation with Scaladoc#
If generating docs takes longer than compiling code, something's off. With modern tooling you can ship glossy HTML docs from any Scala project in one command.
To follow along you need JDK 11+ and either sbt 1.6+ or scala-cli 0.1+. Clone any project, open a terminal, and run:
# sbt users
sbt doc
# scala-cli users
scala-cli doc .
That's it. The build spits out target/scala-*/api/index.html
; open the file in
your browser and you have navigable, cross-linked docs.
A tiny build.sbt
is enough to polish the result:
scalacOptions ++= Seq(
"-doc-title", "Payment API",
"-doc-version", "1.0.0",
"-author"
)
Those flags are used to control the title bar, version badge, and author footer
in Scaladoc-generated documentation, but they are documented in the Scala
compiler or Scaladoc generation guides, not in the official Scaladoc style
guide. Add "-groups"
to sort members by visibility, or "-doc-external-doc"
to link out to external libraries.
Your comments drive the output, so write them like you write code: clear, terse, example-driven.
/** Calculates VAT for a given amount.
*
* @param amount gross price in cents
* @return VAT value in cents
*/
def vat(amount: Long): Long = (amount * 20) / 100
The Scaladoc team keeps a sample repo with every feature turned on; clone it,
run sbt doc
, and explore the generated site.
Automating Documentation in CI/CD Pipelines#
Manual doc generation breaks the moment a deadline hits. Automating it fixes the drift that forces developers to reverse-engineer code later—a pain highlighted in recent maintenance surveys.
A healthy pipeline looks like this: push → test → generate docs → publish. Copy-paste the GitHub Actions file below and you're 90% there.
name: docs
on:
push:
branches: [main]
jobs:
build-docs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Java
uses: actions/setup-java@v3
with:
distribution: temurin
java-version: 17
- name: Cache sbt
uses: actions/cache@v3
with:
path: ~/.ivy2/cache
key: ${{ runner.os }}-ivy-${{ hashFiles('**/build.sbt') }}
- name: Test and document
run: |
sbt test
sbt doc
- name: Deploy to GitHub Pages
if: github.ref == 'refs/heads/main'
run: ./scripts/publish-docs.sh
Swap the deploy step for a GitLab Pages job or a Jenkins post-build if that's your world—the commands stay identical.
Keep pipelines fast and cheap by running build and doc jobs in parallel; matrix
them for Scala 2.13 and 3.x. Cache .ivy2
and .coursier
directories to skip
dependency downloads. Zip the api/
directory before uploading it as an
artifact since HTML compresses well. Store deploy keys and tokens in the
platform's secret manager, never in source control.
Treat docs like code. Commit the generated site only on release branches, keep the source-of-truth in Scaladoc comments, and let Git diffs show what changed. Tools like validation scripts from workflow automation guides can fail the build if docs don't match the API signature. Automation turns stale docs into an impossibility.
Choosing the Right Tool: Scaladoc vs Guardrail vs OpenAPI Generator vs OpenApi4s#
Scaladoc isn't the only game in town. Your choice depends on whether you start with code or with an OpenAPI file, how much boilerplate you tolerate, and how your team works.
Scaladoc#
Scaladoc ships with the compiler; no extra
dependencies, no config. You sprinkle triple-star comments, run sbt doc
, and
share the HTML. Perfect for internal libraries or services that don't expose a
public REST surface.
/** Returns the exchange rate USD -> EUR at market close. */
def fxRate(): BigDecimal = ???
The downside: it documents Scala symbols, not HTTP endpoints. If you need Swagger UI or client SDKs, you'll look elsewhere.
Guardrail#
Guardrail flips the workflow: feed
it an openapi.yaml
and it spits out type-safe server stubs and clients.
addSbtPlugin("com.twilio" % "sbt-guardrail" % "0.74.0")
guardrailTasks in Compile ++= Seq(
ScalaServer(file("specs/petstore.yaml")),
ScalaClient(file("specs/petstore.yaml"))
)
No manual mapping between spec and code, generated models use Cats Effect and http4s out of the box, and your clients stay in lock-step with the contract. The spec is the truth; if you tweak code without updating YAML, drift creeps in. Best for green-field, spec-first teams.
OpenAPI Generator#
OpenAPI Generator is language-agnostic. Run one command and generate Scala, Go, TypeScript, whatever:
openapi-generator-cli generate \
-i petstore.yaml \
-g scala-akka-http-server \
-o /tmp/server
It supports Akka HTTP, http4s, Play, endpoints4s, and more frameworks than most teams will ever use. The flip side is template churn—minor version bumps can reshape generated code, so pin generator versions in CI.
Need to scaffold a Scala client for the Slack API? OpenAPI Generator ships a ready-made template that saves hours of boilerplate.
OpenApi4s#
OpenApi4s sits between code-first and spec-first. Write type-safe endpoint descriptions in Scala, then emit an OpenAPI file or generate routes back into code. Because the library holds both views, accidental overwrites are impossible.
val hello = endpoint.get
.in("hello" / path[String]("name"))
.out(jsonBody[Greeting])
.description("Greets the caller")
Tightly coupled to http4s and functional programming, it feels natural if you already use Cats Effect; less so if you live in Play Framework land.
Tool | Ownership Model | Learning Curve | Team Size Sweet Spot | API Change Frequency |
---|---|---|---|---|
Scaladoc | Code-first | Near zero | Any | Low-medium |
Guardrail | Spec-first | Moderate | 3-10 devs | Medium-high |
OpenAPI Generator | Spec-first | Moderate-high | Polyglot teams | Medium |
OpenApi4s | Bidirectional | Low-moderate | FP/http4s teams | High |
Here's what actually works for most Scala teams: start with Scaladoc for internal documentation, then add Guardrail or OpenApi4s when you need client SDKs or API contracts. OpenAPI Generator works well for polyglot teams, but the template maintenance overhead often outweighs its flexibility.
The teams shipping fastest pick one tool and stick with it rather than mixing approaches. Tool proliferation creates more maintenance burden than feature benefits. Choose based on your primary workflow—code-first or spec-first—not on edge case requirements.
Pick the tool that matches your workflow, not the one with the most stars on GitHub.
Bridging Code-First and API-First Workflows#
Most real projects straddle both worlds: legacy controllers built before Swagger was cool and shiny endpoints defined in YAML. You can merge them without a rewrite.
Generate the combined openapi.yaml
—your canonical
API definition—commit
it, and wire a CI check that diff-fails if the spec and code
diverge—automation workflows
show the pattern.
For teams migrating gradually, expose both endpoints side by side. Tag the old
ones as deprecated
in Scaladoc and in the OpenAPI file; consumers get a clear
nudge while you keep the lights on.
Scala's rich type system helps here: wrap legacy JSON payloads in new case classes, document them once, and reuse them across both routes. You avoid copy-paste docs and reduce the "accidental complexity" the community calls out in API-first development discussions.
When the last legacy endpoint is dead, delete the old tags, remove the shims, and your doc pipeline doesn't notice—it's already running on every commit. The result: one source of truth, zero reverse-engineering sessions, and happier devs reading accurate docs instead of outdated wiki pages.
Best-Practice Checklist for Rock-Solid Scala API Docs#
A structured approach prevents the documentation drift that forces developers to reverse-engineer APIs. This eight-point audit fits right into your workflow and ensures docs stay current.
Start with the Why#
Your first sentence should tell readers what the API does and why they should
care. If someone needs to read it twice, rewrite it. Use Scaladoc's inline
linking syntax like [[ClassName]]
or [[com.example.User]]
so refactors don't
break navigation.
Document Every Parameter and Return Type#
Use @param
, @tparam
, and @return
tags to keep signatures self-explanatory.
But don't stop at signatures—show concrete examples with actual JSON payloads
and HTTP codes:
// 201 Created
{ "id": 42, "name": "Ada" }
Version Decisively#
Use semantic versioning in both code and docs. Tag major releases and keep multiple doc versions live so clients aren't forced to upgrade blindly.
Make Builds Fail on Bad Docs#
Add a CI step that runs sbt doc
and checks git diff --exit-code docs/
. If
the docs drift, the merge fails—no excuses.
Ensure Accessibility#
Generated HTML needs basic a11y support: alt text for images, keyboard-navigable tables, color-safe palettes.
Treat Docs as Code#
Keep Scaladoc comments, OpenAPI YAML, and Markdown guides in the same repo. Review them in pull requests like any other change.
Here's why these rules matter:
// ❌ Poor
def get(id: Int) = ???
// ✅ Good: fetch a single user by id.
/**
* @param id Unique user identifier.
* @return Some(User) when found, None otherwise.
* {{{
* GET /users/42 → 200 OK
* }}}
*/
def getUser(id: Int): Option[User] = ???
The second snippet auto-generates rich HTML via Scaladoc, creates live links to
User
, and survives refactors—exactly what the Scaladoc style guide recommends.
Combine that with CI checks and spec-first docs from earlier tools, and you'll
never land in that bucket of poor documentation complaints. Your future
teammates will thank you.
Troubleshooting & Common Pitfalls#
Doc builds fail at the worst times. Here are the five issues that show up most often and how to fix them.
CI failures usually come from JDK and Scala version mismatches. If your
build.sbt
targets Scala 3.4.0, use a matching LTS JDK (11 or 17). In GitHub
Actions:
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 17
Pair that with scalaVersion := "3.4.0"
and your pipeline stops failing on
byte-code errors before the docs task runs.
Broken links kill documentation credibility. Scaladoc's link checker ships with
Scala 3. Add scalacOptions += "-Wconf:cat=doc:warning"
and the compiler flags
every unresolved [[link]]
during compile, catching problems early.
Large HTML bundles slow cloning and waste CI minutes. Split them per module with
aggregate in doc := false
. Each sub-project produces its own site, then copy
the pieces into gh-pages
. Static hosts cache aggressively, so users never
notice the difference.
Over-sized bearer tokens can bloat request headers; some browsers respond with HTTP error 431 before your API code even runs.
API specification drift breaks trust with your users. Run an OpenAPI diff in CI:
openapi-diff old.yaml new.yaml --fail-on-changed
If the command exits non-zero, block the merge until docs catch up. This approach prevents embarrassing mismatches between code and documentation.
Windows encoding issues still hit mixed OS teams. Set
scalacOptions += "-encoding UTF-8"
and standardize Git line endings with
git config --global core.autocrlf input
to greatly reduce issues with "weird
characters." For full consistency, consider additional safeguards like a
.gitattributes
file and consistent editor settings across your team.
Set these up as guardrails, not emergency fixes. When your pipeline enforces versions, checks links, diff-tests specs, and standardizes encoding, documentation ships reliably with every commit.
Why Most Scala Teams Are Moving Beyond Traditional Documentation#
Here's the uncomfortable truth about API documentation in 2025: maintaining separate documentation infrastructure is becoming a competitive disadvantage.
While you're updating Scaladoc comments, fixing broken CI builds, and managing static site hosting, teams using integrated API platforms are shipping features faster. They've eliminated the entire category of "documentation debt" by making docs a byproduct of their API gateway configuration.
Traditional documentation workflows have fundamental problems:
- Manual sync overhead: Every API change requires updating multiple places—code comments, OpenAPI specs, deployment docs
- CI complexity: Documentation builds add failure points to your deployment pipeline
- Discovery friction: Static documentation sites require developers to find them, bookmark them, and remember to check them
- AI blindness: Generated HTML doesn't provide the programmatic interfaces AI agents need for integration
The teams shipping the fastest Scala APIs in 2025 have moved to platforms that eliminate this maintenance burden entirely.
Publishing Docs & Making Your Scala APIs AI-Ready with Zuplo#
Your CI pipeline generates HTML and OpenAPI files—but nobody can find them, clients hit unexpected rate limits, and you're debugging static site auth at midnight. You're not alone: developers still reverse-engineer poorly documented components, draining hours from real coding.
Modern API platforms eliminate this entire category of problems. Instead of maintaining separate documentation infrastructure, your API gateway auto-generates everything and keeps it instantly current.
How Zuplo Transforms Your Documentation Workflow:
- Import & Deploy in Seconds. Import your OpenAPI spec → Zuplo builds routes, docs, and policies automatically → Push to Git → Deploy globally to 300+ edge locations in under 20 seconds
- Zero-Maintenance Documentation. When you deploy new Scala code, documentation updates automatically. No CI builds, no static deployments, no manual sync steps. API changes propagate globally in under 20 seconds.
- Built-in API Management. Add OAuth, rate limiting, and error handling with JavaScript policies. Consumers get explicit 429 error codes instead of mysterious failures while your Scala service stays clean.
- AI-Ready by Default. Always-current, machine-readable contracts make your APIs instantly compatible with code generators, SDK helpers, and AI agents. Built-in analytics provide the data AI models need without extra logging code.
Compare this to traditional workflows:
Traditional | Modern |
---|---|
Write Scaladoc → Generate HTML → Deploy to static host → Hope developers find it → Manually sync when APIs change | Configure gateway policies → Auto-generated docs deploy globally → Developers get interactive portals → Documentation updates with every code deploy |
While competitors debug documentation CI failures, you're shipping features. While they manually update API specs, yours stay perfectly synchronized. While they struggle with AI integration, your APIs are already AI-ready.
Ready to eliminate documentation maintenance entirely? Start building with Zuplo and watch your reverse-engineering sessions disappear.