Spring MVC and REST APIs - Complete Notes
A complete guide to the servlet request lifecycle, DispatcherServlet, annotated controllers, request and response binding, DTOs, validation, error contracts, HTTP semantics, pagination, versioning, idempotency, file handling, CORS, and production-quality API design.
00. The Spring MVC mental model
Spring MVC is a request-routing and adaptation pipeline on the Servlet stack. It maps an HTTP request to an application method, converts wire data into Java values, invokes the method, and converts the result back into an HTTP response.
The controller should translate between HTTP and an application use case. It should not contain database queries, transaction choreography, pricing rules, or remote-client details. A focused controller validates the transport contract, calls a service, and maps the outcome to an HTTP contract.
Think of an airport. The front controller is air-traffic control. Handler mappings identify the correct runway, argument resolvers prepare the arriving request, the controller performs the scheduled handoff, and message converters prepare the departing response. Business services are the destination operations, not the control tower.
A REST endpoint is not only a Java method exposed over the network. Method semantics, status codes, headers, cache behavior, idempotency, media types, and error bodies form a contract with clients.
01. Servlet stack and request lifecycle
The servlet container accepts network traffic and creates Servlet API request and response objects. Spring MVC runs inside that container.
- A client sends an HTTP request to the server.
- The servlet container accepts the connection and applies container-level processing.
- Servlet filters run around the request.
- The request reaches Spring's
DispatcherServlet. - A handler mapping finds the matching controller method.
- A handler adapter resolves method arguments and invokes the method.
- The application service performs the use case.
- The return value is handled, often through an HTTP message converter.
- Exception resolvers translate failures when necessary.
- The response passes back through filters and the container writes it to the client.
A traditional servlet request is normally handled by one container thread. Blocking database or network calls occupy that thread until completion. Set timeouts, bound thread and connection pools, and avoid unbounded blocking work.
02. DispatcherServlet and front controller
DispatcherServlet implements the front-controller pattern: one central servlet
coordinates request processing through specialized delegates.
| Delegate | Responsibility |
|---|---|
HandlerMapping |
Find the handler for the request |
HandlerAdapter |
Invoke a handler using its programming model |
HandlerMethodArgumentResolver |
Create controller method arguments |
HandlerMethodReturnValueHandler |
Interpret controller return values |
HttpMessageConverter |
Read and write HTTP bodies |
HandlerExceptionResolver |
Map exceptions to responses |
ViewResolver |
Resolve server-rendered views when used |
Spring Boot registers the MVC infrastructure when the servlet web stack is present and conditions
match. Applications usually customize it through WebMvcConfigurer rather than
replacing the complete MVC configuration.
It imports full MVC configuration and can replace Boot's MVC auto-configuration. Implement
WebMvcConfigurer for targeted customization unless complete control is intentional.
03. Controllers and REST controllers
@Controller marks an MVC component. @RestController combines controller
behavior with @ResponseBody semantics.
@RestController
@RequestMapping("/api/orders")
final class OrderController {
private final OrderApplicationService orders;
OrderController(OrderApplicationService orders) {
this.orders = orders;
}
@GetMapping("/{orderId}")
OrderResponse get(@PathVariable UUID orderId) {
return OrderResponse.from(orders.get(orderId));
}
}
@RestController return values normally go through message conversion instead of view
resolution. Constructor injection makes the required use case explicit and keeps controller tests
simple.
Keep the boundary thin
- Parse and validate HTTP inputs.
- Map transport DTOs to application commands.
- Invoke one clear application use case.
- Map the result to a response DTO and HTTP status.
- Delegate cross-cutting error formatting to controller advice.
04. Request mappings
A mapping can match path, HTTP method, parameters, headers, consumed media types, and produced media types.
@PostMapping(
consumes = MediaType.APPLICATION_JSON_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE
)
ResponseEntity<OrderResponse> create(
@Valid @RequestBody CreateOrderRequest request
) {
Order order = orders.create(request.toCommand());
URI location = URI.create("/api/orders/" + order.id());
return ResponseEntity.created(location)
.body(OrderResponse.from(order));
}
Prefer @GetMapping, @PostMapping, @PutMapping,
@PatchMapping, and @DeleteMapping over a method-level
@RequestMapping without an HTTP method. Explicit method semantics reduce accidental
matches.
Path design
- Use nouns for resources:
/orders/{id}. - Use hierarchy only when it expresses real containment:
/orders/{id}/items. - Avoid deeply nested paths that require unrelated identifiers.
- Use stable opaque identifiers rather than mutable names when identity matters.
- Use an action subresource only for a real command that does not fit CRUD cleanly.
05. Request input annotations
| Annotation | Source | Typical use |
|---|---|---|
@PathVariable |
Matched URI path segment | Resource identity |
@RequestParam |
Query string or form parameter | Filtering, pagination, simple form data |
@RequestHeader |
HTTP header | Conditional request or client metadata |
@CookieValue |
Cookie | Explicit cookie contract |
@RequestBody |
Message-converted body | JSON or another structured representation |
@RequestPart |
Multipart part | File plus structured metadata |
@ModelAttribute |
Bound request parameters | Forms and query objects |
@GetMapping
PageResponse<OrderSummary> search(
@RequestParam(required = false) OrderStatus status,
@RequestParam(defaultValue = "0") @Min(0) int page,
@RequestParam(defaultValue = "20") @Min(1) @Max(100) int size
) {
return OrderSummaryPage.from(orders.search(status, page, size));
}
Conversion failures are client errors, not server errors. A malformed UUID, unknown enum, or non-numeric page value should produce a stable 400 response through centralized error handling.
06. Request and response DTOs
Transport DTOs protect the API contract from persistence and domain implementation details.
public record CreateOrderRequest(
@NotNull UUID customerId,
@NotEmpty List<@Valid OrderItemRequest> items,
@Size(max = 500) String note
) {
CreateOrderCommand toCommand() {
return new CreateOrderCommand(customerId, items, note);
}
}
public record OrderResponse(
UUID id,
String status,
BigDecimal total,
Instant createdAt
) {
static OrderResponse from(Order order) {
return new OrderResponse(
order.id(),
order.status().name(),
order.total().amount(),
order.createdAt()
);
}
}
Entity relationships can trigger lazy queries, cycles, oversized JSON, accidental field exposure, and API changes when the schema changes. DTOs make loading, security, versioning, and compatibility deliberate.
07. HTTP message conversion and JSON
HttpMessageConverter implementations turn request bodies into Java objects and Java
return values into response bodies.
Converter selection uses the Java target type, request Content-Type, client
Accept header, controller mapping constraints, and available converters. JSON support
commonly uses Jackson in Spring Boot web applications.
| Header | Meaning | Typical failure |
|---|---|---|
Content-Type |
Format of the request body | 415 Unsupported Media Type |
Accept |
Response formats the client accepts | 406 Not Acceptable |
Define time, number, enum, null, and unknown-field behavior intentionally. Public APIs should not inherit accidental serializer settings. Use ISO-8601 timestamps with an offset or UTC instant and decimal types for exact monetary values.
08. Bean Validation
Validation checks the shape and local constraints of incoming data before the use case runs.
public record OrderItemRequest(
@NotNull UUID productId,
@Min(1) @Max(1000) int quantity
) {}
@PostMapping
OrderResponse create(@Valid @RequestBody CreateOrderRequest request) {
return OrderResponse.from(orders.create(request.toCommand()));
}
@Valid triggers cascaded validation of the body and nested values. Direct constraints
on controller method parameters can trigger method validation. Current Spring MVC can report
body-object validation and method validation through different exception types, so global error
handling should cover both.
Validation belongs at multiple layers
| Layer | Example rule |
|---|---|
| Transport DTO | Required field, string length, numeric range |
| Application or domain | Order can be cancelled only before shipment |
| Database | Unique key, foreign key, non-null invariant |
Do not perform a database lookup inside a simple field validator for every element. Cross-record business rules belong in an application service with a transaction and a concurrency-safe database constraint where applicable.
Custom class-level validator
@ValidDateRange
public record ReportRequest(
@NotNull LocalDate from,
@NotNull LocalDate to
) {}
final class DateRangeValidator
implements ConstraintValidator<ValidDateRange, ReportRequest> {
public boolean isValid(
ReportRequest value,
ConstraintValidatorContext context) {
return value == null
|| value.from() == null
|| value.to() == null
|| !value.to().isBefore(value.from());
}
}
09. Return values and ResponseEntity
Return a body directly when the status and headers are fixed. Use
ResponseEntity<T> when the endpoint controls status or headers.
@PostMapping
ResponseEntity<OrderResponse> create(
@Valid @RequestBody CreateOrderRequest request) {
Order created = orders.create(request.toCommand());
URI location = ServletUriComponentsBuilder
.fromCurrentRequest()
.path("/{id}")
.buildAndExpand(created.id())
.toUri();
return ResponseEntity.created(location)
.body(OrderResponse.from(created));
}
| Status | Use |
|---|---|
| 200 OK | Successful response with representation |
| 201 Created | Resource created, usually with Location |
| 202 Accepted | Work accepted but not completed |
| 204 No Content | Successful response with no body |
| 400 Bad Request | Malformed syntax, conversion, or validation |
| 401 Unauthorized | Authentication required or invalid |
| 403 Forbidden | Authenticated caller lacks permission |
| 404 Not Found | Resource does not exist or is intentionally concealed |
| 409 Conflict | Request conflicts with current resource state |
| 412 Precondition Failed | Conditional update precondition failed |
| 422 Unprocessable Content | Well-formed input violates semantic rules |
| 500 Internal Server Error | Unexpected server failure |
10. Global error handling and ProblemDetail
Errors need a stable machine-readable contract, a useful human explanation, and a correlation identifier for support.
Spring supports RFC 9457 problem details through ProblemDetail. Standard fields
include type, title, status, detail, and instance. Extra properties can carry safe structured
fields such as validation errors or a trace identifier.
@RestControllerAdvice
class ApiExceptionHandler {
@ExceptionHandler(OrderNotFoundException.class)
ResponseEntity<ProblemDetail> handleNotFound(
OrderNotFoundException ex,
HttpServletRequest request) {
ProblemDetail problem = ProblemDetail.forStatusAndDetail(
HttpStatus.NOT_FOUND,
"The requested order does not exist"
);
problem.setTitle("Order not found");
problem.setType(URI.create("https://api.example/problems/order-not-found"));
problem.setInstance(URI.create(request.getRequestURI()));
problem.setProperty("orderId", ex.orderId());
return ResponseEntity.status(problem.getStatus()).body(problem);
}
}
Validation error shape
{
"type": "https://api.example/problems/validation",
"title": "Request validation failed",
"status": 400,
"detail": "One or more fields are invalid",
"instance": "/api/orders",
"errors": [
{
"field": "items[0].quantity",
"code": "Min",
"message": "must be greater than or equal to 1"
}
],
"traceId": "8f0b31a6"
}
Never return stack traces, SQL, table names, class names, secrets, or raw upstream responses to public clients. Log internal context with the same trace identifier and return a safe contract.
Handle known domain and framework failures specifically. Keep a final unexpected-error handler, but do not convert every exception into 200 or 400. A server bug is still a 500 and must remain observable.
11. HTTP method semantics
| Method | Safe | Idempotent | Typical meaning |
|---|---|---|---|
| GET | Yes | Yes | Read a representation |
| HEAD | Yes | Yes | Read response metadata without body |
| POST | No | Not inherently | Create subordinate resource or execute command |
| PUT | No | Yes | Replace resource at known URI |
| PATCH | No | Depends on patch semantics | Apply partial modification |
| DELETE | No | Yes | Remove resource state |
Idempotent means repeating the same request has the same intended effect, not that every response byte or status must be identical. Repeating a DELETE may return 204 first and 404 later while the resource remains deleted.
12. PUT, PATCH, and lost updates
Partial updates must distinguish "field absent" from "field present with null."
A normal Java nullable property cannot represent all patch states by itself. Use a documented patch media type, explicit wrapper, JSON Merge Patch, JSON Patch, or command-specific endpoint. Validate the final state after applying the patch.
Optimistic HTTP concurrency
GET /api/orders/42
ETag: "v7"
PUT /api/orders/42
If-Match: "v7"
// If current version is no longer v7:
HTTP/1.1 412 Precondition Failed
Conditional requests prevent one client from silently overwriting another client's update. Map the HTTP version token to an application or persistence version deliberately.
13. Pagination, sorting, and filtering
Every collection endpoint needs a bounded result size and deterministic order.
public record PageResponse<T>(
List<T> items,
int page,
int size,
long totalElements,
int totalPages
) {}
Offset pagination is easy and supports page numbers, but deep offsets become expensive and results can shift during concurrent writes. Cursor or keyset pagination uses the last ordered key and is stronger for large changing feeds.
- Enforce a maximum page size.
- Allow only a whitelist of sortable fields.
- Add a unique tie-breaker to every sort, such as
createdAt,id. - Do not pass raw property or SQL fragments from clients to the database.
- Document whether totals are exact, approximate, or omitted.
- Encode cursor state so clients do not construct invalid cursors.
14. Content negotiation
Content negotiation selects a representation that the client accepts and the server can produce.
Spring MVC checks the Accept header by default. Controller produces and
consumes constraints make supported media types explicit. Prefer header-based
negotiation. Path extensions can create ambiguous routing and security problems.
@GetMapping(
path = "/{id}",
produces = "application/vnd.example.order-v2+json"
)
OrderV2Response getV2(@PathVariable UUID id) {
return OrderV2Response.from(orders.get(id));
}
15. API versioning and compatibility
Version only when an incompatible contract change cannot be introduced additively.
| Strategy | Benefit | Tradeoff |
|---|---|---|
URI, such as /v2/orders |
Visible and easy to route | Version becomes part of resource URI |
| Media type | Versions representation explicitly | Harder for humans and some tooling |
| Custom header | Keeps paths stable | Less standard and less visible |
| Query parameter | Simple to test | Can interact poorly with caches |
Adding an optional response field is often compatible for tolerant clients. Renaming a field, changing meaning, removing an enum value, or making an optional request field required can be breaking. Maintain contract tests and publish a deprecation and retirement policy.
16. Idempotency for unsafe operations
Networks retry. A client can lose the response even when the server committed the operation.
For payment, order, or booking creation, accept an idempotency key scoped to the caller and operation. Atomically store the key, a fingerprint of the request, operation status, and final response. A repeated matching request returns the stored result. A reused key with different input is rejected.
POST /api/orders
Idempotency-Key: 64f7d828-9ce0-4fcb-a3f2-a6524fb874d2
// Server transaction:
// 1. Insert unique (client_id, idempotency_key).
// 2. Create order exactly once.
// 3. Store status and response reference.
// 4. Commit.
It is lost on restart, differs across instances, and races under concurrency. Use durable shared storage and a unique constraint. Define retention and in-progress retry behavior.
17. File upload and download
Files are untrusted, potentially large inputs. Validate limits and stream data where possible.
@PostMapping(
path = "/attachments",
consumes = MediaType.MULTIPART_FORM_DATA_VALUE
)
AttachmentResponse upload(
@Valid @RequestPart("metadata") AttachmentMetadata metadata,
@RequestPart("file") MultipartFile file
) {
return attachments.store(metadata, file);
}
- Set request and per-file size limits.
- Do not trust the supplied filename or content type.
- Generate storage keys and prevent path traversal.
- Scan files where the risk model requires it.
- Store large objects outside the application filesystem when deployments are ephemeral.
- Stream downloads and support cache or range semantics when appropriate.
- Set safe
Content-Dispositionand response content type.
18. CORS
CORS is a browser policy that controls which origins may read cross-origin responses. It is not authentication or authorization.
Browsers send a preflight request for certain cross-origin operations. Spring MVC can process preflight, simple, and actual CORS requests using handler-level or global configuration.
@Configuration
class WebConfiguration implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("https://app.example.com")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("Content-Type", "Authorization", "Idempotency-Key")
.exposedHeaders("Location", "ETag")
.allowCredentials(true)
.maxAge(1800);
}
}
Use a finite origin allowlist when cookies or other credentials are allowed. A permissive CORS response does not grant server authorization by itself, but it can expose authenticated browser data to an untrusted origin when configured badly.
19. Filters, interceptors, and advice
| Mechanism | Boundary | Good use |
|---|---|---|
| Servlet filter | Raw servlet request and response | Security chain, correlation ID, request wrapping |
| Handler interceptor | Mapped MVC handler | Handler-aware timing or locale behavior |
ResponseBodyAdvice |
Before response body conversion | Focused response-body policy |
@ControllerAdvice |
Across selected controllers | Error mapping and shared binding concerns |
| Service AOP | Application method boundary | Transactions and method security |
Do not read a large request body in a logging filter without size limits and redaction. Servlet streams are normally one-shot unless wrapped. Log metadata by default and sample safe bodies only under an explicit policy.
20. Asynchronous request handling
Servlet async support can release the container thread while application work completes elsewhere.
Spring MVC supports return types such as Callable, DeferredResult, and
streaming response forms. This does not make blocking libraries non-blocking. Work still consumes
threads in the configured executor. Define timeouts, cancellation behavior, executor capacity, and
observability.
For long durable jobs, return 202 with a job resource, persist job state, and process through a durable worker mechanism. Keeping one HTTP connection open for minutes is not a reliable job queue.
21. Security at the web boundary
- Authenticate and authorize on the server for every protected operation.
- Validate resource ownership, not only broad roles.
- Keep CORS, CSRF, authentication, and authorization as separate concepts.
- Apply request size, header size, and multipart limits.
- Use HTTPS and secure proxy-header configuration.
- Do not accept arbitrary sort fields, class names, URLs, or file paths.
- Prevent mass assignment by mapping explicit request DTO fields.
- Rate-limit costly and abuse-sensitive operations at a suitable boundary.
- Return safe errors and keep detailed evidence in protected logs.
22. Testing controllers
Controller tests should verify the HTTP contract: mapping, conversion, validation, status, headers, body, and error shape.
@WebMvcTest(OrderController.class)
class OrderControllerTest {
@Autowired MockMvc mvc;
@MockBean OrderApplicationService orders;
@Test
void rejectsEmptyItems() throws Exception {
mvc.perform(post("/api/orders")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{"customerId":"6b6c0c98-f7d1-48ef-9435-4a532daff4ee",
"items":[]}
"""))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.type")
.value("https://api.example/problems/validation"));
}
}
Use unit tests for mapping helpers and business services, MVC slice tests for the HTTP layer, and a smaller number of full integration tests for security, serialization, database, and deployment wiring. Test the actual error contract, not only the status.
23. Production API checklist
- Use resource-oriented paths and explicit HTTP methods.
- Keep controllers thin and use request and response DTOs.
- Validate input shape, domain rules, and database invariants at the right layers.
- Return correct statuses, headers, and stable RFC 9457 error bodies.
- Bound collection sizes, file sizes, timeouts, pools, and response payloads.
- Use deterministic pagination and whitelist filter and sort fields.
- Protect unsafe retryable operations with durable idempotency.
- Use optimistic concurrency for update conflicts when clients can race.
- Configure a finite CORS policy and enforce server-side authorization.
- Document compatibility, versioning, deprecation, and error contracts.
- Emit trace identifiers, metrics, structured logs, and safe diagnostics.
- Test mappings, validation, serialization, security, and failure responses.
24. Common mistakes
| Mistake | Failure | Correction |
|---|---|---|
| Returning entities | Leaks fields and triggers uncontrolled loading | Map explicit response DTOs |
| Business logic in controllers | Hard-to-test and duplicated use cases | Delegate to application services |
| Returning 200 for every result | Clients cannot use HTTP semantics | Choose status codes by outcome |
| One generic catch-all 400 handler | Server bugs look like client errors | Map known failures and preserve 500 |
| Unbounded list endpoint | Memory and database exhaustion | Require bounded pagination |
| Wildcard credentialed CORS | Untrusted origins can access sensitive browser data | Use a finite allowlist |
| In-memory idempotency keys | Duplicates across restarts and instances | Use durable unique storage |
| PATCH with ordinary nullable fields | Absent and explicit null are confused | Use explicit patch semantics |
25. Interview questions and answers
What is DispatcherServlet?
It is Spring MVC's front controller. It coordinates handler mapping, handler invocation, argument resolution, return-value processing, message conversion, exception resolution, and view handling.
What is the difference between @Controller and @RestController?
@Controller participates in MVC and commonly returns view names unless response-body
behavior is selected. @RestController combines @Controller and
@ResponseBody, so return values normally become response bodies.
How does @RequestBody work?
MVC selects an HttpMessageConverter using the target Java type and request media
type, reads the body, deserializes it, and optionally validates the resulting object.
Why use DTOs instead of entities?
DTOs define a stable transport contract, limit accepted and exposed fields, avoid lazy-loading and object-cycle problems, and decouple API evolution from persistence design.
What is the difference between @Valid and @Validated?
Both can trigger validation. @Valid is the Jakarta standard and supports cascaded
validation. Spring's @Validated also supports validation groups and is used in
additional Spring validation scenarios. Exact MVC method-validation behavior depends on where
constraints are declared.
Filter versus interceptor?
A filter runs at the Servlet boundary and can wrap raw requests and responses. An interceptor runs after MVC maps a handler and can access handler metadata. Security is normally implemented in the filter chain.
What is content negotiation?
It is selecting a response representation compatible with what the client accepts and the server
can produce. Spring MVC uses the Accept header by default and available message
converters.
How do you make POST creation safe to retry?
Accept an idempotency key, atomically claim it in durable shared storage, bind it to a request fingerprint, store the operation result, and return that result for matching retries.
CORS versus CSRF?
CORS controls whether browser JavaScript may read a cross-origin response. CSRF protects a server from unwanted state-changing requests made with automatically attached credentials. One does not replace the other.
Offset versus cursor pagination?
Offset pagination supports page numbers but deep offsets cost more and concurrent changes can shift results. Cursor pagination continues after a stable ordered key and is stronger for large, changing datasets.
26. Practice exercises
- Design CRUD endpoints for orders and justify every method and status code.
- Create nested request validation and return a stable list of field errors.
- Implement ProblemDetail mappings for not found, conflict, and unexpected failure.
- Add deterministic offset pagination, then redesign it with a cursor.
- Protect an update with ETag and
If-Match. - Design durable idempotency for order creation under concurrent duplicate requests.
- Upload a file safely with metadata, size limits, and generated storage keys.
- Write MockMvc tests for media type, validation, status, headers, and error body.
27. One-screen cheat sheet
DispatcherServletcoordinates the MVC request pipeline.- Controllers translate HTTP and delegate business work.
- Mappings match path, method, headers, parameters, and media types.
- Message converters read request bodies and write response bodies.
- DTOs protect API contracts from domain and persistence details.
- Validate transport shape, domain rules, and storage invariants separately.
- Use correct status codes, headers, and stable ProblemDetail responses.
- Bound and deterministically order every collection endpoint.
- Use durable idempotency and optimistic concurrency where retries or races matter.
- CORS is a browser read policy, not authorization.
- Test the complete HTTP contract, including failures.