Production · Lesson 86 of 95
Swagger and OpenAPI Documentation
Swagger and OpenAPI documentation in Spring Boot: add springdoc, open Swagger UI, describe endpoints with annotations and hide the docs in production.
When you buy a new mixer grinder, it comes with a manual: what each button does, what the jar sizes are, and what to do if it stops. Without it, you would press buttons and hope. An API is the same. A mobile developer who wants to call your endpoints needs a manual: which addresses exist, what to send, and what comes back. Swagger and OpenAPI documentation gives your Spring Boot app a manual that writes itself from your code and even lets people try the calls in the browser.
In this guide you will add API documentation to a hotel rooms service, open the interactive page, describe endpoints with annotations, and look at the raw OpenAPI file behind it.
What is Swagger and OpenAPI Documentation?
People often use the two names as one, so here is the clear picture:
- OpenAPI is the specification, the format of the manual.
- Swagger UI is the reader that draws the manual as a web page.
- springdoc-openapi is the library that scans your Spring code and produces the OpenAPI file for you.
You do not write the file by hand. Springdoc reads your controllers, request bodies and return types, and builds it while the app runs.
Why is it used?
- A manual that stays current. It is created from the code, so it does not go out of date like a separate document.
- Try before you code. Frontend and mobile teams can call the API from the browser page and see real answers.
- Fewer questions. Testers and partners can find out what an endpoint expects without asking you.
- Tool support. From one OpenAPI file, tools can create client code, Postman collections and test cases.
How it works
textYour controllers (@RestController, records) | v +------------------------+ | springdoc-openapi | | scans them at runtime | +------------------------+ | v +------------------------+ | /v3/api-docs | | the OpenAPI file, JSON | +------------------------+ | v +------------------------+ | /swagger-ui.html | | the clickable page | +------------------------+
When the app starts, springdoc looks at your mapped endpoints and their types. It publishes the result as JSON at /v3/api-docs. Then Swagger UI, which is bundled in the starter, loads that JSON and draws the page. Anything you add with annotations, such as descriptions and example values, is merged into the same file.
Real-Life Example
A restaurant prints a menu card with dish names, prices and notes like "contains nuts". The card is made from the kitchen's real list, so it always matches what the kitchen can cook. New waiters read the card instead of asking the chef. The OpenAPI file is that menu card, Swagger UI is the printed page, and the try-it button is a waiter who fetches a sample dish for you.
Code Example
Let's document StayWell Hotels. The app has three endpoints for rooms. We use the springdoc starter for Spring MVC with the UI included. Spring Boot does not manage its version, so we give it ourselves. Version 3.1.1 built and ran with Spring Boot 4.1.1 here.
File: pom.xml
xml<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>4.1.1</version> <relativePath/> </parent> <groupId>com.staywell</groupId> <artifactId>rooms</artifactId> <version>0.0.1-SNAPSHOT</version> <properties> <java.version>21</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webmvc</artifactId> </dependency> <dependency> <groupId>org.springdoc</groupId> <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId> <version>3.1.1</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
File: application.properties in src/main/resources
propertiesspring.application.name=staywell-rooms springdoc.swagger-ui.path=/docs springdoc.swagger-ui.operations-sorter=method
File: RoomsApplication.java in package com.staywell.rooms
javapackage com.staywell.rooms; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.info.Info; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; @SpringBootApplication public class RoomsApplication { public static void main(String[] args) { SpringApplication.run(RoomsApplication.class, args); } @Bean OpenAPI staywellApi() { return new OpenAPI().info(new Info() .title("StayWell Rooms API") .version("1.0") .description("Search and manage hotel rooms.")); } }
File: Room.java in package com.staywell.rooms
javapackage com.staywell.rooms; import io.swagger.v3.oas.annotations.media.Schema; @Schema(description = "A hotel room") public record Room( @Schema(description = "Room number", example = "101") int number, @Schema(description = "Room type", example = "Deluxe") String type, @Schema(description = "Price per night in rupees", example = "3500") int pricePerNight) {}
File: RoomController.java in package com.staywell.rooms
javapackage com.staywell.rooms; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; 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.RequestMapping; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/rooms") @Tag(name = "Rooms", description = "Room search and management") public class RoomController { private final List<Room> rooms = new CopyOnWriteArrayList<>(List.of( new Room(101, "Deluxe", 3500), new Room(102, "Standard", 2200))); @Operation(summary = "List all rooms") @GetMapping public List<Room> all() { return rooms; } @Operation(summary = "Find one room by its number") @ApiResponse(responseCode = "200", description = "Room found") @ApiResponse(responseCode = "404", description = "No room with that number", content = @io.swagger.v3.oas.annotations.media.Content) @GetMapping("/{number}") public ResponseEntity<Room> one(@PathVariable int number) { return rooms.stream().filter(r -> r.number() == number).findFirst() .map(ResponseEntity::ok) .orElse(ResponseEntity.notFound().build()); } @Operation(summary = "Add a new room") @ResponseStatus(HttpStatus.CREATED) @PostMapping public Room add(@RequestBody Room room) { rooms.add(room); return room; } }
Run the app with ./mvnw spring-boot:run, then open http://localhost:8080/docs in a browser for the interactive page. To see the raw OpenAPI file, open the address below.
bashcurl http://localhost:8080/v3/api-docs
Output:
textopenapi: 3.1.0 title: StayWell Rooms API version: 1.0 GET /rooms summary: List all rooms responses: 200 POST /rooms summary: Add a new room responses: 201 GET /rooms/{number} summary: Find one room by its number responses: 200, 404 schema Room: number, type, pricePerNight
The real reply is one long line of JSON. The text above is a short summary of it. When we ran the app, /docs and /swagger-ui/index.html both answered with status 200.
Code Explained
- The single springdoc starter brings both the scanner and the Swagger UI files. Spring Boot does not manage its version, so it must be written in the
pom.xml. Pick the release that matches your Spring Boot line, as listed in the springdoc documentation. - With no code at all, springdoc already lists the three endpoints, their paths and the
Roomshape. The annotations only make the manual friendlier. - The
OpenAPIbean sets the title, version and description shown at the top of the page. @Schemaon the record describes each field and gives an example value. Those examples are what "Try it out" pre-fills in the request body.@Taggroups the endpoints under the heading "Rooms".@Operation(summary = ...)gives each one a short, readable title.@ApiResponsedocuments the status codes an endpoint can return, such as the404for a missing room.- The property
springdoc.swagger-ui.pathmoves the page to/docs. The raw file stays at/v3/api-docs.
Hiding the Docs in Production
The manual lists every endpoint, which is helpful for you and for attackers who want to explore your API. Turn it off where it should not be public.
propertiesspringdoc.api-docs.enabled=false springdoc.swagger-ui.enabled=false
Put these in the production profile, for example application-prod.properties, so local and test environments keep the docs. If the docs stay on, put them behind a login.
Common Mistakes
- Forgetting the version. The springdoc starter is not managed by Spring Boot, so a missing
<version>fails the build. - Using the wrong springdoc line for your Spring Boot. Old 1.x and 2.x releases target older Spring Boot versions. Check the compatibility table.
- Blocking the docs with Spring Security. If you use security, allow
/v3/api-docs/**and the Swagger UI paths, or log in first. - Returning `Map` or `Object` everywhere. The manual can only describe what it can see. Use records for requests and responses.
- Over-annotating. Long descriptions on every field slow you down. Add the notes that help a reader, and skip the rest.
- Treating the docs as a test. The page shows what the code does, not whether it is right.
Interview Questions
What is the difference between Swagger and OpenAPI?
Ans:OpenAPI is the specification for describing a REST API. Swagger is the set of tools around it, such as Swagger UI.
Which library generates OpenAPI docs in Spring Boot?
Ans:springdoc-openapi. It scans your controllers at runtime and serves the OpenAPI file and Swagger UI.
Where is the raw OpenAPI file served?
Ans:At /v3/api-docs by default.
How do you document a status code on an endpoint?
Ans:Add @ApiResponse(responseCode = "404", description = "...") to the method.
Should Swagger UI be enabled in production?
Ans:Usually not. Disable it or protect it behind a login, because it exposes the full API surface.
Key Points to Remember
- OpenAPI describes an API, and Swagger UI shows it as a web page.
- One springdoc starter gives you both, with the version set in your
pom.xml. - The file is built from your code, so it stays in step with it.
@Tag,@Operation,@Schemaand@ApiResponseadd clear notes.- Turn the docs off or protect them in production.
Frequently Asked Questions
Is Swagger and OpenAPI documentation automatic in Spring Boot?
Mostly. Springdoc creates a working manual from your controllers with no annotations. You add annotations only to improve it.
What URL opens Swagger UI?
By default /swagger-ui.html (or /swagger-ui/index.html). In our example we changed it to /docs with a property.
Can I use Swagger with Spring Security?
Yes. Allow the docs paths in your security rules, or add a login button using a security scheme in the OpenAPI bean.
Can I generate a client from the OpenAPI file?
Yes. Tools such as OpenAPI Generator read the file and create client code for Java, TypeScript and many other languages.
Related Topics
- Building a Complete CRUD REST API: the kind of API this page documents.
- Standard API Error Response: document your error shape too.
- Spring Security Basics: keep the docs page safe.
- DTO Pattern: clear request and response types make clear documentation.
Practice Problems
Try each problem on your own first. Both use the same springdoc starter and version as the StayWell example, so the pom.xml shows it again.
Easy: Documented Show List
StarPlex Cinema has GET /shows, which returns a list of shows (title and start time). Add Swagger documentation so the page shows the API title StarPlex Shows API, a tag named Shows, and the summary List today's shows for the endpoint. Check /v3/api-docs for them.
Show answerHide answer
File: pom.xml
xml<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>4.1.1</version> <relativePath/> </parent> <groupId>com.starplex</groupId> <artifactId>shows</artifactId> <version>0.0.1-SNAPSHOT</version> <properties> <java.version>21</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webmvc</artifactId> </dependency> <dependency> <groupId>org.springdoc</groupId> <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId> <version>3.1.1</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
File: ShowsApplication.java in package com.starplex.shows
javapackage com.starplex.shows; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.info.Info; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; @SpringBootApplication public class ShowsApplication { public static void main(String[] args) { SpringApplication.run(ShowsApplication.class, args); } @Bean OpenAPI showsApi() { return new OpenAPI().info(new Info().title("StarPlex Shows API").version("1.0")); } }
File: ShowController.java in package com.starplex.shows
javapackage com.starplex.shows; import java.util.List; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController @Tag(name = "Shows") public class ShowController { record Show(String title, String startsAt) {} @Operation(summary = "List today's shows") @GetMapping("/shows") public List<Show> shows() { return List.of(new Show("Monsoon Express", "18:30"), new Show("Chai at Midnight", "21:00")); } }
Output:
texttitle: StarPlex Shows API tag: Shows GET /shows summary: List today's shows responses: 200 schemas: Show
This is a short summary read from the JSON that /v3/api-docs returns.
Medium: Bakery Orders with an Error Shape and a Hidden Endpoint
Golden Crust Bakery has POST /orders (returns 201 with the order, or 400 with an error body {"message": "..."}) and an internal POST /internal/reset used by staff scripts. Document the 400 with its error shape, and make sure the internal endpoint does not appear in the documentation.
Show answerHide answer
@ApiResponse with a schema puts the ErrorBody shape into the file. @Hidden removes the internal endpoint, so it is not listed, although it still works when called.File: pom.xml
xml<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>4.1.1</version> <relativePath/> </parent> <groupId>com.goldencrust</groupId> <artifactId>orders</artifactId> <version>0.0.1-SNAPSHOT</version> <properties> <java.version>21</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webmvc</artifactId> </dependency> <dependency> <groupId>org.springdoc</groupId> <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId> <version>3.1.1</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
File: OrdersApplication.java in package com.goldencrust.orders
javapackage com.goldencrust.orders; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class OrdersApplication { public static void main(String[] args) { SpringApplication.run(OrdersApplication.class, args); } }
File: ErrorBody.java in package com.goldencrust.orders
javapackage com.goldencrust.orders; public record ErrorBody(String message) {}
File: OrderController.java in package com.goldencrust.orders
javapackage com.goldencrust.orders; import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; @RestController public class OrderController { record Order(String item, int quantity) {} @Operation(summary = "Place an order") @ApiResponse(responseCode = "201", description = "Order placed") @ApiResponse(responseCode = "400", description = "Invalid order", content = @Content(schema = @Schema(implementation = ErrorBody.class))) @PostMapping("/orders") public ResponseEntity<?> place(@RequestBody Order order) { if (order.quantity() <= 0) { return ResponseEntity.badRequest().body(new ErrorBody("Quantity must be positive")); } return ResponseEntity.status(201).body(order); } @Hidden @PostMapping("/internal/reset") public String reset() { return "reset done"; } }
Output:
textPOST /orders summary: Place an order responses: 201, 400 schemas: Order, ErrorBody /internal/reset: not listed
This is a short summary read from the JSON that /v3/api-docs returns. The hidden endpoint is missing from it.