Agent skill

spring-boot-rest-controller

Guide for creating RESTful controllers with proper request/response handling, validation, and documentation. Use this when implementing new API endpoints or refactoring existing controllers.

Stars 163
Forks 31

Install this agent skill to your Project

npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/data/spring-boot-rest-controller

SKILL.md

Spring Boot REST Controller Best Practices

Follow these practices for implementing RESTful APIs.

Controller Structure

java
@RestController
@RequestMapping("/api/appointments")
@Tag(name = "Appointments", description = "Appointment management endpoints")
public class AppointmentController {

    private final AppointmentService appointmentService;

    // Constructor injection
    public AppointmentController(AppointmentService appointmentService) {
        this.appointmentService = appointmentService;
    }

    // Endpoints follow CRUD order: Create, Read, Update, Delete
}

HTTP Methods and Status Codes

java
@RestController
@RequestMapping("/api/appointments")
public class AppointmentController {

    // GET all - Returns 200 OK
    @GetMapping
    public List<AppointmentResponseDTO> getAll() {
        return appointmentService.findAll();
    }

    // GET by ID - Returns 200 OK or 404 Not Found
    @GetMapping("/{id}")
    public AppointmentResponseDTO getById(@PathVariable Long id) {
        return appointmentService.findById(id);
    }

    // POST create - Returns 201 Created
    @PostMapping
    public ResponseEntity<AppointmentResponseDTO> create(
            @Valid @RequestBody AppointmentRequestDTO request) {
        AppointmentResponseDTO created = appointmentService.create(request);
        URI location = ServletUriComponentsBuilder
            .fromCurrentRequest()
            .path("/{id}")
            .buildAndExpand(created.getId())
            .toUri();
        return ResponseEntity.created(location).body(created);
    }

    // PUT update - Returns 200 OK or 404 Not Found
    @PutMapping("/{id}")
    public AppointmentResponseDTO update(
            @PathVariable Long id,
            @Valid @RequestBody AppointmentRequestDTO request) {
        return appointmentService.update(id, request);
    }

    // PATCH partial update - Returns 200 OK or 404 Not Found
    @PatchMapping("/{id}/status")
    public AppointmentResponseDTO updateStatus(
            @PathVariable Long id,
            @RequestParam String status) {
        return appointmentService.updateStatus(id, status);
    }

    // DELETE - Returns 204 No Content
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> delete(@PathVariable Long id) {
        appointmentService.delete(id);
        return ResponseEntity.noContent().build();
    }
}

Request Validation

java
// Request DTO with validation
public class AppointmentRequestDTO {

    @NotNull(message = "Customer ID is required")
    private Long customerId;

    @NotNull(message = "Employee ID is required")
    private Long employeeId;

    @NotNull(message = "Service type ID is required")
    private Long serviceTypeId;

    @NotNull(message = "Appointment time is required")
    @FutureOrPresent(message = "Appointment time must be in the future")
    private LocalDateTime appointmentTime;

    @Size(max = 500, message = "Notes cannot exceed 500 characters")
    private String notes;

    // Getters and setters
}

// Controller with validation
@PostMapping
public ResponseEntity<AppointmentResponseDTO> create(
        @Valid @RequestBody AppointmentRequestDTO request) {
    // Request is automatically validated
    return ResponseEntity.status(HttpStatus.CREATED)
        .body(appointmentService.create(request));
}

Pagination and Sorting

java
@GetMapping
public Page<AppointmentResponseDTO> getAll(
        @RequestParam(defaultValue = "0") int page,
        @RequestParam(defaultValue = "20") int size,
        @RequestParam(defaultValue = "appointmentTime") String sortBy,
        @RequestParam(defaultValue = "asc") String sortDir) {
    
    Sort sort = sortDir.equalsIgnoreCase("desc") 
        ? Sort.by(sortBy).descending() 
        : Sort.by(sortBy).ascending();
    
    Pageable pageable = PageRequest.of(page, size, sort);
    return appointmentService.findAll(pageable);
}

Filtering and Search

java
@GetMapping("/search")
public List<AppointmentResponseDTO> search(
        @RequestParam(required = false) Long customerId,
        @RequestParam(required = false) Long employeeId,
        @RequestParam(required = false) String status,
        @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date) {
    
    return appointmentService.search(customerId, employeeId, status, date);
}

Response DTOs

java
// Response DTO - what API returns
public class AppointmentResponseDTO {
    private Long id;
    private CustomerSummaryDTO customer;
    private EmployeeSummaryDTO employee;
    private ServiceTypeSummaryDTO serviceType;
    private LocalDateTime appointmentTime;
    private String status;
    private String notes;
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;

    // Getters and setters
}

// Nested summary DTO - avoid circular references
public class CustomerSummaryDTO {
    private Long id;
    private String name;
    private String phone;
    
    // Getters and setters
}

OpenAPI Documentation

java
@RestController
@RequestMapping("/api/appointments")
@Tag(name = "Appointments", description = "Appointment management endpoints")
public class AppointmentController {

    @Operation(summary = "Get appointment by ID", description = "Returns a single appointment")
    @ApiResponses(value = {
        @ApiResponse(responseCode = "200", description = "Found the appointment",
            content = @Content(schema = @Schema(implementation = AppointmentResponseDTO.class))),
        @ApiResponse(responseCode = "404", description = "Appointment not found",
            content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
    })
    @GetMapping("/{id}")
    public AppointmentResponseDTO getById(
            @Parameter(description = "ID of appointment to return") @PathVariable Long id) {
        return appointmentService.findById(id);
    }

    @Operation(summary = "Create new appointment")
    @ApiResponses(value = {
        @ApiResponse(responseCode = "201", description = "Appointment created"),
        @ApiResponse(responseCode = "400", description = "Invalid input"),
        @ApiResponse(responseCode = "404", description = "Referenced entity not found")
    })
    @PostMapping
    public ResponseEntity<AppointmentResponseDTO> create(
            @io.swagger.v3.oas.annotations.parameters.RequestBody(
                description = "Appointment to create",
                required = true,
                content = @Content(schema = @Schema(implementation = AppointmentRequestDTO.class)))
            @Valid @RequestBody AppointmentRequestDTO request) {
        return ResponseEntity.status(HttpStatus.CREATED)
            .body(appointmentService.create(request));
    }
}

Security Annotations

java
@RestController
@RequestMapping("/api/appointments")
public class AppointmentController {

    // Admin only
    @PreAuthorize("hasRole('ADMIN')")
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> delete(@PathVariable Long id) {
        appointmentService.delete(id);
        return ResponseEntity.noContent().build();
    }

    // Front desk and above
    @PreAuthorize("hasRole('FRONT_DESK')")
    @PostMapping
    public ResponseEntity<AppointmentResponseDTO> create(@Valid @RequestBody AppointmentRequestDTO request) {
        return ResponseEntity.status(HttpStatus.CREATED)
            .body(appointmentService.create(request));
    }

    // Self-access or higher role
    @PreAuthorize("hasRole('MANAGER') or #customerId == authentication.principal.customerId")
    @GetMapping("/customer/{customerId}")
    public List<AppointmentResponseDTO> getByCustomer(@PathVariable Long customerId) {
        return appointmentService.findByCustomerId(customerId);
    }
}

Error Handling

java
// Let GlobalExceptionHandler handle exceptions
@GetMapping("/{id}")
public AppointmentResponseDTO getById(@PathVariable Long id) {
    return appointmentService.findById(id);
    // Service throws ResourceNotFoundException if not found
    // GlobalExceptionHandler converts to 404 response
}

// Custom error response format
public class ErrorResponse {
    private String code;
    private String message;
    private LocalDateTime timestamp;
    private Map<String, String> details;
    
    // Constructor, getters
}

Response Headers

java
@GetMapping("/{id}")
public ResponseEntity<AppointmentResponseDTO> getById(@PathVariable Long id) {
    AppointmentResponseDTO appointment = appointmentService.findById(id);
    
    return ResponseEntity.ok()
        .header("X-Request-Id", UUID.randomUUID().toString())
        .header("Cache-Control", "max-age=3600")
        .body(appointment);
}

Controller Checklist

  • Use meaningful URL paths (/api/appointments, not /api/getAppointments)
  • Use correct HTTP methods (GET, POST, PUT, PATCH, DELETE)
  • Return appropriate status codes (200, 201, 204, 400, 404, etc.)
  • Validate all input with @Valid
  • Add security annotations (@PreAuthorize)
  • Document with OpenAPI annotations
  • Keep controllers thin - delegate to services
  • Use DTOs for requests and responses
  • Support pagination for list endpoints
  • Add proper error handling

Didn't find tool you were looking for?

Be as detailed as possible for better results