Quick answer
A Spring Boot REST controller is a class that exposes HTTP endpoints and returns response bodies such as JSON. It usually uses @RestController, request mapping annotations, DTOs, validation, and service-layer calls.
A good controller should be thin. It should translate HTTP requests into application actions, validate input, choose clear status codes, and return stable response shapes. Business rules should live in services, not inside controller methods.
Minimal controller example
A simple controller might look like this:
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/{userId}")
public UserResponse getUser(@PathVariable String userId) {
return userService.getUser(userId);
}
}
@RestController combines controller behavior with response-body serialization. In typical Spring Boot APIs, returning a Java object lets Spring serialize that object as JSON.
@RestController vs @Controller
@Controller is often used for MVC pages where methods return view names.
@RestController is used for APIs where methods return data directly.
For backend APIs, @RestController is the normal default.
@RestController
class HealthController {
@GetMapping("/health")
Map<String, String> health() {
return Map.of("status", "ok");
}
}
That method returns JSON, not a template view.
Request mapping annotations
Spring provides method-specific annotations:
| Annotation | Common use |
|---|---|
@GetMapping | Read a resource or list. |
@PostMapping | Create a resource or start a command. |
@PutMapping | Replace a resource. |
@PatchMapping | Partially update a resource. |
@DeleteMapping | Delete or deactivate a resource. |
These annotations are easier to read than a generic @RequestMapping(method = ...) for ordinary controllers.
Read REST API Design Checklist and PUT vs PATCH in REST APIs when choosing endpoint semantics.
Path variables and query parameters
Use @PathVariable for resource identity:
@GetMapping("/{orderId}")
public OrderResponse getOrder(@PathVariable String orderId) {
return orderService.getOrder(orderId);
}
Use @RequestParam for filters, sorting, and pagination:
@GetMapping
public PageResponse<OrderSummary> listOrders(
@RequestParam(defaultValue = "20") int limit,
@RequestParam(required = false) String cursor
) {
return orderService.listOrders(limit, cursor);
}
Do not let unbounded query parameters pass directly into database queries. Define maximum limits and stable sort behavior.
Read Pagination Strategies for REST APIs for list endpoint design.
Request bodies and DTOs
Use @RequestBody to read a JSON body.
@PostMapping
public ResponseEntity<UserResponse> createUser(@Valid @RequestBody CreateUserRequest request) {
UserResponse created = userService.createUser(request);
return ResponseEntity.status(HttpStatus.CREATED).body(created);
}
A request DTO should represent the API contract, not the database entity.
public record CreateUserRequest(
@NotBlank String email,
@NotBlank String displayName
) {}
DTOs help protect internal models, support validation, and keep future API changes easier to manage.
Validation
Bean Validation annotations such as @NotBlank, @Email, @Size, and @Min make request rules visible.
public record CreateAccountRequest(
@NotBlank String name,
@Email String billingEmail
) {}
Validation should produce a stable error response, not a raw stack trace. Many APIs use a shared exception handler with @ControllerAdvice to map validation errors into a consistent JSON shape.
Read REST API Error Handling Best Practices for response design.
ResponseEntity and status codes
Returning a plain object is fine for simple 200 OK responses. Use ResponseEntity when the method needs to control status, headers, or empty responses.
@DeleteMapping("/{userId}")
public ResponseEntity<Void> deleteUser(@PathVariable String userId) {
userService.deleteUser(userId);
return ResponseEntity.noContent().build();
}
Common choices:
200 OKfor successful reads and updates with a body.201 Createdfor successful creates.202 Acceptedfor async work.204 No Contentfor successful deletes without a body.400or422for validation errors.404for missing resources.409for conflicts.
Read REST API Status Codes Explained before inventing custom response behavior.
Keep controllers thin
A controller should not become a business service.
Good controller responsibilities:
- Route HTTP requests.
- Read path variables, query parameters, headers, and bodies.
- Trigger validation.
- Call the service layer.
- Choose status codes and response wrappers.
- Return DTOs.
Avoid putting these directly in controller methods:
- Complex business rules.
- SQL queries.
- Transaction orchestration.
- External API retry logic.
- Large mapping blocks repeated across endpoints.
Thin controllers are easier to test and easier to keep consistent.
Error handling with ControllerAdvice
A centralized exception handler keeps endpoints from repeating error logic.
@RestControllerAdvice
public class ApiExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
ResponseEntity<ApiError> notFound(ResourceNotFoundException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(new ApiError("not_found", ex.getMessage()));
}
}
This pattern helps make errors predictable across controllers.
For public APIs, include stable error codes and request IDs when possible.
Testing controllers
Controller tests should verify the HTTP contract:
- Route and method.
- Request validation.
- Status code.
- Response body shape.
- Error response shape.
- Service interaction for important cases.
A focused test might use Spring MVC testing support to call the controller without running the whole application stack.
The goal is not to duplicate every service test. The goal is to catch contract drift at the HTTP boundary.
Common mistakes
The first mistake is returning JPA entities directly. That can leak fields, trigger lazy loading, and couple the API to the database model.
The second mistake is catching every exception inside each controller method. Use centralized error handling for consistency.
The third mistake is using POST for every endpoint. HTTP method semantics are part of the API contract.
The fourth mistake is allowing unbounded limit or vague sorting on list endpoints.
The fifth mistake is putting transactions, remote calls, and complex branching directly in controller code.
Practical backend checklist
When reviewing a Spring Boot REST controller:
- Does the URL represent a clear resource or command?
- Is the HTTP method intentional?
- Are request and response DTOs separate from entities?
- Is input validated?
- Are status codes documented and consistent?
- Are errors handled through a shared shape?
- Are list endpoints bounded?
- Is business logic delegated to a service?
- Are controller tests focused on the HTTP contract?
- Is the endpoint represented in OpenAPI if the API is public or shared?
A controller is the front door of a backend API. Keep it predictable, small, and contract-focused.
Related reading
Read REST API Design Checklist for endpoint review, REST API Status Codes Explained for response choices, and REST API Error Handling Best Practices for consistent error bodies. For contract documentation, read OpenAPI Explained for Backend Developers. Browse the API Design Learning Path, Java Collections Learning Path, and Topics for adjacent backend guides.