> Use when generating REST controllers, response wrappers, DTOs, error handlers, or any HTTP-facing code. Defines response envelope, HTTP status mapping, pagination, and API versioning (including Spring Boot 4's native version routing).
npx skills add https://github.com/rrezartprebreza/spring-boot-skills --skill rest-api-conventions
All endpoints return a consistent envelope:
{
"success": true,
"data": { },
"error": null,
"timestamp": "2026-04-13T10:00:00Z"
}
Error response:
{
"success": false,
"data": null,
"error": {
"code": "ORDER_NOT_FOUND",
"message": "Order with id 123 not found",
"details": []
},
"timestamp": "2026-04-13T10:00:00Z"
}
@JsonInclude(JsonInclude.Include.NON_NULL)
public record ApiResponse<T>(
boolean success,
T data,
ApiError error,
Instant timestamp
) {
public static <T> ApiResponse<T> ok(T data) {
return new ApiResponse<>(true, data, null, Instant.now());
}
public static <T> ApiResponse<T> error(String code, String message) {
return new ApiResponse<>(false, null, new ApiError(code, message, List.of()), Instant.now());
}
}
public record ApiError(String code, String message, List<String> details) {}
| Scenario | Status |
|----------|--------|
| GET — found | 200 |
| POST — created resource | 201 |
| PUT/PATCH — updated | 200 |
| DELETE — deleted | 204 (no body) |
| Validation failure | 400 |
| Unauthenticated | 401 |
| Forbidden | 403 |
| Not found | 404 |
| Conflict (duplicate) | 409 |
| Unhandled server error | 500 |
/orders, /users, /products/order-items, not /orderItems/api/v1/orders — route with native API versioning (below), don't duplicate controllers per version/orders/{id}/items ✅, /orders/{id}/items/{itemId}/notes ❌ — flatten to /order-item-notes/{id}GET /api/v1/orders → list (paginated)
POST /api/v1/orders → create
GET /api/v1/orders/{id} → get one
PUT /api/v1/orders/{id} → full update
PATCH /api/v1/orders/{id} → partial update
DELETE /api/v1/orders/{id} → delete
GET /api/v1/orders/{id}/items → nested resource
Spring Boot 4 / Framework 7 route requests by API version natively — never hand-roll it
with duplicated V1/V2 controllers, custom RequestConditions, or header if checks.
Pick ONE resolution strategy per API (path segment, header, query param, or media-type param):
spring:
mvc: # WebFlux: same keys under spring.webflux.apiversion.*
apiversion:
use:
path-segment: 1 # index of the path segment holding the version: /api/v1.1/orders
# header: X-API-Version
# query-parameter: version
supported: [1.0, 1.1, 2.0]
default: 1.0
Route with the version attribute on any mapping annotation:
@GetMapping("/{id}") // no version — matches any
public OrderResponse getById(@PathVariable UUID id) { ... }
@GetMapping(value = "/{id}", version = "1.1") // fixed: matches 1.1 only
public OrderResponseV1_1 getByIdV1_1(@PathVariable UUID id) { ... }
@GetMapping(value = "/{id}", version = "1.2+") // baseline: 1.2 and supported versions above
public OrderResponseV2 getByIdV2(@PathVariable UUID id) { ... }
The most specific matching version wins. Unsupported version → 400 (InvalidApiVersionException);
missing required version → 400 (MissingApiVersionException).
StandardApiVersionDeprecationHandler (register viaWebMvcConfigurer#configureApiVersioning(ApiVersionConfigurer)) — it emits RFC 9745
Deprecation/Sunset and Link response headers
RestClient/WebClient and HTTP interface clients send versions too —configure .apiVersionInserter(ApiVersionInserter.fromHeader("X-API-Version").build())
and .defaultVersion("1.2") on the builder, matching the server's strategy
{
"success": true,
"data": {
"content": [...],
"page": 0,
"size": 20,
"totalElements": 150,
"totalPages": 8,
"last": false
}
}
Query params: ?page=0&size=20&sort=createdAt,desc
Use Spring Data Pageable in controllers:
@GetMapping
public ApiResponse<Page<OrderResponse>> list(Pageable pageable) {
return ApiResponse.ok(orderService.findAll(pageable).map(OrderResponse::from));
}
Cap the page size. A bare Pageable accepts ?size=100000 from any client — one request can
drag your whole table into memory. Spring's default cap is 2000, still too high for most APIs:
spring:
data:
web:
pageable:
default-page-size: 20
max-page-size: 100 # requests above this are silently clamped
@RestControllerAdvice
@RequiredArgsConstructor
public class GlobalExceptionHandler {
@ExceptionHandler(EntityNotFoundException.class)
public ResponseEntity<ApiResponse<Void>> handleNotFound(EntityNotFoundException ex) {
return ResponseEntity.status(404).body(ApiResponse.error("NOT_FOUND", ex.getMessage()));
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ApiResponse<Void>> handleValidation(MethodArgumentNotValidException ex) {
List<String> details = ex.getBindingResult().getFieldErrors().stream()
.map(e -> e.getField() + ": " + e.getDefaultMessage()).toList();
return ResponseEntity.status(400)
.body(new ApiResponse<>(false, null, new ApiError("VALIDATION_FAILED", "Invalid input", details), Instant.now()));
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiResponse<Void>> handleGeneric(Exception ex) {
return ResponseEntity.status(500).body(ApiResponse.error("INTERNAL_ERROR", "An unexpected error occurred"));
}
}
ApiResponse.ok(...)ResponseEntity<Map<String, Object>> for errors — use ApiResponse@RestControllerAdviceLong IDs in URLs — use UUIDPageable — set spring.data.web.pageable.max-page-size or one request can pull the whole tablePage<Entity> serialized directly — exposes Hibernate internals; map to DTOs first/v1//v2 controllers — Boot 4 has native API versioning: version attribute on mappings + spring.mvc.apiversion.*spring-boot-starter-web — renamed spring-boot-starter-webmvc in Boot 4 (MockMvc tests: spring-boot-starter-webmvc-test)@JsonComponent or Jackson2ObjectMapperBuilderCustomizer to tune serialization — Jackson 3 renames: @JacksonComponent, JsonMapperBuilderCustomizer; declare JsonMapper beans, not generic ObjectMapper@SpringBootTest expecting MockMvc — Boot 4 no longer auto-provides it; add @AutoConfigureMockMvc (or the new RestTestClient via @AutoConfigureRestTestClient)Guide for creating high-quality MCP (Model Context Protocol) servers that enable LLMs to interact with external services through well-designed tools. Use when building MCP servers to integrate external APIs or services, whether in Python (FastMCP) or Node/TypeScript (MCP SDK).
Automatically creates user-facing changelogs from git commits by analyzing commit history, categorizing changes, and transforming technical commits into clear, customer-friendly release notes. Turns hours of manual changelog writing into minutes of automated generation.
Use when implementation is complete, all tests pass, and you need to decide how to integrate the work - guides completion of development work by presenting structured options for merge, PR, or cleanup
Guide for creating high-quality MCP (Model Context Protocol) servers that enable LLMs to interact with external services through well-designed tools. Use when building MCP servers to integrate external APIs or services, whether in Python (FastMCP) or Node/TypeScript (MCP SDK).
React Native and Expo best practices for building performant mobile apps. Use when building React Native components, optimizing list performance, implementing animations, or working with native modules. Triggers on tasks involving React Native, Expo, mobile performance, or native platform APIs.
React and Next.js performance optimization guidelines from Vercel Engineering. This skill should be used when writing, reviewing, or refactoring React/Next.js code to ensure optimal performance patterns. Triggers on tasks involving React components, Next.js pages, data fetching, bundle optimization, or performance improvements.
Next.js best practices - file conventions, RSC boundaries, data patterns, async APIs, metadata, error handling, route handlers, image/font optimization, bundling
Use when starting feature work that needs isolation from current workspace or before executing implementation plans - creates isolated git worktrees with smart directory selection and safety verification
Take rrezartprebreza/spring-boot-rest-api-conventions from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.