API Code-first is the art of building an API, and then popping some annotations or metadata in there to output API documentation in an API description format like OpenAPI.
The most popular API Code-first approach in Spring uses a tool called Springdoc, which can generate OpenAPI v3.1 from any Spring-based application (Boot, Web MVC, Webflux, etc). A basic skeleton will be generated with absolutely no work whatsoever, then adding annotations will keep improving the usefulness of the generated OpenAPI.
Prerequisites
To follow this guide, you will need:
- Java 21 or later
- Maven 3.5+ or Gradle 7.5+
- A Spring Boot application, which you can create by following the Spring Boot REST Service guide or cloning down our sample code.
Here’s a few commands to help you out.
# check for java. v21 or above is fine
java --version
# check if maven or gradle are there
which mvn || which gradle
If you are new to working with Java applications it’s recommended to use an IDE like IntelliJ IDEA or VS Code (using this great guide to running Spring in VS Code). You can use the command line if you prefer.
Creating OpenAPI with Springdoc
Step 1: Install the Springdoc dependency
If you are using Maven, add the following dependency to your pom.xml
file:
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-api</artifactId>
<version>2.8.8</version>
</dependency>
If you are using Gradle, add this to your build.gradle
file:
dependencies {
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-api:2.8.8'
}
The springdoc-openapi-starter-webmvc-api
package supports Spring MVC and
Spring Boot, but for Spring Webflux support use the
springdoc-openapi-starter-webflux-api
package instead.
Step 2: Configure Springdoc
You can customize the OpenAPI generation by adding properties to your
application.properties
or application.yml
file.
# application.properties
springdoc.api-docs.path=/openapi
Or if you are using YAML:
# application.yml
springdoc:
api-docs:
path: /openapi
title: My API
version: 1.0.0
The path setting defines where the OpenAPI description will be available.
Springdoc makes it available on http://localhost:8080/v3/api-docs
by default,
with the v3
being a reference to OpenAPI v3.x, but you can change it to
something more suitable for your application.
Why not a classy /openapi
path so the OpenAPI description can be hosted on a
nice clean http://localhost:8080/openapi
once the application is running.
Step 3: Create/find some REST controllers
This step is less of a step, and more “have some controllers and models” which
you probably already do. Whether you are starting from scratch or looking at an
existing application, let’s use the example of a EmployeeController
, using
Hibernate and Spring Data JPA to make life a little easier.
# src/main/java/com/bumpsh/demo/EmployeeController.java
import java.util.List;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
class EmployeeController {
private final EmployeeRepository repository;
EmployeeController(EmployeeRepository repository) {
this.repository = repository;
}
@GetMapping("/employees")
List<Employee> all() {
return repository.findAll();
}
@PostMapping("/employees")
Employee newEmployee(@RequestBody Employee newEmployee) {
return repository.save(newEmployee);
}
@GetMapping("/employees/{id}")
Employee one(@PathVariable Long id) {
return repository.findById(id)
.orElseThrow(() -> new EmployeeNotFoundException(id));
}
// snipped PUT and DELETE for now
}
The repository is wrapping around the model to handle some tedious database interaction, and the model looks like this:
# src/main/java/com/bumpsh/demo/Employee.java
import java.util.Objects;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
@Entity
class Employee {
private @Id
@GeneratedValue Long id;
private String name;
private String role;
// snip constructor, getters and setters
}
So far we’ve done absolutely nothing to this application code specific to Springdoc or OpenAPI, but this is already enough for Springdoc to generate a basic OpenAPI description.
Step 4: Run the application
Boot up the Spring Boot application to install all the dependencies and start the HTTP server.
If you are using an IDE, you can run the application directly from there. For
example, in IntelliJ IDEA, you can right-click on the main class (the one
annotated with @SpringBootApplication
) and select “Run”.
If you are using Maven, run:
mvn spring-boot:run
If you are using Gradle, run:
./gradlew bootRun
Once the application is running and the HTTP server is available, the OpenAPI description will be available at http://localhost:8080/openapi for JSON, or http://localhost:8080/openapi.yaml for YAML.
Very basic, and it’s only saying “what” which misses out on the critical “why” and “how” aspect of API documentation, but it will be a start.
Step 5: Deploy to Bump.sh
Before we get stuck into making this OpenAPI perfect, lets get used to deploying it to Bump.sh so we can see how things look. Iterative improvements are easy with Bump.sh, as it can be hooked up to deploy a new version of the documentation every time a pull request is merged in.
If you’re new to Bump.sh, create your first API. Then retrieve the name and token of this documentation from the CI deployment settings page. We’ll use this to deploy the OpenAPI description with the Bump.sh CLI.
npm install -g bump-cli
Now we can deploy the OpenAPI description to Bump.sh using the bump deploy
command.
$ bump deploy http://localhost:8080/openapi \
--doc spring-code-first \
--token my-documentation-token
* Your new documentation version will soon be ready at https://bump.sh/bump-examples/hub/code-samples/doc/spring-code-first
This can then be automated in your CI/CD pipeline, so every time you merge a pull request, the OpenAPI description is automatically deployed to Bump.sh. We’ll show you how to do that in the end, but for now, let’s focus on improving the OpenAPI description.
Step 6: Improve OpenAPI with Java annotations
The skeleton OpenAPI description generated by Springdoc is a start, but it lacks context and explanations. To make it more useful, we can add annotations to the controllers and models providing more context to how and why this API works the way it does.
This is done with annotations from the io.swagger.v3.oas.annotations
package,
which allows you to add metadata around the Java source code. The Spring
framework is already making clear which HTTP methods and URL paths are being
used, so the job of these new annotations is to focus on the rest of it:
parameters, request body, responses, status codes, and adding useful
descriptions and examples wherever possible.
Let’s look at some examples of how to do this.
Describing Operation
Adding a summary and description to the operations in the controller is an important first move, because the summary is used to generate the navigation menu, and the description is used to explain what the operation does in more detail.
# src/main/java/com/bumpsh/demo/EmployeeController.java
import io.swagger.v3.oas.annotations.Operation;
@RestController
class EmployeeController {
// Add the @Operation annotation to the method
@GetMapping("/employees/{id}")
@Operation(summary = "Get an employee by ID", description = "Returns the details of an employee based on the provided ID")
List<Employee> all() {
return repository.findAll();
}
// Do this for each HTTP method in the controller
@PostMapping("/employees")
@Operation(summary = "Create a new employee", description = "Adds a new employee to the system")
Employee newEmployee(@RequestBody Employee newEmployee) {
return repository.save(newEmployee);
}
}
Just adding summary and description to the operation is a solid first step to making your OpenAPI documentation more useful and interesting to look at. See the OpenAPI YAML description by Springdoc after adding these annotations.
openapi: 3.1.0
info:
title: OpenAPI definition
version: v0
servers:
- url: http://localhost:8080
description: Generated server url
paths:
/employees/{id}:
get:
tags:
- Employee Management
summary: Get an employee by ID
description: Returns the details of an employee based on the provided ID
operationId: one
parameters:
- name: id
in: path
required: true
schema:
type: integer
format: int64
responses:
"200":
description: OK
content:
'*/*':
schema:
$ref: "#/components/schemas/Employee"
Of course these example operations are very basic, but this is a good place to get stuck into letting users know what to expect when they call your API. Is the response paginated? Is there a default sort order?
Tagging Controllers
# src/main/java/com/bumpsh/demo/EmployeeController.java
import io.swagger.v3.oas.annotations.tags.Tag;
// Add the @Tag annotation to the class
@RestController
@Tag(name = "Employee Management", description = "Operations related to employee management")
class EmployeeController {
// ... existing code ...
}
Adding Parameters
Parameters can be added to the method using the @Parameter
annotation. This is
useful for describing path parameters, query parameters, and request body
parameters.
These parameter annotations are applied to the method parameters, like this:
# src/main/java/com/bumpsh/demo/EmployeeController.java
import io.swagger.v3.oas.annotations.Parameter;
@RestController
class EmployeeController {
@GetMapping("/employees/{id}")
@Operation(summary = "Find an employee", description = "Returns the details of an employee based on the provided ID")
public Employee one(
@Parameter(in = ParameterIn.PATH, description = "ID of the employee to retrieve", required = true, schema = @Schema(type = "integer"))
@PathVariable Long id) {
// ... existing code ...
}
}
This will add a parameter to the OpenAPI description for the id
path variable,
which looks like this in the generated OpenAPI:
paths:
'/employees/{id}':
get:
parameters:
- name: id
in: path
required: true
description: ID of the employee to retrieve
schema:
type: integer
format: int64
Parameter descriptions can contain all sorts of handy information, like letting people know that a date parameter is in ISO 8601 format, or that a string parameter is a UUID. This is useful for consumers of the API to understand how to use the API correctly without having to look at the code or ask questions.
Adding Responses
How about adding a response schema to the @ApiResponse
annotation to describe
what the response body will look like? By default, Springdoc will use the return
type of the method to generate the response schema, but you can override this by
using the @ApiResponse
annotation.
# src/main/java/com/bumpsh/demo/EmployeeController.java
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
@RestController
class EmployeeController {
@GetMapping("/employees/{id}")
@Operation(summary = "Find an employee", description = "Returns the details of an employee based on the provided ID")
@ApiResponse(
responseCode = "200",
description = "Successful operation",
content = @Content(mediaType = "application/json",
schema = @Schema(implementation = Employee.class))
)
public Employee one(@PathVariable Long id) {
// ... existing code ...
}
Annotating the Model
To make the OpenAPI description even more useful, you can annotate the model
class to describe its properties. This is done using the @Schema
annotation.
# src/main/java/com/bumpsh/demo/Employee.java
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import io.swagger.v3.oas.annotations.media.Schema;
@Entity
@Schema(description = "Employees are individuals who work for an organization, " +
"contributing their skills and expertise to achieve the organization's goals." +
"Roles are the specific functions or positions that employees occupy within the organization, " +
"defining their duties, responsibilities, and the scope of their work."
)
class Employee {
private @Id
@Schema(accessMode = Schema.AccessMode.READ_ONLY, description = "Unique identifier which should not be shared publicly", example = "1234")
@GeneratedValue Long id;
@Schema(description = "Employee name", example = "Bilbo Baggins")
private String name;
@Schema(description = "Employee role", example = "Software Engineer")
private String role;
Employee() {}
Employee(String name, String role) {
this.name = name;
this.role = role;
}
// Getters and Setters
}
This will add a description to the Employee
model, and also describe each
property with its type, description, and example value. The generated OpenAPI
will look like this:
components:
schemas:
Employee:
type: object
description: "Employees are individuals who work for an organization, contributing\
\ their skills and expertise to achieve the organization's goals.Roles are\
\ the specific functions or positions that employees occupy within the organization,\
\ defining their duties, responsibilities, and the scope of their work."
properties:
id:
type: integer
format: int64
description: Unique identifier which should not be shared publicly
example: 1234
readOnly: true
name:
type: string
description: Employee name
example: Bilbo Baggins
role:
type: string
description: Employee role
example: Software Engineer
This will make the OpenAPI description much more useful, as it provides a clear understanding of the model and its properties, including their types, descriptions, and example values.
Over time as properties change, the OpenAPI description will also change, allowing consumers of the API to always have an up-to-date understanding of the API and its models.
Step 7: Deploy the improved OpenAPI description
Now that we’ve added some annotations to the controllers and models, we can generate a new OpenAPI description and deploy it to Bump.sh.
Run the application again to generate the new OpenAPI description, and then
deploy it to Bump.sh using the bump deploy
command.
$ bump deploy http://localhost:8080/openapi \
--doc spring-code-first \
--token my-documentation-token
* Your new documentation version will soon be ready at https://bump.sh/bump-examples/hub/code-samples/doc/spring-code-first
This will update the OpenAPI description on Bump.sh with the new version, which will now include the additional context and explanations we added with the annotations.
Step 8: Automate the deployment
To automate the deployment of the OpenAPI description to Bump.sh, you can add a
step in your CI/CD pipeline to run the bump deploy
command every time a pull
request is merged. This way, the OpenAPI description will always be up-to-date
and available on Bump.sh.
You can use a CI/CD tool like GitHub Actions, GitLab CI, or Jenkins to run the deployment command automatically.
For example, if you are using GitHub Actions, you can create a workflow file
.github/workflows/bump.yml
with the following content:
name: Check & deploy API documentation
permissions:
contents: read
pull-requests: write
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
deploy-doc:
if: ${{ github.event_name == 'push' }}
name: Deploy API documentation on Bump.sh
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up JDK
uses: actions/setup-java@v2
with:
distribution: 'temurin'
java-version: '21'
- name: Build and run the application
run: |
mvn clean spring-boot:run &
- name: Wait for the application to start
run: sleep 30
- name: Deploy API documentation
uses: bump-sh/github-action@v1
with:
doc: spring-code-first
token: ${{secrets.BUMP_TOKEN}}
file: http://localhost:8080/openapi
This workflow will run every time a commit is pushed to the main
branch, build
the application, wait for it to start, and then deploy the OpenAPI description
to Bump.sh using the bump deploy
command.
Make sure to set the BUMP_TOKEN
secret in your GitHub repository settings with
the token you retrieved from the Bump.sh CI deployment settings page.
Sample Code
The sample code for this guide is published on GitHub so you can try that if you’re having trouble adding it to your application: spring-code-first, and the deployed documentation is over here.