OpenAPI First
Use when the project follows API-first / OpenAPI-first approach: generating controller interfaces, DTOs, and clients from an OpenAPI spec. Use when you see openapi.yaml, openapi-generator-maven-plugin, or ApiDelegate pattern in the project.
View SKILL.mdMaven Plugin Setup
<plugin>
<groupId>org.openapitools</groupId>
<artifactId>openapi-generator-maven-plugin</artifactId>
<version>7.5.0</version>
<executions>
<execution>
<goals><goal>generate</goal></goals>
<configuration>
<inputSpec>${project.basedir}/src/main/resources/openapi.yaml</inputSpec>
<generatorName>spring</generatorName>
<apiPackage>com.example.api</apiPackage>
<modelPackage>com.example.api.model</modelPackage>
<configOptions>
<delegatePattern>true</delegatePattern> <!-- implement delegate, not controller -->
<interfaceOnly>false</interfaceOnly>
<useSpringBoot3>true</useSpringBoot3>
<useTags>true</useTags>
<dateLibrary>java8</dateLibrary>
<openApiNullable>false</openApiNullable>
<skipDefaultInterface>false</skipDefaultInterface> <!-- must stay false with delegatePattern: the controller instantiates a no-op delegate -->
</configOptions>
<!-- NOT generateSupportingFiles=true: that adds a second @SpringBootApplication main class
(repackage fails: "Unable to find a single main class") and a SpringDocConfiguration -->
<supportingFilesToGenerate>ApiUtil.java</supportingFilesToGenerate>
<modelNameSuffix>Dto</modelNameSuffix> <!-- OrderDto, not Order: no clash with domain types -->
<output>${project.build.directory}/generated-sources/openapi</output>
</configuration>
</execution>
</executions>
</plugin>
OpenAPI Spec Example
# src/main/resources/openapi.yaml
openapi: 3.0.3
info:
title: Order Service API
version: 1.0.0
paths:
/api/v1/orders:
post:
tags: [Orders]
operationId: createOrder
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateOrderRequest'
responses:
'201':
description: Order created
content:
application/json:
schema:
$ref: '#/components/schemas/OrderResponse'
'400':
$ref: '#/components/responses/ValidationError'
get:
tags: [Orders]
operationId: listOrders
parameters:
- name: page
in: query
schema: { type: integer, default: 0 }
- name: size
in: query
schema: { type: integer, default: 20 }
responses:
'200':
description: Paginated orders
content:
application/json:
schema:
$ref: '#/components/schemas/OrderPage'
components:
schemas:
CreateOrderRequest:
type: object
required: [customerEmail, items]
properties:
customerEmail:
type: string
format: email
items:
type: array
minItems: 1
items:
$ref: '#/components/schemas/OrderItemRequest'
OrderResponse:
type: object
properties:
id:
type: string
format: uuid
status:
type: string
enum: [PENDING, PROCESSING, SHIPPED, DELIVERED, CANCELLED]
customerEmail:
type: string
createdAt:
type: string
format: date-time
OrderItemRequest:
type: object
required: [productId, quantity]
properties:
productId:
type: string
format: uuid
quantity:
type: integer
minimum: 1
OrderPage:
type: object
properties:
content:
type: array
items:
$ref: '#/components/schemas/OrderResponse'
page:
type: integer
size:
type: integer
totalElements:
type: integer
format: int64
ApiError:
type: object
properties:
code:
type: string
message:
type: string
details:
type: array
items:
type: string
responses:
ValidationError:
description: Validation failed
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
Implementing the Delegate
// Generated: OrdersApi interface with delegate
// Your implementation — never modify generated files
@Service
@RequiredArgsConstructor
public class OrdersApiDelegateImpl implements OrdersApiDelegate {
private final OrderService orderService;
@Override
public ResponseEntity<OrderResponse> createOrder(CreateOrderRequest request) {
Order order = orderService.createOrder(request);
return ResponseEntity.status(HttpStatus.CREATED)
.body(OrderApiMapper.toResponse(order));
}
@Override
public ResponseEntity<OrderPage> listOrders(Integer page, Integer size) {
Page<Order> orders = orderService.findAll(PageRequest.of(page, size));
return ResponseEntity.ok(OrderApiMapper.toPage(orders));
}
}
.gitignore — Never Commit Generated Files
target/generated-sources/openapi/
Gotchas
- Agent modifies generated controller files — NEVER modify generated code, implement delegate
- Agent generates code without
useSpringBoot3=true— uses oldjavax.*imports - Agent commits generated sources — add to
.gitignore, generate on build - Agent sets
skipDefaultInterface=truewithdelegatePattern=true— the generated controller instantiates a no-op delegate, so it stops compiling; keep itfalseand cover every operation with a contract test - Agent sets
generateSupportingFiles=true— generates a second@SpringBootApplicationand SpringDoc config; list onlyApiUtil.javainsupportingFilesToGenerate - Agent mixes generated models with hand-written models — keep them separate
Examples & templates
bad-delegate-impl.javaexamples
Source on GitHub
// ❌ BAD — modifies generated controller, mixes models, wrong config
// Editing the GENERATED controller directly — will be overwritten on next build!
@RestController
@RequestMapping("/api/v1/orders")
public class OrdersApiController { // this file is generated — don't touch it
@Autowired // field injection
private OrderRepository orderRepository;
@GetMapping("/{id}")
public Order getOrder(@PathVariable Long id) { // Long ID instead of UUID
return orderRepository.findById(id)
.orElseThrow(); // returns domain entity, not generated DTO
}
@PostMapping
public ResponseEntity<OrderDto> createOrder(@RequestBody OrderDto dto) {
// Using generated DTO as both request AND response — spec has separate schemas
Order order = new Order();
order.setEmail(dto.getCustomerEmail());
order.setStatus(dto.getStatus().name()); // mixes generated enum with domain
orderRepository.save(order);
return ResponseEntity.ok(dto); // returns input DTO, not persisted state
}
}
// Plugin config missing useSpringBoot3=true:
// <configOptions>
// <useJakartaEe>false</useJakartaEe> <!-- generates javax.* imports on Spring Boot 3 -->
// <delegatePattern>false</delegatePattern> <!-- no delegate — must edit generated code -->
// </configOptions>
// Generated sources committed to git — should be in .gitignore and target/good-delegate-impl.javaexamples
Source on GitHub
// ✅ GOOD — implements generated delegate, never modifies generated code, maps between models
@Service
@RequiredArgsConstructor
public class OrderApiDelegateImpl implements OrdersApiDelegate {
private final OrderService orderService;
@Override
public ResponseEntity<OrderDto> getOrderById(UUID id) {
Order order = orderService.findById(id);
return ResponseEntity.ok(toDto(order));
}
@Override
public ResponseEntity<OrderDto> createOrder(CreateOrderRequestDto request) {
Order order = orderService.createOrder(
request.getCustomerEmail(),
request.getItems().stream()
.map(i -> new OrderItemCommand(i.getProductId(), i.getQuantity()))
.toList());
return ResponseEntity.status(HttpStatus.CREATED).body(toDto(order));
}
@Override
public ResponseEntity<PageOrderDto> listOrders(Integer page, Integer size) {
Page<Order> orders = orderService.findAll(PageRequest.of(page, size));
PageOrderDto dto = new PageOrderDto()
.content(orders.getContent().stream().map(this::toDto).toList())
.totalElements(orders.getTotalElements())
.totalPages(orders.getTotalPages());
return ResponseEntity.ok(dto);
}
// Map domain → generated DTO (never expose domain model via API)
private OrderDto toDto(Order order) {
return new OrderDto()
.id(order.getId())
.status(OrderDto.StatusEnum.fromValue(order.getStatus().name()))
.customerEmail(order.getCustomerEmail())
.createdAt(order.getCreatedAt());
}
}openapi.yamltemplates
Source on GitHub
openapi: 3.0.3
info:
title: Order API
version: 1.0.0
description: Order management REST API
servers:
- url: http://localhost:8080/api/v1
paths:
/orders:
get:
operationId: listOrders
summary: List orders with pagination
tags: [Orders]
parameters:
- name: page
in: query
schema: { type: integer, default: 0 }
- name: size
in: query
schema: { type: integer, default: 20 }
responses:
'200':
description: Paginated list of orders
content:
application/json:
schema:
$ref: '#/components/schemas/PageOrder'
post:
operationId: createOrder
summary: Create a new order
tags: [Orders]
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateOrderRequest'
responses:
'201':
description: Order created
content:
application/json:
schema:
$ref: '#/components/schemas/Order'
'400':
$ref: '#/components/responses/ValidationError'
/orders/{id}:
get:
operationId: getOrderById
summary: Get order by ID
tags: [Orders]
parameters:
- $ref: '#/components/parameters/OrderId'
responses:
'200':
description: Order found
content:
application/json:
schema:
$ref: '#/components/schemas/Order'
'404':
$ref: '#/components/responses/NotFound'
delete:
operationId: deleteOrder
summary: Delete an order
tags: [Orders]
parameters:
- $ref: '#/components/parameters/OrderId'
responses:
'204':
description: Order deleted
components:
parameters:
OrderId:
name: id
in: path
required: true
schema:
type: string
format: uuid
schemas:
Order:
type: object
properties:
id:
type: string
format: uuid
customerEmail:
type: string
format: email
status:
type: string
enum: [PENDING, CONFIRMED, SHIPPED, DELIVERED, CANCELLED]
items:
type: array
items:
$ref: '#/components/schemas/OrderItem'
createdAt:
type: string
format: date-time
OrderItem:
type: object
properties:
productId:
type: string
format: uuid
productName:
type: string
quantity:
type: integer
minimum: 1
unitPrice:
type: number
format: double
CreateOrderRequest:
type: object
required: [customerEmail, items]
properties:
customerEmail:
type: string
format: email
items:
type: array
minItems: 1
items:
type: object
required: [productId, quantity]
properties:
productId:
type: string
format: uuid
quantity:
type: integer
minimum: 1
PageOrder:
type: object
properties:
content:
type: array
items:
$ref: '#/components/schemas/Order'
totalElements:
type: integer
format: int64
totalPages:
type: integer
ProblemDetail:
type: object
properties:
type:
type: string
format: uri
title:
type: string
status:
type: integer
detail:
type: string
instance:
type: string
format: uri
responses:
NotFound:
description: Resource not found
content:
application/problem+json:
schema:
$ref: '#/components/schemas/ProblemDetail'
ValidationError:
description: Validation failed
content:
application/problem+json:
schema:
$ref: '#/components/schemas/ProblemDetail'Verification scope
- Compiled sources
- No
- Runtime checks
- No
Coverage describes checked-in fixtures, not a guarantee for every snippet or project.
Coverage matrix & agent evidenceADD TO YOUR AGENTInstall Boot 3 skills
Install Boot 3 skills
claude plugin marketplace add rrezartprebreza/spring-boot-skills
claude plugin install spring-boot-3-skills@spring-boot-skillscodex plugin marketplace add rrezartprebreza/spring-boot-skills
codex plugin add spring-boot-3-skills@spring-boot-skills