Skip to content
Back to skills

Rest Api Conventions

ASecurity

Use when generating REST controllers, DTOs, success response contracts, pagination, HTTP status mapping, or API versioning. For RFC 9457 exception and error response formatting, use problem-details-rfc9457 unless the project explicitly requires a legacy error envelope.

  • 272 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added September 6, 2026
ai-agentsgojavaspringapi

Works with

  • cli
  • api

Security analysis

A100/100

Pro scans all 6 files and shows the line behind each finding

Scanned September 22, 2026

npx -y skills add rrezartprebreza/spring-boot-skills --skill rest-api-conventions --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Rest Api Conventions?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Rest Api Conventions
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/rrezartprebreza-rest-api-conventions-spring-boot-skills/badge)](https://www.skillsdirectory.com/skills/rrezartprebreza-rest-api-conventions-spring-boot-skills)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: rest-api-conventions
description: >
  Use when generating REST controllers, DTOs, success response contracts, pagination, HTTP status
  mapping, or API versioning. For RFC 9457 exception and error response formatting, use
  problem-details-rfc9457 unless the project explicitly requires a legacy error envelope.
---

# REST API Conventions

## Project Contract

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"
}
```

## ApiResponse Wrapper

```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) {}
```

## HTTP Status Mapping

| 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 |

## URL Conventions

- **Plural nouns** for resources: `/orders`, `/users`, `/products`
- **Kebab-case** for multi-word: `/order-items`, not `/orderItems`
- **Versioning in path**: `/api/v1/orders` — route with native API versioning (below), don't duplicate controllers per version
- **Nested resources** max 2 levels: `/orders/{id}/items` ✅, `/orders/{id}/items/{itemId}/notes` ❌ — flatten to `/order-item-notes/{id}`
- **IDs**: preserve the existing ID type. UUIDs are an option, not an authorization mechanism.

```
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
```

## API Versioning (Native in Boot 4)

Spring Boot 4 / Framework 7 route requests by API version natively — never hand-roll it
with duplicated `V1`/`V2` controllers, custom `RequestCondition`s, or header `if` checks.

Pick ONE resolution strategy per API (path segment, header, query param, or media-type param):

```yaml
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:

```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`).

- **Deprecate old versions** with `StandardApiVersionDeprecationHandler` (register via
  `WebMvcConfigurer#configureApiVersioning(ApiVersionConfigurer)`) — it emits RFC 9745
  `Deprecation`/`Sunset` and `Link` response headers
- **Clients**: `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

## Pagination

```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:

```yaml
spring:
  data:
    web:
      pageable:
        default-page-size: 20
        max-page-size: 100   # requests above this are silently clamped
```

The compiled [PageResponse](templates/PageResponse.java) 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.

## Legacy Envelope Exception Handler

```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"));
    }
}
```

## Gotchas
- Agent imposes a new envelope - preserve the existing success contract.
- Agent invents competing error handlers - use the project's established error contract.
- Agent puts exception handlers in controllers — always use `@RestControllerAdvice`
- Agent changes ID types while adding an endpoint - retain the existing ID scheme.
- Agent accepts unbounded `Pageable` — set `spring.data.web.pageable.max-page-size` or one request can pull the whole table
- Agent serializes PageImpl directly - map entities to DTOs, then wrap in PageResponse.
- Agent hand-rolls versioning with duplicated `/v1`/`/v2` controllers — Boot 4 has native API versioning: `version` attribute on mappings + `spring.mvc.apiversion.*`
- Agent adds `spring-boot-starter-web` — renamed `spring-boot-starter-webmvc` in Boot 4 (MockMvc tests: `spring-boot-starter-webmvc-test`)
- Agent uses `@JsonComponent` or `Jackson2ObjectMapperBuilderCustomizer` to tune serialization — Jackson 3 renames: `@JacksonComponent`, `JsonMapperBuilderCustomizer`; declare `JsonMapper` beans, not generic `ObjectMapper`
- Agent tests controllers with bare `@SpringBootTest` expecting MockMvc — Boot 4 no longer auto-provides it; add `@AutoConfigureMockMvc` (or the new `RestTestClient` via `@AutoConfigureRestTestClient`)

Files in this skill

  • SKILL.md7.4 KB
  • agents/openai.yaml202 B
  • examples/bad-controller.java1.3 KB
  • examples/good-controller.java1.1 KB
  • templates/ApiResponse.java925 B
  • templates/GlobalExceptionHandler.java1.6 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.