---
title: "API-First Development in Scala"
description: "Learn how to adopt API-first practices in Scala using modern tooling like OpenApi4s."
canonicalUrl: "https://zuplo.com/blog/2025/04/11/api-first-development-in-scala"
pageType: "blog"
date: "2025-04-11"
authors: "sakib"
tags: "Scala, API Tooling, Tutorial"
image: "https://cdn.zuplo.com/cdn-cgi/image/fit=crop,width=1200,height=630/www/media/posts/2025-04-11-api-first-development-in-scala/image.png"
---
> This article is written by [Sakib Hadžiavdić](https://sake.ba/), owner of
> [SaCode](https://sacode.dev/), and creator of several Scala libraries
> including [FlowRun](https://flowrun.io/),
> [hepek](https://github.com/sake92/hepek), and recently,
> [openapi4s](https://github.com/sake92/openapi4s). All opinions expressed are
> his own.

There are generally two ways to build APIs that have a specification:

- the code-first approach and
- the API-first approach (schema-first)

A specification is useful when:

- there are many consumers of your API
- you need to generate clients in many languages
- you need to generate some API mocks for testing
- you need documentation for consumers of the API
- you need to maintain backwards compatibility of the API
- etc

## Code-first Approach

In the "code-first" approach you start with the coding right away. Schedules are
tight, the pressure is real.

The specification (if any) is mostly an afterthought. Consider annotations in
Spring framework, you can always sprinkle some `@ApiParam` here and there in
your controllers. And voila, you got yourself an OpenApi spec generated from
code!

There are also approaches like
[Tapir](https://tapir.softwaremill.com/en/latest/), where you use a Scala DSL to
define your API. Then you implement the server logic, and generate the spec from
that Scala code. Potentially the client(s) too.

**Benefits**:

- you start with coding right away
- you can generate many different artifacts: server, clients, documentation etc.
- it is very popular, so you can find lots of examples on the Internet

**Downsides**:

- you kinda play russian roulette with the spec output, since it is _generated
  from the code_, you can't really call it stable
- it is more code to maintain, increasing your _cognitive load_ and _compile
  times_ (especially in Scala due to many implicits and macros)
- bundle size is increased, since you have to include the dependencies
- sometimes you have to fight the DSL to get what you want (e.g polymorphic
  payloads and such), and most of the time you repeat 99% of the same code as
  for JSON codecs for example
- you are _limited by what the DSL provides_, if you need some
  framework-specific feature, it's challenging to get to it (or impossible). In
  this sense code-first approach feels like using an ORM, and API-first like
  using SQL directly.

## API-first Approach

In the "API-first" approach you start with the API specification. You define the
API usually as an OpenAPI (Swagger) spec in a YAML or JSON format. Then you
generate the server from that spec (and client code etc).

This approach has some advantages:

- you are in _full control of the spec_, it is _the source of truth_
- spec is _stable_ and _versioned_
- faster collaboration between frontend and backend:
  - you can quickly update the spec
  - frontend devs can make an openapi example of what they need
- less code to maintain, there is no DSLs or annotations that obfuscate your
  code
- you can leverage your framework's features to the max

Disadvantages:

- you need to write the spec, it is lots of boilerplate (there are some UI tools
  and IDE plugins that help)
- the spec file can get huge, but OpenApi supports splitting it into multiple
  files

Several tools exist to facilitate the API-first approach in Scala, such as:

- various [openapi-generator](https://github.com/OpenAPITools/openapi-generator)
  plugins
- [Guardrail](https://guardrail.dev) from Twilio
- [OpenApi4s](https://github.com/sake92/openapi4s)

### OpenApi Generator Plugins

This project supports generating many different server frameworks: Akka, Finch,
Lagom, Play, Cask, Scalatra.  
The main painpoint is that it is one-shot, meaning if you introduce any changes,
the generator would overwrite your code.

This is a good starting point for some smaller projects.

### Guardrail

[Guardrail](https://guardrail.dev) generates server boilerplate code to the
`target/.../src_managed/main` folder (you don't commit this to git). Then you
implement the server logic by _overriding those abstract definitions_, fill in
the blanks essentially. Note that when you change the `openapi.json`, the
`src_managed` code is overwritten.

This approach is good, the main problem I see is that the generated code is
hidden from your eyes and git. Also, it doesn't support Scala 3 yet.

### OpenApi4s

A new kid on the block is my [OpenApi4s](https://github.com/sake92/openapi4s)
tool. It takes a bit different approach than Guardrail.  
OpenApi4s _doesn't hide the code from you_, it generates it **directly in your
`src` folder** (that you commit to git).  
Yes, you read that right. Exactly like you would have written it by hand.  
So how does it handle changes in the spec?  
You might try to guess:

- overwrites the code every time? that would be pointless
- some AI tool doing it behind the scenes? hmm

Answer is: no, and no, so let's see how it actually works...

---

TLDR: [OpenApi4s](https://github.com/sake92/openapi4s) refactors your code
automatically, by using [regenesca](https://github.com/sake92/regenesca)
diff+merge library.  
Regenesca uses [Scalameta](https://scalameta.org/) to parse the Scala code and
do the diffing and patching with precision.

Generating the models and controllers is the easy part:

1. parse the OpenAPI spec
2. generate models
3. generate controllers

and that's it.

---

The hard part is _how to handle changes in the spec_.

Consider the classical [PetStore spec](https://petstore3.swagger.io).

> The following examples use the
> [Sharaf framework](https://sake92.github.io/sharaf/).

OpenApi4s will generate this for the `User` model:

```scala
case class User(
    id: Option[Long],
    username: Option[String],
    firstName: Option[String],
    lastName: Option[String],
    email: Option[String],
    password: Option[String],
    phone: Option[String],
    userStatus: Option[Int]
) derives JsonRW
```

#### Adding a New Model Property

Let's say you add a new property to the `User` model: `age` with type `integer`.
OpenApi4s will compare the newly generated `case class User(..)` (in-memory),
with the existing `case class User(..)` in your existing source code. Then it
will figure out that `age: Int` parameter is missing, and it will add it.

Here is the git diff you would see:

```diff
--- a/api/src/com/example/petstore/api/models/User.scala
+++ b/api/src/com/example/petstore/api/models/User.scala
@@ -14,5 +14,5 @@ case class User(
    email: Option[String],
    password: Option[String],
    phone: Option[String],
-  userStatus: Option[Int]
+  userStatus: Option[Int], age: Long
    ) derives JsonRW
```

#### Changing a Model Property

Let's say you change the `userStatus`'s format from `int32` to `int64`.
OpenApi4s will figure out that `userStatus: Option[Int]` needs to be changed to
`userStatus: Option[Long]`.

```diff
--- a/api/src/com/example/petstore/api/models/User.scala
+++ b/api/src/com/example/petstore/api/models/User.scala
@@ -14,5 +14,6 @@ case class User(
    email: Option[String],
    password: Option[String],
    phone: Option[String],
-  userStatus: Option[Int]
+  userStatus: Option[Long]
```

#### Adding a New Endpoint

Adding a new endpoint to existing controller is easy too. It will just add
another `case` to your existing routes:

```scala
case GET -> Path("user", "new-endpoint") =>
  Response.withStatus(StatusCodes.NOT_IMPLEMENTED)
```

It will even generate a boilerplate implementation for you. You can then fill in
the blanks.

#### Changing the Route Implementation

What happens when you change the body of a route? Now this is a bit more tricky.
OpenApi4s must not touch your existing code, since you might have already
_implemented some logic_. So it will not touch your existing expressions, for
example `Response.withStatus(..)` in the previous example.

But it will update the query parameters for example (if needed). For example if
you add query params to an existing endpoint it would add this snippet:

```scala
case class QP(country: String) derives QueryStringRW
val qp = Request.current.queryParamsValidated[QP]
```

#### Removing a Model or Endpoint

When you remove a model or endpoint from the OpenApi spec.. OpenApi4s will do ..
nothing!  
This is because sometimes you have endpoints that _should not be exposed in the
openapi spec_ (e.g. payment webhooks and similar).  
That's why OpenApi4s does not remove any code.

You are responsible to remove it, if you really want to do that. This is a
limitation, or a feature, depends on the perspective.

## CI

### Preventing Accidental Overwrites

You might be cautious about the changes that OpenApi4s makes. Thinking, will it
overwrite my code? How can I make sure it doesn't? Here is a useful check you
can do in your CI pipeline:

- change the `openapi.json` file, just so that
  [`mill` build tool](https://mill-build.org/mill/index.html) detects a change
  and triggers OpenApi4s
- compile the code, to regenerate the files
- see if there are any changes in the git diff

Example:

```sh
echo " " >> api/resources/openapi.json      # append a space at end of openapi.json
./mill api.compile                          # trigger OpenApi4s
truncate -s -1 api/resources/openapi.json   # remove the space at end of openapi.json
git diff --exit-code                        # check if there are any accidental diffs
[ $$? -eq 0 ]  || exit 1                    # abort if there are
```

### Preventing Breaking Changes

Since the `openapi.json` is now in `git`, you can do some cool stuff with it.
One very useful check is preventing breaking changes. You can do it with
[openapi-diff](https://github.com/OpenAPITools/openapi-diff) for example:

```sh
# copy the openapi.json from main branch to a temp file
git show origin/main:api/resources/public/openapi.json > main_openapi.json

# compare them and exit with error if there are breaking changes
cs launch org.openapitools.openapidiff:openapi-diff-cli:2.1.0-beta.12 -M org.openapitools.openapidiff.cli.Main -- --fail-on-incompatible main_openapi.json ./api/resources/public/openapi.json
[ $$? -eq 0 ]  || exit 1
```

This example is using [Coursier](https://get-coursier.io/) for launching a JVM
app.  
But you could use any other CLI tool you find useful.

See the
[CI script](https://github.com/sake92/openapi4s-demo/blob/main/.github/workflows/ci.yml)
in the openapi4s demo repo.

Here is an example of
[compatible change PR](https://github.com/sake92/openapi4s-demo/pull/2).  
And an example of
[breaking change PR](https://github.com/sake92/openapi4s-demo/pull/3), the CI
build fails of course.

## Conclusion

OpenApi4s uses a different approach to generating code.  
All new ideas feel strange when you hear them.  
But in the era of AI tools that almost "randomly" refactor your code and
hallucinate, using OpenApi4s feels much more robust, straightforward and
predictable.

Hope you find this post (and the OpenApi4s tool) useful.  
You can find more tools to combine with the API-first approach at
[https://openapi.tools/](https://openapi.tools/)

Check out the OpenApi4s
[demo repository](https://github.com/sake92/openapi4s-demo) and try it out in
your next Scala API project!  
There is also a video demo on
[YouTube](https://www.youtube.com/watch?v=kf0vGrlKNb8).

## Additional Resources

- [Understanding the API-First Approach](https://swagger.io/resources/articles/adopting-an-api-first-approach/)
- [Guardrail](https://guardrail.dev)
- [Tapir](https://tapir.softwaremill.com/en/latest/)
- [Documenting a Spring REST API Using OpenAPI 3.0](https://www.baeldung.com/spring-rest-openapi-documentation)

Sakib Hadžiavdić, 11. April 2025, Sarajevo