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.

HTTP is part of the design

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.

  1. A client sends an HTTP request to the server.
  2. The servlet container accepts the connection and applies container-level processing.
  3. Servlet filters run around the request.
  4. The request reaches Spring's DispatcherServlet.
  5. A handler mapping finds the matching controller method.
  6. A handler adapter resolves method arguments and invokes the method.
  7. The application service performs the use case.
  8. The return value is handled, often through an HTTP message converter.
  9. Exception resolvers translate failures when necessary.
  10. 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.

Use @EnableWebMvc carefully in Spring Boot

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.

A focused REST controller
@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.

Specific method mapping
@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
Filter and pagination inputs
@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.

Request DTO with transport validation
public record CreateOrderRequest(
    @NotNull UUID customerId,
    @NotEmpty List<@Valid OrderItemRequest> items,
    @Size(max = 500) String note
) {
    CreateOrderCommand toCommand() {
        return new CreateOrderCommand(customerId, items, note);
    }
}
Response DTO
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()
        );
    }
}
Do not expose JPA entities directly

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.

Nested validation
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

Cross-field date rule
@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.

Create response with Location header
@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.

Central domain exception mapping
@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

Stable field error payload
{
  "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"
}
Do not leak internals

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

ETag-based update concept
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.

Page response contract
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.

Versioned vendor media type example
@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.

Idempotent create contract
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.
An in-memory map is not enough

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.

Multipart upload with metadata
@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-Disposition and 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.

Explicit global CORS 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);
    }
}
Credentials increase trust

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.

MockMvc contract test
@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

  1. Design CRUD endpoints for orders and justify every method and status code.
  2. Create nested request validation and return a stable list of field errors.
  3. Implement ProblemDetail mappings for not found, conflict, and unexpected failure.
  4. Add deterministic offset pagination, then redesign it with a cursor.
  5. Protect an update with ETag and If-Match.
  6. Design durable idempotency for order creation under concurrent duplicate requests.
  7. Upload a file safely with metadata, size limits, and generated storage keys.
  8. Write MockMvc tests for media type, validation, status, headers, and error body.

27. One-screen cheat sheet

MVC and REST rules
  • DispatcherServlet coordinates 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.

28. Official references

Last reviewed · July 2026 · part of knowledge-base