Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Use when generating REST controllers, response wrappers, DTOs, error handlers, or any HTTP-facing code. Defines response envelope, HTTP status mapping, pagination, and versioning.
.claude/skills/rrezartprebreza-rest-api-conventions/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 50% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 92% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 24% | 0% |
| case-04 | ✗→✓ | ▲ Improved | 84% | 0% |
| case-06 | ✗→✓ | ▲ Improved | 61% | 0% |
Inspect existing controllers, tests and OpenAPI before choosing a response contract. Preserve the project's IDs, response shape and versioning strategy. Do not migrate unrelated endpoints while adding one route. The envelope below is an optional convention for a project that uses it; plain success DTOs are equally valid. A 204 response has no body.
Success and error formats are independent: an existing success envelope can coexist with RFC 9457 errors. Use one consistent error policy; choose the legacy error examples below only when the project already requires that format.
Example success envelope:
json{ "success": true, "data": { }, "error": null, "timestamp": "2026-04-13T10:00:00Z" }
Error response:
json{ "success": false, "data": null, "error": { "code": "ORDER_NOT_FOUND", "message": "Order with id 123 not found", "details": [] }, "timestamp": "2026-04-13T10:00:00Z" }
java@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 resourceSpring 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):
yamlspring: 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:
java@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
json{ "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:
java@GetMapping public ApiResponse<PageResponse<OrderResponse>> list(Pageable pageable) { return ApiResponse.ok(PageResponse.from(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:
yamlspring: data: web: pageable: default-page-size: 20 max-page-size: 100 # requests above this are silently clamped
The compiled PageResponse fixes the JSON pagination contract. Mapping entities to DTOs alone does not stabilize Spring Data PageImpl serialization. Spring Data's org.springframework.data.web.PagedModel is another option when its shape fits the API. Validate sort fields against an allowlist and add an ID tie-breaker to non-unique sorts.
java@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")); } }
@RestControllerAdvicePageable — set spring.data.web.pageable.max-page-size or one request can pull the whole table/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)| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 31,949 | 13,272 | -58% | 1 | 1 | 0% | 3,024 | 4,533 | +50% | 0 | 0 | — |
case-02 | fail→pass | 14,707 | 14,038 | -5% | 1 | 1 | 0% | 2,516 | 4,840 | +92% | 0 | 0 | — |
case-03 | fail→pass | 22,392 | 16,325 | -27% | 1 | 1 | 0% | 4,348 | 5,385 | +24% | 0 | 0 | — |
case-04 | fail→pass | 14,461 | 11,250 | -22% | 1 | 1 | 0% | 2,318 | 4,270 | +84% | 0 | 0 | — |
case-05 | pass→pass | 15,421 | 10,530 | -32% | 1 | 1 | 0% | 2,218 | 3,921 | +77% | 0 | 0 | — |
case-06 | fail→pass | 12,864 | 8,859 | -31% | 1 | 1 | 0% | 2,393 | 3,844 | +61% | 0 | 0 | — |
case-07 | fail→pass | 9,201 | 5,141 | -44% | 1 | 1 | 0% | 1,503 | 3,192 | +112% | 0 | 0 | — |
case-08 | fail→pass | 13,904 | 7,024 | -49% | 1 | 1 | 0% | 2,163 | 3,335 | +54% | 0 | 0 | — |
case-09 | fail→pass | 20,996 | 13,773 | -34% | 1 | 1 | 0% | 4,286 | 4,650 | +8% | 0 | 0 | — |
case-10 | fail→pass | 8,273 | 11,266 | +36% | 1 | 1 | 0% | 1,280 | 3,704 | +189% | 0 | 0 | — |
case-11 | pass→pass | 19,823 | 12,561 | -37% | 1 | 1 | 0% | 2,857 | 4,204 | +47% | 0 | 0 | — |
case-12 | pass→pass | 11,664 | 9,286 | -20% | 1 | 1 | 0% | 2,211 | 4,035 | +82% | 0 | 0 | — |
case-13 | pass→pass | 7,190 | 3,273 | -54% | 1 | 1 | 0% | 831 | 2,580 | +210% | 0 | 0 | — |
case-14 | pass→pass | 7,086 | 3,359 | -53% | 1 | 1 | 0% | 1,020 | 2,669 | +162% | 0 | 0 | — |
case-15 | pass→pass | 11,870 | 2,979 | -75% | 1 | 1 | 0% | 2,031 | 2,604 | +28% | 0 | 0 | — |
case-16 | pass→pass | 21,987 | 18,265 | -17% | 1 | 1 | 0% | 3,217 | 5,150 | +60% | 0 | 0 | — |
case-17 | pass→pass | 11,782 | 7,817 | -34% | 1 | 1 | 0% | 1,579 | 3,202 | +103% | 0 | 0 | — |
case-18 | pass→pass | 13,788 | 6,119 | -56% | 1 | 1 | 0% | 2,002 | 2,990 | +49% | 0 | 0 | — |
case-19 | pass→pass | 7,539 | 14,611 | +94% | 1 | 1 | 0% | 1,165 | 3,076 | +164% | 0 | 0 | — |
case-20 | pass→pass | 11,728 | 9,969 | -15% | 1 | 1 | 0% | 1,939 | 4,056 | +109% | 0 | 0 | — |
case-21 | pass→pass | 12,876 | 16,782 | +30% | 1 | 1 | 0% | 2,199 | 4,093 | +86% | 0 | 0 | — |
case-22 | pass→pass | 20,294 | 21,612 | +6% | 1 | 1 | 0% | 3,907 | 6,494 | +66% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +41 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
| Model | Method | Date | Lift |
|---|---|---|---|
| gemini-3.6-flash | verified | 9/1/2026 | +39% |
Other measured skills in the registry, with their headline benchmark lift.