spring-rest-api

RESTful API design with Spring Boot including OpenAPI/Swagger documentation, content negotiation, CORS, pagination, HATEOAS, and API versioning patterns.

16 stars

Best use case

spring-rest-api is best used when you need a repeatable AI agent workflow instead of a one-off prompt.

RESTful API design with Spring Boot including OpenAPI/Swagger documentation, content negotiation, CORS, pagination, HATEOAS, and API versioning patterns.

Teams using spring-rest-api should expect a more consistent output, faster repeated execution, less prompt rewriting, better workflow continuity with your supporting tools.

When to use this skill

  • You want a reusable workflow that can be run more than once with consistent structure.
  • You already have the supporting tools or dependencies needed by this skill.

When not to use this skill

  • You only need a quick one-off answer and do not need a reusable workflow.
  • You cannot install or maintain the underlying files, dependencies, or repository context.

Installation

Claude Code / Cursor / Codex

$curl -o ~/.claude/skills/spring-rest-api/SKILL.md --create-dirs "https://raw.githubusercontent.com/diegosouzapw/awesome-omni-skill/main/skills/development/spring-rest-api/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/spring-rest-api/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How spring-rest-api Compares

Feature / Agentspring-rest-apiStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/A

Frequently Asked Questions

What does this skill do?

RESTful API design with Spring Boot including OpenAPI/Swagger documentation, content negotiation, CORS, pagination, HATEOAS, and API versioning patterns.

Where can I find the source code?

You can find the source code on GitHub using the link provided at the top of the page.

SKILL.md Source

# Spring REST API

Build production-ready RESTful APIs with Spring Boot following industry best practices for design, documentation, error handling, and security.

## When to Use

- Building HTTP APIs consumed by web/mobile frontends, third-party integrations, or microservices
- Exposing CRUD operations over domain entities with pagination, sorting, and filtering
- Creating public or internal APIs that require OpenAPI/Swagger documentation
- Projects needing standardized error responses (RFC 7807 Problem Detail)
- APIs requiring content negotiation (JSON, XML), CORS, or versioning

## When NOT to Use

- Real-time bidirectional communication -- use WebSockets or SSE instead
- File-heavy streaming endpoints -- consider Spring WebFlux or dedicated file services
- GraphQL APIs -- use `spring-boot-starter-graphql`
- gRPC services -- use `grpc-spring-boot-starter`
- Simple internal method calls between co-located services -- direct invocation is simpler

## Dependencies

### Maven

```xml
<dependencies>
    <!-- Core web starter -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- OpenAPI / Swagger UI -->
    <dependency>
        <groupId>org.springdoc</groupId>
        <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
        <version>2.8.4</version>
    </dependency>

    <!-- HATEOAS (optional, for hypermedia-driven APIs) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-hateoas</artifactId>
    </dependency>

    <!-- Validation -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>

    <!-- XML content negotiation (optional) -->
    <dependency>
        <groupId>com.fasterxml.jackson.dataformat</groupId>
        <artifactId>jackson-dataformat-xml</artifactId>
    </dependency>
</dependencies>
```

### Gradle

```groovy
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.4'
    implementation 'org.springframework.boot:spring-boot-starter-hateoas'
    implementation 'org.springframework.boot:spring-boot-starter-validation'
    implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml' // optional
}
```

## REST Conventions

### URL Patterns

Follow resource-oriented URL design. URLs represent nouns (resources), not verbs (actions).

| Pattern | Purpose | Example |
|---|---|---|
| `GET /api/v1/users` | List collection | Fetch all users (paginated) |
| `GET /api/v1/users/{id}` | Get single resource | Fetch user by ID |
| `POST /api/v1/users` | Create resource | Create a new user |
| `PUT /api/v1/users/{id}` | Full replace | Replace entire user |
| `PATCH /api/v1/users/{id}` | Partial update | Update specific fields |
| `DELETE /api/v1/users/{id}` | Delete resource | Remove user |
| `GET /api/v1/users/{id}/orders` | Sub-resource collection | User's orders |
| `GET /api/v1/users/{id}/orders/{orderId}` | Sub-resource item | Specific order |

Rules:
- Use plural nouns: `/users` not `/user`
- Use kebab-case for multi-word resources: `/order-items` not `/orderItems`
- Nest sub-resources at most one level deep
- Use query parameters for filtering, sorting, and pagination
- Prefix all API routes with `/api/v1` (or versioned equivalent)

### HTTP Methods

| Method | Idempotent | Safe | Request Body | Typical Use |
|---|---|---|---|---|
| `GET` | Yes | Yes | No | Retrieve resource(s) |
| `POST` | No | No | Yes | Create resource |
| `PUT` | Yes | No | Yes | Full replacement |
| `PATCH` | No | No | Yes | Partial update |
| `DELETE` | Yes | No | No | Remove resource |
| `HEAD` | Yes | Yes | No | Check existence |
| `OPTIONS` | Yes | Yes | No | CORS preflight |

### HTTP Status Codes

| Code | When to Use |
|---|---|
| `200 OK` | Successful GET, PUT, PATCH, or DELETE returning body |
| `201 Created` | Successful POST creating a resource (include `Location` header) |
| `204 No Content` | Successful DELETE or PUT/PATCH with no response body |
| `400 Bad Request` | Malformed request syntax, invalid JSON |
| `401 Unauthorized` | Missing or invalid authentication |
| `403 Forbidden` | Authenticated but lacking permission |
| `404 Not Found` | Resource does not exist |
| `409 Conflict` | State conflict (duplicate email, concurrent edit) |
| `422 Unprocessable Entity` | Validation errors on well-formed request |
| `500 Internal Server Error` | Unhandled server exception |

See `references/http-status-codes.md` for complete reference with Spring examples.

## Controller Patterns

### Basic REST Controller

```java
@RestController
@RequestMapping("/api/v1/users")
@Tag(name = "Users", description = "User management operations")
@RequiredArgsConstructor
public class UserController {

    private final UserService userService;

    @GetMapping
    public ResponseEntity<PageResponse<UserResponse>> list(
            @ParameterObject Pageable pageable,
            @RequestParam(required = false) String search) {
        Page<UserResponse> page = userService.findAll(search, pageable);
        return ResponseEntity.ok(PageResponse.of(page));
    }

    @GetMapping("/{id}")
    public ResponseEntity<UserResponse> getById(@PathVariable Long id) {
        return ResponseEntity.ok(userService.findById(id));
    }

    @PostMapping
    public ResponseEntity<UserResponse> create(
            @Valid @RequestBody CreateUserRequest request) {
        UserResponse created = userService.create(request);
        URI location = ServletUriComponentsBuilder.fromCurrentRequest()
                .path("/{id}").buildAndExpand(created.id()).toUri();
        return ResponseEntity.created(location).body(created);
    }

    @PutMapping("/{id}")
    public ResponseEntity<UserResponse> replace(
            @PathVariable Long id,
            @Valid @RequestBody UpdateUserRequest request) {
        return ResponseEntity.ok(userService.replace(id, request));
    }

    @PatchMapping("/{id}")
    public ResponseEntity<UserResponse> patch(
            @PathVariable Long id,
            @Valid @RequestBody PatchUserRequest request) {
        return ResponseEntity.ok(userService.patch(id, request));
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<Void> delete(@PathVariable Long id) {
        userService.delete(id);
        return ResponseEntity.noContent().build();
    }
}
```

### Key Annotations

| Annotation | Purpose |
|---|---|
| `@RestController` | Combines `@Controller` + `@ResponseBody` |
| `@RequestMapping("/path")` | Base path for all endpoints in controller |
| `@GetMapping` | Handle GET requests |
| `@PostMapping` | Handle POST requests |
| `@PutMapping` | Handle PUT requests |
| `@PatchMapping` | Handle PATCH requests |
| `@DeleteMapping` | Handle DELETE requests |
| `@PathVariable` | Extract value from URL path segment |
| `@RequestParam` | Extract query parameter |
| `@RequestBody` | Deserialize request body |
| `@Valid` | Trigger Bean Validation on parameter |
| `@ResponseStatus` | Set default HTTP status for handler |
| `@ParameterObject` | Expand Pageable into individual query params in OpenAPI |

## Request/Response DTOs

Use Java records for immutable, concise DTOs. Separate request and response representations.

```java
// Request DTO -- what the client sends
public record CreateUserRequest(
        @NotBlank @Size(max = 100)
        @Schema(description = "User's full name", example = "Jane Doe")
        String name,

        @NotBlank @Email @Size(max = 255)
        @Schema(description = "Unique email address", example = "jane@example.com")
        String email,

        @NotBlank @Size(min = 8, max = 72)
        @Schema(description = "Password (8-72 characters)", example = "s3cur3P@ss!")
        String password
) {}

// Response DTO -- what the client receives (never expose passwords, internal IDs, etc.)
public record UserResponse(
        @Schema(description = "Unique identifier", example = "42")
        Long id,

        @Schema(description = "User's full name", example = "Jane Doe")
        String name,

        @Schema(description = "Email address", example = "jane@example.com")
        String email,

        @Schema(description = "Account status", example = "ACTIVE")
        String status,

        @Schema(description = "Account creation timestamp")
        Instant createdAt
) {}
```

Rules:
- Never reuse the same DTO for create, update, and response
- Never expose sensitive fields (passwords, internal flags) in responses
- Use `@Schema` on every field for OpenAPI documentation
- Apply Bean Validation annotations on request DTOs
- Use `Instant` for timestamps, let Jackson serialize to ISO-8601

## Pagination

### Using Spring's Pageable

Spring Data provides `Pageable` and `Page` out of the box. Clients pass `page`, `size`, and `sort` query parameters.

```
GET /api/v1/users?page=0&size=20&sort=name,asc&sort=createdAt,desc
```

### Custom PageResponse Wrapper

Wrap Spring's `Page` in a consistent, API-friendly format:

```java
public record PageResponse<T>(
        List<T> content,
        PageMetadata metadata
) {
    public record PageMetadata(
            int page,
            int size,
            long totalElements,
            int totalPages,
            boolean first,
            boolean last
    ) {}

    public static <T> PageResponse<T> of(Page<T> page) {
        return new PageResponse<>(
                page.getContent(),
                new PageMetadata(
                        page.getNumber(),
                        page.getSize(),
                        page.getTotalElements(),
                        page.getTotalPages(),
                        page.isFirst(),
                        page.isLast()
                )
        );
    }
}
```

### Pagination Configuration

```yaml
spring:
  data:
    web:
      pageable:
        default-page-size: 20
        max-page-size: 100
        one-indexed-parameters: false
```

## Sorting

Accept `sort` as a query parameter. Spring Data parses `sort=field,direction` automatically.

```java
@GetMapping
public ResponseEntity<PageResponse<UserResponse>> list(
        @ParameterObject Pageable pageable) {
    // pageable.getSort() contains parsed Sort object
    Page<UserResponse> page = userService.findAll(pageable);
    return ResponseEntity.ok(PageResponse.of(page));
}
```

To restrict sortable fields, validate in the service layer:

```java
private static final Set<String> ALLOWED_SORT_FIELDS = Set.of("name", "email", "createdAt");

private Pageable validateSort(Pageable pageable) {
    for (Sort.Order order : pageable.getSort()) {
        if (!ALLOWED_SORT_FIELDS.contains(order.getProperty())) {
            throw new InvalidSortFieldException(order.getProperty(), ALLOWED_SORT_FIELDS);
        }
    }
    return pageable;
}
```

## Filtering

### Simple Query Parameters

```java
@GetMapping
public ResponseEntity<PageResponse<UserResponse>> list(
        @RequestParam(required = false) String search,
        @RequestParam(required = false) UserStatus status,
        @RequestParam(required = false) @DateTimeFormat(iso = ISO.DATE) LocalDate createdAfter,
        @ParameterObject Pageable pageable) {
    Page<UserResponse> page = userService.findAll(search, status, createdAfter, pageable);
    return ResponseEntity.ok(PageResponse.of(page));
}
```

### JPA Specification Pattern

For complex filtering, use Spring Data JPA Specifications:

```java
public class UserSpecifications {

    public static Specification<User> hasStatus(UserStatus status) {
        return (root, query, cb) ->
                status == null ? null : cb.equal(root.get("status"), status);
    }

    public static Specification<User> nameLike(String search) {
        return (root, query, cb) ->
                search == null ? null : cb.like(cb.lower(root.get("name")),
                        "%" + search.toLowerCase() + "%");
    }

    public static Specification<User> createdAfter(LocalDate date) {
        return (root, query, cb) ->
                date == null ? null : cb.greaterThanOrEqualTo(
                        root.get("createdAt"), date.atStartOfDay().toInstant(ZoneOffset.UTC));
    }
}
```

Combine in service:

```java
public Page<UserResponse> findAll(String search, UserStatus status,
                                   LocalDate createdAfter, Pageable pageable) {
    Specification<User> spec = Specification
            .where(UserSpecifications.nameLike(search))
            .and(UserSpecifications.hasStatus(status))
            .and(UserSpecifications.createdAfter(createdAfter));
    return userRepository.findAll(spec, pageable).map(userMapper::toResponse);
}
```

## CORS Configuration

### Profile-Based Configuration

```java
@Configuration
public class CorsConfig {

    @Bean
    @Profile("dev")
    public WebMvcConfigurer devCorsConfigurer() {
        return new WebMvcConfigurer() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry.addMapping("/api/**")
                        .allowedOriginPatterns("*")
                        .allowedMethods("*")
                        .allowedHeaders("*")
                        .allowCredentials(true)
                        .maxAge(3600);
            }
        };
    }

    @Bean
    @Profile("prod")
    public WebMvcConfigurer prodCorsConfigurer(
            @Value("${app.cors.allowed-origins}") List<String> allowedOrigins) {
        return new WebMvcConfigurer() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry.addMapping("/api/**")
                        .allowedOrigins(allowedOrigins.toArray(String[]::new))
                        .allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")
                        .allowedHeaders("Authorization", "Content-Type", "Accept")
                        .exposedHeaders("Location", "X-Total-Count")
                        .allowCredentials(true)
                        .maxAge(3600);
            }
        };
    }
}
```

## Content Negotiation

Spring Boot supports JSON by default. Add XML support with `jackson-dataformat-xml`.

```yaml
spring:
  mvc:
    contentnegotiation:
      favor-parameter: false
      favor-path-extension: false
```

Clients use the `Accept` header:

```
GET /api/v1/users
Accept: application/json    # JSON response (default)
Accept: application/xml     # XML response
```

To restrict to JSON only on a specific controller:

```java
@RestController
@RequestMapping(path = "/api/v1/users", produces = MediaType.APPLICATION_JSON_VALUE)
public class UserController { ... }
```

## API Versioning

### Strategy 1: URL Path (Recommended for most projects)

```java
@RestController
@RequestMapping("/api/v1/users")
public class UserControllerV1 { ... }

@RestController
@RequestMapping("/api/v2/users")
public class UserControllerV2 { ... }
```

### Strategy 2: Custom Request Header

```java
@GetMapping(headers = "X-API-Version=1")
public ResponseEntity<UserResponseV1> getUserV1(@PathVariable Long id) { ... }

@GetMapping(headers = "X-API-Version=2")
public ResponseEntity<UserResponseV2> getUserV2(@PathVariable Long id) { ... }
```

### Strategy 3: Media Type (Accept Header)

```java
@GetMapping(produces = "application/vnd.myapp.v1+json")
public ResponseEntity<UserResponseV1> getUserV1(@PathVariable Long id) { ... }

@GetMapping(produces = "application/vnd.myapp.v2+json")
public ResponseEntity<UserResponseV2> getUserV2(@PathVariable Long id) { ... }
```

### Recommendation

Use URL path versioning (`/api/v1/`) for simplicity and discoverability. Reserve header/media-type versioning for APIs with strict backward-compatibility contracts.

## OpenAPI / Swagger

### Configuration

```java
@Configuration
public class OpenApiConfig {

    @Bean
    public OpenAPI customOpenAPI() {
        return new OpenAPI()
                .info(new Info()
                        .title("My Application API")
                        .version("1.0.0")
                        .description("RESTful API documentation")
                        .contact(new Contact()
                                .name("API Support")
                                .email("support@example.com")))
                .addSecurityItem(new SecurityRequirement().addList("Bearer"))
                .components(new Components()
                        .addSecuritySchemes("Bearer",
                                new SecurityScheme()
                                        .type(SecurityScheme.Type.HTTP)
                                        .scheme("bearer")
                                        .bearerFormat("JWT")));
    }
}
```

### Key Annotations

```java
@Operation(summary = "Get user by ID", description = "Retrieves a single user by their unique identifier")
@ApiResponse(responseCode = "200", description = "User found")
@ApiResponse(responseCode = "404", description = "User not found",
        content = @Content(schema = @Schema(implementation = ProblemDetail.class)))
@GetMapping("/{id}")
public ResponseEntity<UserResponse> getById(@PathVariable Long id) { ... }
```

See `references/openapi-annotations.md` for complete annotation reference.

### Swagger UI Access

After starting the application:
- Swagger UI: `http://localhost:8080/swagger-ui.html`
- OpenAPI JSON: `http://localhost:8080/v3/api-docs`
- OpenAPI YAML: `http://localhost:8080/v3/api-docs.yaml`

## Error Responses -- ProblemDetail (RFC 7807)

Spring 6+ natively supports RFC 7807 `ProblemDetail` responses.

### Enable globally

```yaml
spring:
  mvc:
    problemdetails:
      enabled: true
```

### Custom Exception Handler

```java
@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {

    @ExceptionHandler(ResourceNotFoundException.class)
    public ProblemDetail handleNotFound(ResourceNotFoundException ex) {
        ProblemDetail problem = ProblemDetail.forStatusAndDetail(
                HttpStatus.NOT_FOUND, ex.getMessage());
        problem.setTitle("Resource Not Found");
        problem.setProperty("resource", ex.getResourceName());
        problem.setProperty("identifier", ex.getIdentifier());
        return problem;
    }
}
```

### Response Format

```json
{
    "type": "about:blank",
    "title": "Resource Not Found",
    "status": 404,
    "detail": "User with id 42 not found",
    "instance": "/api/v1/users/42",
    "resource": "User",
    "identifier": "42"
}
```

## HATEOAS

Add hypermedia links to responses for API discoverability.

```java
@GetMapping("/{id}")
public EntityModel<UserResponse> getById(@PathVariable Long id) {
    UserResponse user = userService.findById(id);
    return EntityModel.of(user,
            linkTo(methodOn(UserController.class).getById(id)).withSelfRel(),
            linkTo(methodOn(UserController.class).list(Pageable.unpaged(), null))
                    .withRel("users"),
            linkTo(methodOn(OrderController.class).listByUser(id, Pageable.unpaged()))
                    .withRel("orders"));
}
```

Response:

```json
{
    "id": 42,
    "name": "Jane Doe",
    "email": "jane@example.com",
    "_links": {
        "self": { "href": "/api/v1/users/42" },
        "users": { "href": "/api/v1/users" },
        "orders": { "href": "/api/v1/users/42/orders" }
    }
}
```

Use HATEOAS when:
- Building public APIs consumed by diverse, evolving clients
- API discoverability is a requirement
- You want clients to navigate the API without hardcoded URLs

Skip HATEOAS when:
- Internal APIs with known consumers (e.g., your own SPA)
- Simplicity is more important than discoverability

## Code Quality Checklist

Before submitting a REST API for review, verify:

- [ ] All endpoints return appropriate HTTP status codes (201 for POST, 204 for DELETE, etc.)
- [ ] POST endpoints return `Location` header pointing to the created resource
- [ ] Request DTOs have Bean Validation annotations (`@NotBlank`, `@Size`, `@Email`, etc.)
- [ ] Response DTOs never expose sensitive fields (passwords, tokens, internal flags)
- [ ] Separate DTOs for create, update, and response -- never reuse a single DTO
- [ ] Pagination is applied to all collection endpoints (never return unbounded lists)
- [ ] Sort fields are validated against an allow-list
- [ ] All endpoints have OpenAPI annotations (`@Operation`, `@ApiResponse`, `@Schema`)
- [ ] Error responses use ProblemDetail (RFC 7807) format
- [ ] CORS is configured per environment (permissive dev, restrictive prod)
- [ ] No business logic in controllers -- delegate to service layer
- [ ] `@Valid` is present on all `@RequestBody` parameters
- [ ] Path variables and query parameters have clear, documented names
- [ ] API version prefix is consistent across all controllers (`/api/v1/`)
- [ ] Integration tests cover happy path, validation errors, not-found, and conflict scenarios

Related Skills

springboot-architecture-analyzer

16
from diegosouzapw/awesome-omni-skill

系統化分析 Spring Boot 專案並生成完整的企業級架構文件,涵蓋系統概述、架構視圖、技術細節、部署策略等所有關鍵面向。

springboot-4-migration

16
from diegosouzapw/awesome-omni-skill

Comprehensive guide for migrating Spring Boot applications from 3.x to 4.0, focusing on Gradle Kotlin DSL and version catalogs Triggers on: **/*.java, **/*.kt, **/build.gradle.kts, **/build.gradle, **/settings.gradle.kts, **/gradle/libs.versions.toml, **/*.properties, **/*.yml, **/*.yaml

spring-reactive

16
from diegosouzapw/awesome-omni-skill

Build reactive applications - WebFlux, Mono/Flux, R2DBC, backpressure, reactive streams

spring-boot-testing

16
from diegosouzapw/awesome-omni-skill

No description provided.

spring-boot-performance

16
from diegosouzapw/awesome-omni-skill

Guide for optimizing Spring Boot application performance including caching, pagination, async processing, and JPA optimization. Use this when addressing performance issues or implementing high-traffic features.

spring-boot-migrator

16
from diegosouzapw/awesome-omni-skill

Automated migration of Spring Boot 2.x to 3.x with JDK 8 to 21 upgrade using OpenRewrite. Use when asked to migrate, upgrade, or modernize Spring Boot applications, upgrade from JDK 8/11/17 to JDK 21, migrate from javax to jakarta namespace, or when facing Spring Boot 2 EOL migration tasks. Handles Maven projects with standard or custom parent POMs.

spring-boot-engineer

16
from diegosouzapw/awesome-omni-skill

Use when building Spring Boot 3.x applications, microservices, or reactive Java applications. Invoke for Spring Data JPA, Spring Security 6, WebFlux, Spring Cloud integration.

shorten-rest-automation

16
from diegosouzapw/awesome-omni-skill

Automate Shorten Rest tasks via Rube MCP (Composio). Always search tools first for current schemas.

REST Assured API Testing

16
from diegosouzapw/awesome-omni-skill

Java REST API testing with REST Assured including JSON schema validation

rest-api

16
from diegosouzapw/awesome-omni-skill

Expert guidance for designing and building RESTful APIs following industry standards. Use when creating APIs, reviewing designs, implementing endpoints, or troubleshooting API issues.

openapi-to-application-java-spring-boot-openapi-to-application-c

16
from diegosouzapw/awesome-omni-skill

Generate a complete, production-ready application from an OpenAPI specification Use when: the task directly matches openapi to application code responsibilities within plugin openapi-to-application-java-spring-boot. Do not use when: a more specific framework or task-focused skill is clearly a better match.

onesignal-rest-api-automation

16
from diegosouzapw/awesome-omni-skill

Automate OneSignal tasks via Rube MCP (Composio): push notifications, segments, templates, and messaging. Always search tools first for current schemas.