spring-rest

Spring REST Core Knowledge

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "spring-rest" with this command: npx skills add claude-dev-suite/claude-dev-suite/claude-dev-suite-claude-dev-suite-spring-rest

Spring REST Core Knowledge

Controller Basics

@RestController @RequestMapping("/api/users") @RequiredArgsConstructor public class UserController {

private final UserService userService;

@GetMapping
public ResponseEntity<List<UserDto>> getUsers(
        @RequestParam(defaultValue = "0") int page,
        @RequestParam(defaultValue = "10") int size) {
    List<UserDto> users = userService.findAll(page, size);
    return ResponseEntity.ok(users);
}

@GetMapping("/{id}")
public ResponseEntity<UserDto> getUser(@PathVariable Long id) {
    return userService.findById(id)
        .map(ResponseEntity::ok)
        .orElse(ResponseEntity.notFound().build());
}

@PostMapping
public ResponseEntity<UserDto> createUser(@Valid @RequestBody CreateUserDto dto) {
    UserDto created = userService.create(dto);
    URI location = ServletUriComponentsBuilder
        .fromCurrentRequest()
        .path("/{id}")
        .buildAndExpand(created.getId())
        .toUri();
    return ResponseEntity.created(location).body(created);
}

@PutMapping("/{id}")
public ResponseEntity<UserDto> updateUser(
        @PathVariable Long id,
        @Valid @RequestBody UpdateUserDto dto) {
    return userService.update(id, dto)
        .map(ResponseEntity::ok)
        .orElse(ResponseEntity.notFound().build());
}

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

}

ResponseEntity Patterns

// 200 OK with body return ResponseEntity.ok(data); return ResponseEntity.ok().body(data);

// 201 Created with Location header URI location = ServletUriComponentsBuilder .fromCurrentRequest() .path("/{id}") .buildAndExpand(id) .toUri(); return ResponseEntity.created(location).body(data);

// 204 No Content return ResponseEntity.noContent().build();

// 400 Bad Request return ResponseEntity.badRequest().body(error);

// 404 Not Found return ResponseEntity.notFound().build();

// Custom status return ResponseEntity.status(HttpStatus.CONFLICT).body(error);

// With headers return ResponseEntity.ok() .header("X-Custom-Header", "value") .cacheControl(CacheControl.maxAge(1, TimeUnit.HOURS)) .body(data);

Global Exception Handling

@RestControllerAdvice @Slf4j public class GlobalExceptionHandler {

@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(ResourceNotFoundException ex) {
    log.warn("Resource not found: {}", ex.getMessage());
    ErrorResponse error = ErrorResponse.builder()
        .code("NOT_FOUND")
        .message(ex.getMessage())
        .timestamp(Instant.now())
        .build();
    return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}

@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidation(MethodArgumentNotValidException ex) {
    List<FieldError> fieldErrors = ex.getBindingResult()
        .getFieldErrors()
        .stream()
        .map(err -> new FieldError(err.getField(), err.getDefaultMessage()))
        .toList();

    ErrorResponse error = ErrorResponse.builder()
        .code("VALIDATION_ERROR")
        .message("Validation failed")
        .errors(fieldErrors)
        .timestamp(Instant.now())
        .build();
    return ResponseEntity.badRequest().body(error);
}

@ExceptionHandler(DataIntegrityViolationException.class)
public ResponseEntity<ErrorResponse> handleConflict(DataIntegrityViolationException ex) {
    ErrorResponse error = ErrorResponse.builder()
        .code("CONFLICT")
        .message("Data integrity violation")
        .timestamp(Instant.now())
        .build();
    return ResponseEntity.status(HttpStatus.CONFLICT).body(error);
}

@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGeneric(Exception ex) {
    log.error("Unhandled exception", ex);
    ErrorResponse error = ErrorResponse.builder()
        .code("INTERNAL_ERROR")
        .message("An unexpected error occurred")
        .timestamp(Instant.now())
        .build();
    return ResponseEntity.internalServerError().body(error);
}

}

@Data @Builder public class ErrorResponse { private String code; private String message; private List<FieldError> errors; private Instant timestamp; }

Validation

// DTO with validation @Data public class CreateUserDto { @NotBlank(message = "Name is required") @Size(min = 2, max = 100, message = "Name must be 2-100 characters") private String name;

@NotBlank(message = "Email is required")
@Email(message = "Invalid email format")
private String email;

@NotNull(message = "Role is required")
private Role role;

@Pattern(regexp = "^\\+?[1-9]\\d{1,14}$", message = "Invalid phone number")
private String phone;

}

// Controller with validation @PostMapping public ResponseEntity<UserDto> createUser(@Valid @RequestBody CreateUserDto dto) { // Validation happens automatically return ResponseEntity.ok(userService.create(dto)); }

// Validation groups public interface OnCreate {} public interface OnUpdate {}

@Data public class UserDto { @Null(groups = OnCreate.class) @NotNull(groups = OnUpdate.class) private Long id;

@NotBlank(groups = {OnCreate.class, OnUpdate.class})
private String name;

}

@PostMapping public ResponseEntity<UserDto> create( @Validated(OnCreate.class) @RequestBody UserDto dto) { return ResponseEntity.ok(userService.create(dto)); }

HATEOAS

// Add dependency // spring-boot-starter-hateoas

@RestController @RequestMapping("/api/users") public class UserController {

@GetMapping("/{id}")
public EntityModel&#x3C;UserDto> getUser(@PathVariable Long id) {
    UserDto user = userService.findById(id)
        .orElseThrow(() -> new ResourceNotFoundException("User not found"));

    return EntityModel.of(user,
        linkTo(methodOn(UserController.class).getUser(id)).withSelfRel(),
        linkTo(methodOn(UserController.class).getUsers(0, 10)).withRel("users"),
        linkTo(methodOn(OrderController.class).getUserOrders(id)).withRel("orders")
    );
}

@GetMapping
public CollectionModel&#x3C;EntityModel&#x3C;UserDto>> getUsers(
        @RequestParam(defaultValue = "0") int page,
        @RequestParam(defaultValue = "10") int size) {

    List&#x3C;EntityModel&#x3C;UserDto>> users = userService.findAll(page, size)
        .stream()
        .map(user -> EntityModel.of(user,
            linkTo(methodOn(UserController.class).getUser(user.getId())).withSelfRel()))
        .toList();

    return CollectionModel.of(users,
        linkTo(methodOn(UserController.class).getUsers(page, size)).withSelfRel());
}

}

RepresentationModelAssembler

@Component public class UserModelAssembler implements RepresentationModelAssembler<UserDto, EntityModel<UserDto>> {

@Override
public EntityModel&#x3C;UserDto> toModel(UserDto user) {
    return EntityModel.of(user,
        linkTo(methodOn(UserController.class).getUser(user.getId())).withSelfRel(),
        linkTo(methodOn(UserController.class).getUsers(0, 10)).withRel("users"));
}

}

// Usage in controller @RestController @RequiredArgsConstructor public class UserController {

private final UserModelAssembler assembler;

@GetMapping("/{id}")
public EntityModel&#x3C;UserDto> getUser(@PathVariable Long id) {
    UserDto user = userService.findById(id).orElseThrow();
    return assembler.toModel(user);
}

}

Content Negotiation

// Controller supporting multiple formats @GetMapping(value = "/{id}", produces = { MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE }) public ResponseEntity<UserDto> getUser(@PathVariable Long id) { return ResponseEntity.ok(userService.findById(id).orElseThrow()); }

// Configuration @Configuration public class WebConfig implements WebMvcConfigurer {

@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
    configurer
        .favorParameter(true)
        .parameterName("format")
        .defaultContentType(MediaType.APPLICATION_JSON)
        .mediaType("json", MediaType.APPLICATION_JSON)
        .mediaType("xml", MediaType.APPLICATION_XML);
}

}

API Versioning

URL Versioning

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

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

Header Versioning

@GetMapping(value = "/users", headers = "X-API-Version=1") public List<UserV1Dto> getUsersV1() { }

@GetMapping(value = "/users", headers = "X-API-Version=2") public List<UserV2Dto> getUsersV2() { }

Media Type Versioning

@GetMapping(value = "/users", produces = "application/vnd.company.v1+json") public List<UserV1Dto> getUsersV1() { }

@GetMapping(value = "/users", produces = "application/vnd.company.v2+json") public List<UserV2Dto> getUsersV2() { }

Async Controllers

@GetMapping("/async/{id}") public CompletableFuture<ResponseEntity<UserDto>> getAsync(@PathVariable Long id) { return userService.findByIdAsync(id) .thenApply(user -> user .map(ResponseEntity::ok) .orElse(ResponseEntity.notFound().build())); }

@GetMapping("/deferred/{id}") public DeferredResult<ResponseEntity<UserDto>> getDeferred(@PathVariable Long id) { DeferredResult<ResponseEntity<UserDto>> result = new DeferredResult<>();

userService.findByIdAsync(id)
    .thenAccept(user -> result.setResult(
        user.map(ResponseEntity::ok)
           .orElse(ResponseEntity.notFound().build())));

return result;

}

@GetMapping("/callable/{id}") public Callable<ResponseEntity<UserDto>> getCallable(@PathVariable Long id) { return () -> userService.findById(id) .map(ResponseEntity::ok) .orElse(ResponseEntity.notFound().build()); }

Streaming Response

// Server-Sent Events @GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flux<ServerSentEvent<String>> streamEvents() { return Flux.interval(Duration.ofSeconds(1)) .map(seq -> ServerSentEvent.<String>builder() .id(String.valueOf(seq)) .event("message") .data("Event " + seq) .build()); }

// Large file download @GetMapping("/download/{id}") public ResponseEntity<StreamingResponseBody> downloadFile(@PathVariable Long id) { return ResponseEntity.ok() .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=file.csv") .contentType(MediaType.APPLICATION_OCTET_STREAM) .body(outputStream -> { // Write data to outputStream fileService.writeToStream(id, outputStream); }); }

Production Readiness

Request/Response Logging

@Component public class LoggingFilter extends OncePerRequestFilter {

@Override
protected void doFilterInternal(HttpServletRequest request,
        HttpServletResponse response, FilterChain filterChain)
        throws ServletException, IOException {

    long start = System.currentTimeMillis();
    String requestId = UUID.randomUUID().toString();
    MDC.put("requestId", requestId);

    try {
        filterChain.doFilter(request, response);
    } finally {
        long duration = System.currentTimeMillis() - start;
        log.info("{} {} {} {}ms",
            request.getMethod(),
            request.getRequestURI(),
            response.getStatus(),
            duration);
        MDC.clear();
    }
}

}

Checklist

  • ResponseEntity for all responses

  • Global exception handler

  • Input validation with @Valid

  • Proper HTTP status codes

  • Location header for 201 Created

  • HATEOAS links where appropriate

  • Content negotiation configured

  • API versioning strategy

  • Request logging with timing

  • Request ID tracking

When NOT to Use This Skill

  • GraphQL APIs - Use spring-graphql skill

  • Reactive/WebFlux - Use spring-webflux skill

  • HATEOAS deep dive - Use spring-hateoas skill

  • gRPC - Use gRPC-specific patterns

Anti-Patterns

Anti-Pattern Problem Solution

Returning entities Tight coupling Use DTOs

No exception handler Inconsistent errors Add @ControllerAdvice

Missing @Valid No input validation Add @Valid annotation

Wrong HTTP status Confuses clients Use proper status codes

Missing Location header 201 incomplete Add Location for POST

Quick Troubleshooting

Problem Diagnostic Fix

415 Unsupported Media Check Content-Type Add produces/consumes

Validation not working Check @Valid Add @Valid to @RequestBody

Exception not caught Check advice Verify @RestControllerAdvice

Wrong JSON field names Check serialization Configure Jackson

CORS errors Check CORS config Add @CrossOrigin or config

Reference Documentation

  • Spring MVC Reference

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

cron-scheduling

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

token-optimization

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

webrtc

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

react-19

No summary provided by upstream source.

Repository SourceNeeds Review