Foundation · Build

Production Implementation: Order API Contract Slice

Deliver one reviewable REST slice—DTO contracts, validation, ProblemDetail errors, and MockMvc evidence—exactly as it would ship.

Primary source: Spring Boot 3.5: Production Web Applications

45 min

Lesson 11 of 11

1d streak

The Architectural Question

Endpoint snippets ignore the decisions that matter in production: who validates, who maps, who translates errors, and what evidence proves each path.

What this guide proves

  • Keep controllers as HTTP adapters
  • Protect public JSON with DTOs and ProblemDetail errors
  • Prove success and failure paths with MockMvc

Architecture of a production REST API contract slice

A production-grade REST slice isolates HTTP transport concerns from business rules. Controllers act strictly as protocol adapters, request/response records define the public wire contract, Jakarta validation guards boundaries, and RFC 9457 ProblemDetail formats errors predictably.

  1. 01Wire contract: Immutable Java 21 records with Jakarta validation annotations.
  2. 02Controller adapter: Maps routes, invokes service, and constructs ResponseEntity with status and Location headers.
  3. 03Validation boundary: Triggers MethodArgumentNotValidException on malformed payloads.
  4. 04Global exception advice: Translates domain and validation failures into RFC 9457 ProblemDetail JSON documents.
  5. 05Automated contract evidence: @WebMvcTest slice tests proving success, validation rejection, and 404 behavior.

Complete production REST controller implementation

OrderApiController.java
package com.example.orders.api;

import jakarta.validation.Valid;
import jakarta.validation.constraints.*;
import java.net.URI;
import java.util.UUID;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/orders")
public class OrderApiController {
    private final OrderApplicationService service;

    public OrderApiController(OrderApplicationService service) {
        this.service = service;
    }

    @PostMapping
    public ResponseEntity<OrderDto> create(@Valid @RequestBody CreateOrderRequest request) {
        var order = service.createOrder(request.customerId(), request.amount());
        URI location = URI.create("/api/orders/" + order.id());
        return ResponseEntity.created(location).body(order);
    }

    @GetMapping("/{id}")
    public ResponseEntity<OrderDto> findById(@PathVariable UUID id) {
        return ResponseEntity.ok(service.getOrder(id));
    }
}

record CreateOrderRequest(
    @NotNull UUID customerId,
    @Positive(message = "Order amount must be positive") double amount
) {}

record OrderDto(UUID id, UUID customerId, double amount, String status) {}

Verifying the contract with @WebMvcTest slice tests

Slice testing with @WebMvcTest loads only the Spring MVC infrastructure (DispatcherServlet, converters, validation, controller advice) and mocks application collaborators, executing in milliseconds.

OrderApiControllerTest.java
package com.example.orders.api;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import java.util.UUID;
import static org.mockito.BDDMockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@WebMvcTest(OrderApiController.class)
class OrderApiControllerTest {
    @Autowired private MockMvc mvc;
    @MockitoBean private OrderApplicationService service;

    @Test
    void createsOrderAndReturns201WithLocation() throws Exception {
        var customerId = UUID.randomUUID();
        var orderId = UUID.randomUUID();
        given(service.createOrder(eq(customerId), eq(99.0)))
            .willReturn(new OrderDto(orderId, customerId, 99.0, "CONFIRMED"));

        mvc.perform(post("/api/orders")
                .contentType(MediaType.APPLICATION_JSON)
                .content("""
                    {"customerId": "%s", "amount": 99.0}
                    """.formatted(customerId)))
            .andExpect(status().isCreated())
            .andExpect(header().string("Location", "/api/orders/" + orderId))
            .andExpect(jsonPath("$.id").value(orderId.toString()))
            .andExpect(jsonPath("$.status").value("CONFIRMED"));
    }

    @Test
    void rejectsNegativeAmountWith400ProblemDetail() throws Exception {
        mvc.perform(post("/api/orders")
                .contentType(MediaType.APPLICATION_JSON)
                .content("""
                    {"customerId": "%s", "amount": -10.0}
                    """.formatted(UUID.randomUUID())))
            .andExpect(status().isBadRequest())
            .andExpect(jsonPath("$.title").value("Bad Request"));
    }
}

Active Recall Checkpoint

What makes @WebMvcTest superior to @SpringBootTest for testing REST API contracts?

Hint: Think about test execution speed and isolation from the database.

Show answer & explanation

@WebMvcTest initializes only web layer components and avoids booting databases, Hibernate, or messaging brokers. This keeps the test suite fast (milliseconds per test) and isolates failures strictly to HTTP routing, binding, and serialization contracts.

Production readiness checklist for REST endpoints

Use it when

Ship endpoints with dedicated request/response DTOs, @Valid constraints, Location headers for creations, and @WebMvcTest verification in CI.

Reconsider when

Never ship controllers that accept or return raw JPA entities directly.

Working Implementation

Core Spring Implementation

Production REST API contract slice

Deliver one order API slice: validated record DTOs, RFC 9457 ProblemDetail failures, 201-plus-Location creation, and MockMvc coverage of every outcome class.

Component Boundaries

  1. 01Controllers translate HTTP into application commands and return DTO records; entities never reach the wire.
  2. 02One @RestControllerAdvice owns the error vocabulary so every failure serializes as application/problem+json.
  3. 03@WebMvcTest loads only the web slice with @MockitoBean stand-ins for use-case beans, keeping contract tests off the database.

Required Evidence

  • OpenAPI document includes application/problem+json examples for 400, 404, and 409 responses.
  • DTO records carry the Jackson and validation annotations; entities expose none of them.
  • The MockMvc suite covers create 201, validation 400, not found 404, and conflict 409.

Implementation sequence

Step-by-Step Integration

Follow these structured steps to inject Core Spring functionality cleanly across your boundaries.

01

Return 201 with a Location header on create

OrderController.java

Clients need the canonical URL of the new resource; a bare 200 body pushes URL construction onto every caller.

OrderController.java
@RestController
@RequestMapping("/api/orders")
class OrderController {
    @PostMapping
    ResponseEntity<OrderView> create(@Valid @RequestBody CreateOrderRequest request) {
        OrderView created = orders.create(request.toCommand());
        URI location = URI.create("/api/orders/" + created.id());
        return ResponseEntity.created(location).body(created);
    }
}

Verify Configuration

POST /api/orders with a valid body answers 201 Created, a Location header of /api/orders/<uuid>, and a response body whose fields match OrderView.

02

Centralize failures as ProblemDetail

ApiExceptionHandler.java

Whitelabel pages and stack traces leak internals and break client parsing; mapped exceptions produce stable type, title, and detail fields under one content type.

ApiExceptionHandler.java
@RestControllerAdvice
class ApiExceptionHandler {
    @ExceptionHandler(OrderNotFound.class)
    ProblemDetail notFound(OrderNotFound failure) {
        ProblemDetail problem = ProblemDetail.forStatus(404);
        problem.setTitle("Order not found");
        problem.setDetail(failure.getMessage());
        return problem;
    }
}

Verify Configuration

GET /api/orders/00000000-0000-0000-0000-000000000000 returns 404 with Content-Type application/problem+json and a title field, not an HTML error page.

03

Lock the contract with @WebMvcTest

OrderControllerTest.java

The slice wires controller, advice, validators, and Jackson mapping together, so a changed constraint or serializer breaks the test instead of a client integration.

OrderControllerTest.java
@WebMvcTest(OrderController.class)
class OrderControllerTest {
    @Autowired MockMvc mvc;
    @MockitoBean OrderUseCases orders;

    @Test
    void rejects_empty_lines() throws Exception {
        mvc.perform(post("/api/orders")
                .contentType(APPLICATION_JSON)
                .content("{\"lines\":[]}"))
            .andExpect(status().isBadRequest());
    }
}

Verify Configuration

./mvnw test -Dtest=OrderControllerTest passes three outcome classes: empty lines yield 400, unknown ids yield 404 problem+json, and valid creates yield 201 with Location.

Synthesis & Production Verification

Deliberate Failure Condition

  1. Break: Expose repository entities straight through controllers with no advice
  2. Observe: Lazy fields explode during serialization and errors arrive in whatever shape the exception happened to choose
  3. Repair: DTO boundaries, validation, ProblemDetail advice, and controller tests are the pieces that made the slice reviewable.

Production Acceptance Criteria

  • Deliver CRUD plus a paged list over explicit DTOs
  • Return stable ProblemDetail payloads for validation and conflict paths
  • Prove every path with MockMvc tests checked into CI

Architectural & Technical Review

Core Interview Questions

Q01Why should controllers avoid containing business logic?Show answer

Controllers are transport adapters. Keeping business logic in domain services makes rules reusable across CLI, batch, and event-driven consumers.

Q02What is the advantage of @WebMvcTest over @SpringBootTest for API contract testing?Show answer

@WebMvcTest boots only the web infrastructure (DispatcherServlet, validation, converters) and mocks dependencies, running in milliseconds.

Q03What elements are required for an endpoint to meet production readiness?Show answer

Dedicated request/response DTOs, Jakarta validation constraints, RFC 9457 error contracts, and automated slice test evidence.

Production Pitfalls

Critical Anti-Patterns

  • Pitfall: Exposing JPA entities in controller method signatures — Exposing entities leaks internal schema structure, risks unintended mutations via dirty checking, and causes LazyInitializationExceptions.
  • Pitfall: Skipping slice tests in CI pipelines — Without @WebMvcTest coverage, HTTP status codes, validation annotations, and JSON serialization contracts are left unverified.

Module Certification Quiz

Prove Module Mastery

Comprehensive question set covering all lessons in this module.

Take Module Quiz

Primary Documentation & Specifications

Supplementary Module Resources