Fork me on GitHub

Spring REST OpenAPI Tutorial

This tutorial demonstrates that OfficeFloor REST endpoints declared as YAML files appear automatically in the OpenAPI specification generated by springdoc-openapi. No additional configuration is needed — adding the springdoc dependency is sufficient.

The tutorial exposes a simple product catalogue with two endpoints:

  • GET /product — returns a list of all products
  • GET /product/{id} — returns a single product by identifier

Tutorial Source

Maven dependency

Add springdoc-openapi-starter-webmvc-ui alongside the OfficeFloor starter:

		<dependency>
			<groupId>net.officefloor.springboot</groupId>
			<artifactId>officefloor-rest-spring-boot-starter</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springdoc</groupId>
			<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
			<version>2.8.6</version>
		</dependency>

That is the only change required — OfficeFloor's built-in SpringDoc integration registers all YAML-declared endpoints with the OpenApiCustomizer at start-up.

Application class

@SpringBootApplication
public class SpringRestOpenApiApplication {

	public static void main(String[] args) {
		SpringApplication.run(SpringRestOpenApiApplication.class, args);
	}
}

Response DTO with @Schema annotations

Swagger annotations on the response class populate the component schema in the generated spec:

@Data
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "A product in the catalogue")
public class Product {

	@Schema(description = "Unique product identifier")
	private Long id;

	@Schema(description = "Product name")
	private String name;

	@Schema(description = "Product price in cents")
	private int priceCents;
}

Service beans

A plain Spring @Service holds the in-memory catalogue:

@Service
public class ProductCatalogueService {

	private final List<Product> products = List.of(
			new Product(1L, "Widget", 999),
			new Product(2L, "Gadget", 1999),
			new Product(3L, "Doohickey", 4999));

	public List<Product> findAll() {
		return products;
	}

	public Product findById(long id) {
		return products.stream()
				.filter(p -> p.getId() == id)
				.findFirst()
				.orElseThrow(() -> new IllegalArgumentException("Product not found: " + id));
	}
}

Service methods with @Operation

@Operation on the service method populates the operation summary in the spec. The ProductCatalogueService is injected by type — no annotation needed on that parameter:

public class ListProductsService {

	@Operation(summary = "List all products")
	public void service(ProductCatalogueService catalogue, ObjectResponse<List<Product>> response) {
		response.send(catalogue.findAll());
	}
}
public class GetProductService {

	@Operation(summary = "Get product by ID")
	public void service(@PathVariable(name = "id") Long id,
			ProductCatalogueService catalogue,
			ObjectResponse<Product> response) {
		response.send(catalogue.findById(id));
	}
}

YAML endpoint files

Each endpoint is declared as a YAML file under officefloor/rest/:

officefloor/rest/
├── product.GET.yml          →  GET /product
└── product/
    └── {id}.GET.yml         →  GET /product/{id}
service:
  class: net.officefloor.tutorial.springrestopenapi.ListProductsService
service:
  class: net.officefloor.tutorial.springrestopenapi.GetProductService

Testing

Tests verify both endpoint behaviour and that the paths appear in the live OpenAPI JSON at GET /v3/api-docs:

@SpringBootTest
@AutoConfigureMockMvc
public class SpringRestOpenApiTest {

	@Autowired
	private MockMvc mvc;

	@Autowired
	private ObjectMapper mapper;

	@Test
	public void listProducts() throws Exception {
		mvc.perform(get("/product").accept(MediaType.APPLICATION_JSON))
				.andExpect(status().isOk())
				.andExpect(jsonPath("$[0].name").value("Widget"))
				.andExpect(jsonPath("$[1].name").value("Gadget"));
	}

	@Test
	public void getProductById() throws Exception {
		mvc.perform(get("/product/1").accept(MediaType.APPLICATION_JSON))
				.andExpect(status().isOk())
				.andExpect(jsonPath("$.name").value("Widget"))
				.andExpect(jsonPath("$.priceCents").value(999));
	}

	@Test
	public void openApiSpecIncludesListEndpoint() throws Exception {
		mvc.perform(get("/v3/api-docs"))
				.andExpect(status().isOk())
				.andExpect(content().string(containsString("/product")));
	}

	@Test
	public void openApiSpecIncludesGetByIdEndpoint() throws Exception {
		mvc.perform(get("/v3/api-docs"))
				.andExpect(status().isOk())
				.andExpect(content().string(containsString("/product/{id}")));
	}
}

The Swagger UI is available at http://localhost:8080/swagger-ui.html when the application is running.

Next

The Spring REST Thymeleaf tutorial demonstrates server-side HTML rendering with Thymeleaf from OfficeFloor REST service methods.