API Documentation
Document your REST API with OpenAPI and Swagger UI
What You Will Learn
Good documentation makes your API usable. In this practical work, you'll learn:
- OpenAPI Specification - The standard for describing REST APIs
- Swagger UI - Interactive documentation and testing interface
- SpringDoc - Library to auto-generate OpenAPI from Spring code
- Annotations - How to add descriptions, examples, and schemas
- Code Generation - Generate client code from your API spec
Understanding OpenAPI
OpenAPI (formerly Swagger) is a specification for describing REST APIs. Think of it as a "blueprint" that tells developers:
- What endpoints exist
- What parameters each endpoint accepts
- What responses each endpoint returns
- How to authenticate
This blueprint can be used to auto-generate documentation, client code, and tests!
Your Code → SpringDoc → OpenAPI Spec (JSON) → Swagger UI
│ │ │
│ │ └── Interactive docs
│ │
│ └── Machine-readable API description
│
└── Java annotations describe your API
Prerequisites
- Java 17+ and Maven installed
- Basic REST API knowledge
- A working Spring Boot REST API (or use our starter)
Part 1: Project Setup
Step 1.1: Create Project Structure
documented-api/
├── pom.xml
└── src/
└── main/
├── java/com/example/docs/
│ ├── DocsApiApplication.java
│ ├── config/
│ ├── controller/
│ ├── dto/
│ └── entity/
└── resources/
└── application.properties
Step 1.2: Create pom.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
http://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>3.2.3</version>
</parent>
<groupId>com.example</groupId>
<artifactId>documented-api</artifactId>
<version>1.0.0-SNAPSHOT</version>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- (#1:SpringDoc OpenAPI - this is the magic!) -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.3.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Just by adding the springdoc-openapi-starter-webmvc-ui dependency, SpringDoc will automatically scan your controllers and generate documentation!
Step 1.3: Create application.properties
server.port=8080
# (#1:Customize Swagger UI path - default is /swagger-ui.html)
springdoc.swagger-ui.path=/swagger-ui.html
# (#2:Customize API docs path - default is /v3/api-docs)
springdoc.api-docs.path=/api-docs
# (#3:Show operation IDs in UI)
springdoc.swagger-ui.operations-sorter=method
# (#4:Expand all by default)
springdoc.swagger-ui.doc-expansion=list
Step 1.4: Create Main Application
Create src/main/java/com/example/docs/DocsApiApplication.java:
package com.example.docs;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DocsApiApplication {
public static void main(String[] args) {
SpringApplication.run(DocsApiApplication.class, args);
}
}
Part 2: Create a Simple API
Step 2.1: Create Product DTO
Create src/main/java/com/example/docs/dto/ProductDTO.java:
package com.example.docs.dto;
import jakarta.validation.constraints.*;
import java.math.BigDecimal;
public class ProductDTO {
private Long id;
@NotBlank(message = "Name is required")
@Size(min = 2, max = 100)
private String name;
@Size(max = 500)
private String description;
@NotNull(message = "Price is required")
@DecimalMin(value = "0.01", message = "Price must be positive")
private BigDecimal price;
@NotBlank(message = "Category is required")
private String category;
private boolean inStock = true;
// Constructors
public ProductDTO() {}
public ProductDTO(Long id, String name, String description, BigDecimal price, String category) {
this.id = id;
this.name = name;
this.description = description;
this.price = price;
this.category = category;
}
// Getters and Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public BigDecimal getPrice() { return price; }
public void setPrice(BigDecimal price) { this.price = price; }
public String getCategory() { return category; }
public void setCategory(String category) { this.category = category; }
public boolean isInStock() { return inStock; }
public void setInStock(boolean inStock) { this.inStock = inStock; }
}
Step 2.2: Create Basic Controller
Create src/main/java/com/example/docs/controller/ProductController.java:
package com.example.docs.controller;
import com.example.docs.dto.ProductDTO;
import jakarta.validation.Valid;
import org.springframework.http.*;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
import java.util.*;
import java.util.concurrent.atomic.AtomicLong;
@RestController
@RequestMapping("/api/products")
public class ProductController {
// In-memory storage for demo
private final Map<Long, ProductDTO> products = new HashMap<>();
private final AtomicLong idGenerator = new AtomicLong(1);
public ProductController() {
// Sample data
ProductDTO laptop = new ProductDTO(idGenerator.getAndIncrement(), "Laptop", "High-performance laptop", new BigDecimal("999.99"), "Electronics");
ProductDTO mouse = new ProductDTO(idGenerator.getAndIncrement(), "Mouse", "Wireless mouse", new BigDecimal("29.99"), "Electronics");
products.put(laptop.getId(), laptop);
products.put(mouse.getId(), mouse);
}
@GetMapping
public List<ProductDTO> getAllProducts() {
return new ArrayList<>(products.values());
}
@GetMapping("/{id}")
public ResponseEntity<ProductDTO> getProductById(@PathVariable Long id) {
ProductDTO product = products.get(id);
if (product == null) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(product);
}
@PostMapping
public ResponseEntity<ProductDTO> createProduct(@Valid @RequestBody ProductDTO product) {
product.setId(idGenerator.getAndIncrement());
products.put(product.getId(), product);
return ResponseEntity.status(HttpStatus.CREATED).body(product);
}
@PutMapping("/{id}")
public ResponseEntity<ProductDTO> updateProduct(@PathVariable Long id, @Valid @RequestBody ProductDTO product) {
if (!products.containsKey(id)) {
return ResponseEntity.notFound().build();
}
product.setId(id);
products.put(id, product);
return ResponseEntity.ok(product);
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteProduct(@PathVariable Long id) {
if (!products.containsKey(id)) {
return ResponseEntity.notFound().build();
}
products.remove(id);
return ResponseEntity.noContent().build();
}
}
Step 2.3: Test Auto-Generated Docs
cd documented-api
mvn spring-boot:run
Open your browser:
- Swagger UI: http://localhost:8080/swagger-ui.html
- OpenAPI JSON: http://localhost:8080/api-docs
SpringDoc automatically discovered your controller and created documentation!
Part 3: Add Rich Documentation
Step 3.1: Configure Global API Info
Create src/main/java/com/example/docs/config/OpenApiConfig.java:
package com.example.docs.config;
import io.swagger.v3.oas.models.*;
import io.swagger.v3.oas.models.info.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class OpenApiConfig {
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
// (#1:API metadata)
.info(new Info()
.title("Product Catalog API")
.version("1.0.0")
.description("A comprehensive API for managing product inventory. " +
"This API supports CRUD operations on products with filtering and pagination.")
.termsOfService("https://example.com/terms")
.contact(new Contact()
.name("API Support Team")
.email("api-support@example.com")
.url("https://example.com/support"))
.license(new License()
.name("Apache 2.0")
.url("https://www.apache.org/licenses/LICENSE-2.0")))
// (#2:External documentation link)
.externalDocs(new ExternalDocumentation()
.description("Full API Documentation")
.url("https://example.com/docs"));
}
}
Step 3.2: Add Documentation Annotations to Controller
Update ProductController.java with OpenAPI annotations:
package com.example.docs.controller;
import com.example.docs.dto.ProductDTO;
import io.swagger.v3.oas.annotations.*;
import io.swagger.v3.oas.annotations.media.*;
import io.swagger.v3.oas.annotations.responses.*;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import org.springframework.http.*;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
import java.util.*;
import java.util.concurrent.atomic.AtomicLong;
@RestController
@RequestMapping("/api/products")
@Tag(name = "Products", description = "Product management operations") // (#1:Group endpoints)
public class ProductController {
private final Map<Long, ProductDTO> products = new HashMap<>();
private final AtomicLong idGenerator = new AtomicLong(1);
public ProductController() {
ProductDTO laptop = new ProductDTO(idGenerator.getAndIncrement(), "Laptop", "High-performance laptop", new BigDecimal("999.99"), "Electronics");
ProductDTO mouse = new ProductDTO(idGenerator.getAndIncrement(), "Mouse", "Wireless mouse", new BigDecimal("29.99"), "Electronics");
products.put(laptop.getId(), laptop);
products.put(mouse.getId(), mouse);
}
// (#2:Document the operation)
@Operation(
summary = "Get all products",
description = "Retrieves a list of all products in the catalog"
)
@ApiResponses({
@ApiResponse(responseCode = "200", description = "Successfully retrieved products")
})
@GetMapping
public List<ProductDTO> getAllProducts() {
return new ArrayList<>(products.values());
}
// (#3:Document with multiple responses)
@Operation(
summary = "Get product by ID",
description = "Retrieves a single product by its unique identifier"
)
@ApiResponses({
@ApiResponse(responseCode = "200", description = "Product found",
content = @Content(schema = @Schema(implementation = ProductDTO.class))),
@ApiResponse(responseCode = "404", description = "Product not found",
content = @Content)
})
@GetMapping("/{id}")
public ResponseEntity<ProductDTO> getProductById(
@Parameter(description = "Product ID", example = "1") // (#4:Document parameter)
@PathVariable Long id) {
ProductDTO product = products.get(id);
if (product == null) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(product);
}
// (#5:Document request body)
@Operation(
summary = "Create a new product",
description = "Adds a new product to the catalog"
)
@ApiResponses({
@ApiResponse(responseCode = "201", description = "Product created successfully"),
@ApiResponse(responseCode = "400", description = "Invalid input data")
})
@PostMapping
public ResponseEntity<ProductDTO> createProduct(
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "Product to create",
required = true,
content = @Content(schema = @Schema(implementation = ProductDTO.class)))
@Valid @RequestBody ProductDTO product) {
product.setId(idGenerator.getAndIncrement());
products.put(product.getId(), product);
return ResponseEntity.status(HttpStatus.CREATED).body(product);
}
@Operation(summary = "Update a product", description = "Updates an existing product")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "Product updated"),
@ApiResponse(responseCode = "404", description = "Product not found"),
@ApiResponse(responseCode = "400", description = "Invalid input")
})
@PutMapping("/{id}")
public ResponseEntity<ProductDTO> updateProduct(
@Parameter(description = "Product ID to update") @PathVariable Long id,
@Valid @RequestBody ProductDTO product) {
if (!products.containsKey(id)) {
return ResponseEntity.notFound().build();
}
product.setId(id);
products.put(id, product);
return ResponseEntity.ok(product);
}
@Operation(summary = "Delete a product", description = "Removes a product from the catalog")
@ApiResponses({
@ApiResponse(responseCode = "204", description = "Product deleted"),
@ApiResponse(responseCode = "404", description = "Product not found")
})
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteProduct(
@Parameter(description = "Product ID to delete") @PathVariable Long id) {
if (!products.containsKey(id)) {
return ResponseEntity.notFound().build();
}
products.remove(id);
return ResponseEntity.noContent().build();
}
}
Step 3.3: Add Schema Documentation to DTO
Update ProductDTO.java with schema annotations:
package com.example.docs.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.*;
import java.math.BigDecimal;
@Schema(description = "Product information") // (#1:Describe the schema)
public class ProductDTO {
@Schema(description = "Unique product identifier", example = "1", accessMode = Schema.AccessMode.READ_ONLY)
private Long id;
@NotBlank(message = "Name is required")
@Size(min = 2, max = 100)
@Schema(description = "Product name", example = "MacBook Pro", required = true, minLength = 2, maxLength = 100)
private String name;
@Size(max = 500)
@Schema(description = "Product description", example = "16-inch laptop with M3 chip")
private String description;
@NotNull(message = "Price is required")
@DecimalMin(value = "0.01", message = "Price must be positive")
@Schema(description = "Product price in USD", example = "2499.99", required = true, minimum = "0.01")
private BigDecimal price;
@NotBlank(message = "Category is required")
@Schema(description = "Product category", example = "Electronics", required = true, allowableValues = {"Electronics", "Clothing", "Books", "Food"})
private String category;
@Schema(description = "Whether the product is currently in stock", example = "true", defaultValue = "true")
private boolean inStock = true;
// Constructors
public ProductDTO() {}
public ProductDTO(Long id, String name, String description, BigDecimal price, String category) {
this.id = id;
this.name = name;
this.description = description;
this.price = price;
this.category = category;
}
// Getters and Setters (same as before)
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public BigDecimal getPrice() { return price; }
public void setPrice(BigDecimal price) { this.price = price; }
public String getCategory() { return category; }
public void setCategory(String category) { this.category = category; }
public boolean isInStock() { return inStock; }
public void setInStock(boolean inStock) { this.inStock = inStock; }
}
Step 3.4: View Enhanced Documentation
Restart the application and refresh Swagger UI. You'll now see:
- API title, version, and description
- Contact and license information
- Detailed descriptions for each endpoint
- Example values in the schemas
- Try-it-out functionality with pre-filled examples
Part 4: Add Query Parameters and Filtering
Step 4.1: Add Search Endpoint
Add this method to ProductController.java:
@Operation(
summary = "Search products",
description = "Search and filter products by various criteria"
)
@ApiResponses({
@ApiResponse(responseCode = "200", description = "Search results returned")
})
@GetMapping("/search")
public List<ProductDTO> searchProducts(
@Parameter(description = "Filter by product name (partial match)", example = "laptop")
@RequestParam(required = false) String name,
@Parameter(description = "Filter by category", example = "Electronics",
schema = @Schema(allowableValues = {"Electronics", "Clothing", "Books", "Food"}))
@RequestParam(required = false) String category,
@Parameter(description = "Minimum price filter", example = "10.00")
@RequestParam(required = false) BigDecimal minPrice,
@Parameter(description = "Maximum price filter", example = "1000.00")
@RequestParam(required = false) BigDecimal maxPrice,
@Parameter(description = "Filter by stock availability")
@RequestParam(required = false, defaultValue = "true") Boolean inStock) {
return products.values().stream()
.filter(p -> name == null || p.getName().toLowerCase().contains(name.toLowerCase()))
.filter(p -> category == null || p.getCategory().equalsIgnoreCase(category))
.filter(p -> minPrice == null || p.getPrice().compareTo(minPrice) >= 0)
.filter(p -> maxPrice == null || p.getPrice().compareTo(maxPrice) <= 0)
.filter(p -> inStock == null || p.isInStock() == inStock)
.toList();
}
Part 5: Add JWT Security Documentation
Step 5.1: Configure Security Scheme
Update OpenApiConfig.java:
package com.example.docs.config;
import io.swagger.v3.oas.models.*;
import io.swagger.v3.oas.models.info.*;
import io.swagger.v3.oas.models.security.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class OpenApiConfig {
@Bean
public OpenAPI customOpenAPI() {
// (#1:Define JWT security scheme)
final String securitySchemeName = "bearerAuth";
return new OpenAPI()
.info(new Info()
.title("Product Catalog API")
.version("1.0.0")
.description("A comprehensive API for managing product inventory.\n\n" +
"## Authentication\n" +
"This API uses JWT Bearer tokens for authentication.\n\n" +
"1. Get a token from `/api/auth/login`\n" +
"2. Click 'Authorize' button above\n" +
"3. Enter: `Bearer your-token-here`")
.contact(new Contact()
.name("API Support")
.email("support@example.com")))
// (#2:Add security component)
.addSecurityItem(new SecurityRequirement()
.addList(securitySchemeName))
.components(new Components()
.addSecuritySchemes(securitySchemeName,
new SecurityScheme()
.name(securitySchemeName)
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")
.description("Enter JWT token")));
}
}
Adds an "Authorize" button to Swagger UI. Users can enter their JWT token, and it will be automatically included in all subsequent requests.
Step 5.2: Mark Public Endpoints
For endpoints that don't require authentication, add this annotation:
import io.swagger.v3.oas.annotations.security.SecurityRequirements;
@Operation(summary = "Get all products")
@SecurityRequirements // (#1:Empty = no auth required)
@GetMapping
public List<ProductDTO> getAllProducts() {
// ...
}
Part 6: Group APIs with Tags
Step 6.1: Create Multiple Controllers with Tags
Create src/main/java/com/example/docs/controller/CategoryController.java:
package com.example.docs.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.*;
import java.util.*;
@RestController
@RequestMapping("/api/categories")
@Tag(name = "Categories", description = "Product category management")
public class CategoryController {
private final List<String> categories = new ArrayList<>(
List.of("Electronics", "Clothing", "Books", "Food", "Home & Garden")
);
@Operation(summary = "Get all categories")
@GetMapping
public List<String> getAllCategories() {
return categories;
}
@Operation(summary = "Add a new category")
@PostMapping
public List<String> addCategory(@RequestBody String category) {
categories.add(category);
return categories;
}
}
Step 6.2: Configure Tag Order
Update application.properties:
# Sort tags alphabetically
springdoc.swagger-ui.tags-sorter=alpha
# Or define custom order in OpenApiConfig:
# springdoc.swagger-ui.tagsSorter = custom order defined in OpenAPI bean
Part 7: Export and Use OpenAPI Spec
Step 7.1: Download OpenAPI JSON
While your app is running, download the spec:
# Download OpenAPI JSON
curl http://localhost:8080/api-docs -o openapi.json
# Or download YAML format
curl http://localhost:8080/api-docs.yaml -o openapi.yaml
Step 7.2: Understanding the Generated Spec
The openapi.json file contains:
{
"openapi": "3.0.1",
"info": {
"title": "Product Catalog API",
"version": "1.0.0"
},
"paths": {
"/api/products": {
"get": {
"tags": ["Products"],
"summary": "Get all products",
"responses": {
"200": {
"description": "Successfully retrieved products",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/ProductDTO"
}
}
}
}
}
}
}
}
},
"components": {
"schemas": {
"ProductDTO": {
"type": "object",
"properties": {
"id": { "type": "integer", "example": 1 },
"name": { "type": "string", "example": "MacBook Pro" }
}
}
}
}
}
Step 7.3: Generate Client Code (Optional)
You can use the OpenAPI spec to generate client code in any language. Example using OpenAPI Generator:
# Install OpenAPI Generator (requires Java)
npm install @openapitools/openapi-generator-cli -g
# Generate TypeScript client
openapi-generator-cli generate \
-i http://localhost:8080/api-docs \
-g typescript-fetch \
-o ./generated-client
# Generate Python client
openapi-generator-cli generate \
-i http://localhost:8080/api-docs \
-g python \
-o ./python-client
Generated clients provide type-safe API access with proper method signatures, error handling, and serialization - all matching your exact API spec!
Deliverables Checklist
- SpringDoc dependency added and working
- Swagger UI accessible at /swagger-ui.html
- API info configured (title, version, description)
- All endpoints documented with @Operation
- Response codes documented with @ApiResponses
- DTOs documented with @Schema
- Query parameters documented with @Parameter
- Security scheme configured for JWT
- OpenAPI JSON downloadable from /api-docs
Key Annotations Summary
| Annotation | Purpose | Location |
|---|---|---|
@Tag | Group related endpoints | Controller class |
@Operation | Describe an endpoint | Controller method |
@ApiResponses | Document response codes | Controller method |
@Parameter | Describe path/query param | Method parameter |
@Schema | Describe DTO properties | DTO class/field |
@Hidden | Exclude from docs | Any |
Bonus Challenges
Add v1 and v2 API versions and configure Swagger to show them separately.
Add @ExampleObject annotations to show different example responses for success and error cases.
Use OpenAPI Generator to create a TypeScript or Python client and test it against your API.