Claude
Skills
Sign in
Back

spring-rest

Included with Lifetime
$97 forever

Spring REST Controller patterns. Covers ResponseEntity, exception handling, validation, HATEOAS, content negotiation, and async controllers. USE WHEN: user mentions "spring rest", "@RestController", "ResponseEntity", "REST API Spring", "exception handling Spring", "@ControllerAdvice" DO NOT USE FOR: GraphQL - use `spring-graphql` skill, WebFlux reactive - use `spring-webflux` skill, HATEOAS deep dive - use `spring-hateoas` skill

Backend & APIs

What this skill does


# Spring REST Core Knowledge

## Controller Basics

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

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

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

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

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

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

    @GetMapping("/{id}")
    public EntityModel<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<EntityModel<UserDto>> getUsers(
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "10") int size) {

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

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

    @Override
    public EntityModel<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<UserDto> getUser(@PathVariable Long id) {
        UserDto user = userService.findById(id).orElseThrow();
        return assembler.toModel(user);
    }
}
```

---

## Content Negotiation

```java
/

Related in Backend & APIs