REST APIs
Every Spring Boot microservice exposes REST APIs — HTTP resources identified by URLs, manipulated with standard verbs (GET, POST, PUT, PATCH, DELETE), and documented for consume…
Introduction
Every Spring Boot microservice exposes REST APIs — HTTP resources identified by URLs, manipulated with standard verbs (GET, POST, PUT, PATCH, DELETE), and documented for consumers via OpenAPI. When a mobile app calls POST /v1/transfers, the request hits a @RestController, deserializes into a validated DTO, invokes domain logic, and returns a consistent JSON envelope — or a structured error if validation fails.
This lesson teaches enterprise REST design: thin controllers, DTOs decoupled from JPA entities, Bean Validation (@NotNull, @Valid), global @ControllerAdvice exception handling, and SpringDoc OpenAPI for auto-generated Swagger UI.
Poor REST design surfaces as breaking API changes, mass-assignment vulnerabilities, and 500 errors leaking stack traces — staff engineers treat the HTTP layer as a published contract, not an afterthought.
Business problem
REST API failures damage integrations and compliance:
- Entity leakage: Returning JPA entities exposes lazy collections, internal IDs, and triggers N+1 during serialization.
- Inconsistent errors: Each controller catches exceptions differently — clients can't parse failures programmatically.
- Breaking changes: Renaming JSON fields without versioning breaks mobile apps in production.
- Validation gaps: Missing server-side validation — attacker sends negative transfer amounts.
- Undocumented APIs: No OpenAPI — partner integration takes weeks of Slack archaeology.
Why this topic exists
REST with Spring MVC is the enterprise HTTP standard:
- Controllers map HTTP to Java: @GetMapping, @PostMapping — declarative routing with content negotiation.
- DTOs protect domain: Input/output shapes independent of database schema — API evolves separately from entities.
- Validation at boundary: JSR-380 annotations reject bad input before business logic — fail fast with 400.
- Centralized errors: @ControllerAdvice maps exceptions to RFC 7807 Problem Details — consistent client experience.
- OpenAPI as contract: springdoc-openapi generates spec from annotations — single source of truth for docs and codegen.
Core concepts
Five REST API pillars in Spring:
- @RestController: @Controller + @ResponseBody — return types serialized to JSON via Jackson.
- DTOs (Data Transfer Objects): Records or classes for request/response — never expose @Entity directly.
- Validation: @Valid on @RequestBody triggers Bean Validation — MethodArgumentNotValidException → 400.
- Exception handling: @ExceptionHandler in @ControllerAdvice — map DomainException → 409, NotFound → 404.
- OpenAPI: @Operation, @Schema annotations — /v3/api-docs and Swagger UI for discovery.
Internal architecture
REST request flow through Spring MVC:
HTTP POST /v1/transfers (JSON body)│▼DispatcherServlet│▼HandlerMapping → TransferController.create(@Valid @RequestBody CreateTransferRequest dto)│├── Jackson HttpMessageConverter (JSON → DTO)├── MethodValidationInterceptor (@Valid failures)│▼TransferService.create(dto) → domain logic → TransferResult│▼TransferResponse.from(result) → Jackson → JSON 201 CreatedException path:InsufficientFundsException│▼GlobalExceptionHandler (@ControllerAdvice)│▼ProblemDetail (RFC 7807) → 409 Conflict + application/problem+json
Controller layer, DTO boundary, validation, and error handling:
Code walkthrough
Enterprise REST API — controller, DTOs, validation, errors, OpenAPI:
- Records as DTOs: Immutable, concise — Java 21 baseline for API types.
- @Valid: Triggers validation before service call — 400 with field errors.
- ProblemDetail: Spring 6 RFC 7807 — standard error JSON, not ad-hoc maps.
- @Tag/@Operation: OpenAPI metadata for partner documentation.
// ── Request / Response DTOs (Java records) ──public record CreateTransferRequest(@NotBlank @Schema(example = "acc-from-1") String fromAccountId,@NotBlank String toAccountId,@Positive @Schema(example = "10000") long amountCents,@NotBlank @Size(max = 64) String idempotencyKey) {}public record TransferResponse(String id, String status, long amountCents, Instant createdAt) {static TransferResponse from(Transfer t) {return new TransferResponse(t.getId(), t.getStatus().name(),t.getAmountCents(), t.getCreatedAt());}}// ── Controller ──@RestController@RequestMapping("/v1/transfers")@Tag(name = "Transfers", description = "Money movement API")@RequiredArgsConstructorpublic class TransferController {private final TransferService service;@PostMapping@ResponseStatus(HttpStatus.CREATED)@Operation(summary = "Create transfer")public TransferResponse create(@Valid @RequestBody CreateTransferRequest req) {return TransferResponse.from(service.create(req));}@GetMapping("/{id}")@Operation(summary = "Get transfer by ID")public TransferResponse get(@PathVariable String id) {return TransferResponse.from(service.findById(id));}}// ── Global exception handler (RFC 7807) ──@RestControllerAdvicepublic class GlobalExceptionHandler {@ExceptionHandler(MethodArgumentNotValidException.class)ResponseEntity<ProblemDetail> validation(MethodArgumentNotValidException ex) {ProblemDetail pd = ProblemDetail.forStatus(HttpStatus.BAD_REQUEST);pd.setTitle("Validation failed");pd.setProperty("errors", ex.getBindingResult().getFieldErrors().stream().map(fe -> fe.getField() + ": " + fe.getDefaultMessage()).toList());return ResponseEntity.badRequest().body(pd);}@ExceptionHandler(TransferNotFoundException.class)ResponseEntity<ProblemDetail> notFound(TransferNotFoundException ex) {ProblemDetail pd = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage());pd.setTitle("Transfer not found");return ResponseEntity.status(404).body(pd);}@ExceptionHandler(InsufficientFundsException.class)ResponseEntity<ProblemDetail> conflict(InsufficientFundsException ex) {return ResponseEntity.status(409).body(ProblemDetail.forStatusAndDetail(HttpStatus.CONFLICT, ex.getMessage()));}}
Production example
Production REST patterns — versioning, idempotency, pagination:
- Idempotency keys: POST retries safe — critical for payment APIs.
- Pageable: Never return unbounded lists — default page size 20.
- Swagger gated: Enable in dev/staging only — or auth-protected.
- Path versioning: /v1 explicit — header versioning harder for CDN/cache.
// API versioning — URL path (explicit, cache-friendly)@RequestMapping("/v1/transfers")// Idempotency — reject duplicate keys@Servicepublic class TransferService {public Transfer create(CreateTransferRequest req) {if (idempotencyStore.exists(req.idempotencyKey()))return idempotencyStore.get(req.idempotencyKey());Transfer t = /* process */;idempotencyStore.save(req.idempotencyKey(), t);return t;}}// Pagination — Spring Data Pageable@GetMappingpublic Page<TransferResponse> list(@PageableDefault(size = 20, sort = "createdAt", direction = DESC) Pageable page) {return service.findAll(page).map(TransferResponse::from);}// springdoc — application.ymlspringdoc:api-docs:path: /v3/api-docsswagger-ui:path: /swagger-ui.htmlenabled: ${SWAGGER_ENABLED:false} # off in prod by default// Security — don't document internal admin paths@BeanGroupedOpenApi publicApi() {return GroupedOpenApi.builder().group("public").pathsToMatch("/v1/**").build();}
Enterprise case study
Fintech partner integration failure: A bank exposed Account @Entity directly in REST responses. Jackson triggered lazy-load of transaction history on every GET — N+1 query storm collapsed read replica. Partner parsed internal UUID foreign keys that changed during DB migration. Fix: TransferResponse DTO with explicit fields, @JsonIgnore on entity relationships, OpenAPI spec published to partner portal, and contract tests in CI validating JSON schema. Added global exception handler after stack traces leaked in 500 responses during pentest.
- Symptom: p99 latency spike on GET /accounts/{id} — Hibernate SQL flood in logs.
- Root cause: Entity returned from controller — lazy collections serialized.
- Fix: DTO layer + @Transactional(readOnly=true) on read service + fetch join where needed.
- Process: OpenAPI diff in PR — breaking change requires /v2 bump.
Performance considerations
REST API performance:
- DTO mapping: MapStruct compile-time mappers faster than reflection-heavy mappers at scale.
- Payload size: Pagination + field filtering — don't return 10 MB JSON arrays.
- Compression: server.compression.enabled for responses > 1KB.
- ETag/Cache-Control: GET resources support conditional requests — reduce DB load.
- Async endpoints: DeferredResult/WebFlux only when justified — MVC + virtual threads often sufficient.
Security considerations
REST API security:
- Input validation: Server-side @Valid always — never trust client validation alone.
- Mass assignment: DTO with only allowed fields — don't bind request to entity with setters for role/admin.
- Rate limiting: Bucket4j or API gateway — protect POST /transfers from abuse.
- Error leakage: Global handler returns generic message in prod — log details server-side only.
- CORS: Explicit allowed origins — not * with credentials.
Scalability considerations
Scaling REST services:
- Stateless controllers: Scale pods behind load balancer — JWT auth, no session.
- API gateway: Kong/AWS API Gateway — TLS termination, rate limit, routing to services.
- HATEOAS optional: Spring HATEOAS for discoverable APIs — many teams prefer OpenAPI instead.
- Content negotiation: JSON default — avoid custom media types unless required.
Production challenges
Common REST production issues:
- 415 Unsupported Media Type: Client missing Content-Type: application/json.
- 406/Content negotiation: Client Accept header doesn't match produced types.
- Date/time zones: Always ISO-8601 UTC in JSON — Instant not LocalDateTime without zone.
- Duplicate validation: @Valid on controller but not on @RequestParam wrapper objects.
- OpenAPI drift: Annotations outdated — generate spec in CI and diff against published.
Common mistakes
- Returning JPA entities from @RestController — use response DTOs.
- Catching Exception in controller — swallows typed errors; use @ControllerAdvice.
- Using double for money — long cents or BigDecimal in DTOs.
- Missing @ResponseStatus on create — defaults 200 instead of 201.
- Exposing Swagger UI on public prod URL without authentication.
Debugging guide
Debug REST API issues:
- Enable HTTP logging: logging.level.org.springframework.web=DEBUG — see mapped handlers.
- curl -v: Inspect request/response headers and status codes.
- ProblemDetail body: Parse application/problem+json for validation errors.
- MockMvc tests: @WebMvcTest isolates controller — fast regression on status codes.
# Test create transfercurl -s -X POST localhost:8080/v1/transfers \-H 'Content-Type: application/json' \-d '{"fromAccountId":"","amountCents":-1,"idempotencyKey":"k1"}' | jq# OpenAPI speccurl -s localhost:8080/v3/api-docs | jq '.paths["/v1/transfers"]'
Best practices
- Thin controllers — validate, delegate to service, map to DTO.
- Use HTTP semantics — 201 + Location header on create, 204 on delete success.
- Version public APIs — /v1 prefix before external consumers depend on shape.
- Publish OpenAPI — contract-first mindset even if code-first generation.
- Idempotency keys on all non-safe POST operations in payment domains.
- RFC 7807 ProblemDetail for errors — consistent type, title, status, detail.
Anti-patterns
- God controller with 2000 lines and business logic inline.
- Map
as response — untyped, undocumented. - Using @RequestParam for complex objects instead of @RequestBody DTO.
- 500 for validation errors — should be 400 Bad Request.
- Changing JSON field names without version bump — breaks clients silently.
Staff engineer notes
- The HTTP layer is a published contract — review DTO changes like database migrations.
- ProblemDetail + stable error codes (TRANSFER_INSUFFICIENT_FUNDS) beats prose messages for clients.
- OpenAPI in CI: diff spec on PR, fail on breaking changes without /v2.
- Controller tests with @WebMvcTest catch 80% of routing/validation regressions in milliseconds.
- Money is never double in JSON — cents as long or string decimal per ISO 4217 patterns.
Interview questions
Interview preparation
15 questions grouped by difficulty — expand any card for a model answer and follow-up probe.
Beginner
1What is @RestController vs @Controller?
BeginnerModel answer
@RestController = @Controller + @ResponseBody on class.
Return values serialized directly to HTTP body (JSON via Jackson).
@Controller typically returns view name for template rendering.
REST APIs use @RestController.
Follow-up probe
@ResponseBody on method?
2Why use DTOs instead of JPA entities in REST?
BeginnerModel answer
Decouple API from persistence schema.
Prevent lazy-loading during serialization (N+1).
Hide internal fields.
Version API independently.
Avoid mass-assignment vulnerabilities.
Entities belong in service/repository layer; DTOs at HTTP boundary.
Follow-up probe
MapStruct vs manual mapping?
3How does @Valid work in Spring?
BeginnerModel answer
@Valid on @RequestBody triggers Bean Validation (JSR-380).
Annotations like @NotNull, @Positive on DTO fields.
Failure throws MethodArgumentNotValidException before controller method runs.
Handle in @ControllerAdvice → 400 with field errors.
Follow-up probe
Custom validator?
4What is @ControllerAdvice?
BeginnerModel answer
- Global component for cross-controller concerns
- typically @ExceptionHandler methods mapping exceptions to HTTP responses. Can also use @ModelAttribute and @InitBinder globally. Keeps controllers thin
- no try/catch in every method.
Follow-up probe
RestControllerAdvice?
5Explain HTTP status codes for REST.
BeginnerModel answer
200 OK GET success, 201 Created POST success, 204 No Content DELETE success, 400 Bad Request validation, 401 Unauthorized auth missing, 403 Forbidden no permission, 404 Not Found, 409 Conflict business rule (insufficient funds), 422 sometimes validation, 500 server error (avoid leaking details).
Follow-up probe
201 Location header?
Intermediate
6What is OpenAPI and springdoc?
IntermediateModel answer
- OpenAPI (Swagger) spec describes REST API
- paths, schemas, security. springdoc-openapi generates spec from Spring annotations at runtime. Endpoints: /v3/api-docs (JSON), /swagger-ui.html. Enables client codegen and contract testing.
Follow-up probe
Disable in prod?
7Design error response for validation failures.
IntermediateModel answer
Return 400 with RFC 7807 ProblemDetail or custom envelope: { code, message, errors: [{field, message}] }.
Same structure for all endpoints via @ControllerAdvice.
Clients parse programmatically.
Log correlation ID in response header.
Follow-up probe
ProblemDetail vs custom?
8How implement API versioning?
IntermediateModel answer
URL path /v1 (most common, explicit).
v1+json.
Query param (discouraged).
') or dedicated controller classes.
Breaking change → new version, deprecate old with sunset header.
Follow-up probe
When breaking change?
9Explain idempotency for POST /transfers.
IntermediateModel answer
Client sends Idempotency-Key header.
Server stores key → result mapping.
Duplicate POST with same key returns original result without re-processing.
Prevents double-charge on network retry.
Store in Redis/DB with TTL.
Return 201 first time, 200 on replay.
Follow-up probe
Idempotency vs dedup?
10Difference @RequestBody, @RequestParam, @PathVariable?
IntermediateModel answer
- @RequestBody: JSON body deserialized to object. @RequestParam: query string ?page=1. @PathVariable: URI template /transfers/{id}. Use appropriate source
- don't PUT complex objects in query params.
Follow-up probe
@RequestHeader?
Advanced
11Prevent N+1 in REST responses.
AdvancedModel answer
- Never return entity with lazy collections. Use DTO projection. @Transactional(readOnly=true) on service read method. JOIN FETCH or @EntityGraph in repository. Test with SQL logging enabled
- assert query count.
Follow-up probe
Spring Data projection?
12Secure REST API for external partners.
AdvancedModel answer
OAuth2 client credentials or mTLS.
Rate limiting at gateway.
Input validation.
Scope-based authorization.
Audit log all mutations.
WAF rules.
No stack traces in prod errors.
Pen test OpenAPI-documented surface.
Follow-up probe
JWT vs mTLS?
13Test REST controller without full context.
AdvancedModel answer
- @WebMvcTest(TransferController.class) loads MVC slice. @MockBean TransferService. MockMvc perform post/get
- assert status, JSON path. @AutoConfigureMockMvc. Faster than @SpringBootTest. Add @Import for security if needed.
Follow-up probe
Testcontainers for integration?
14Design pagination response envelope.
AdvancedModel answer
Spring Pageserializes to { content: [], totalElements, totalPages, size, number }. Or custom { data: [], meta: { page, total } }. Link headers (RFC 5988) optional. Always cap max page size. Cursor pagination for large datasets. Follow-up probe
Cursor vs offset?
15Architect breaking API change for 200 mobile clients.
AdvancedModel answer
- Ship /v2 alongside /v1. Deprecation header Sunset on v1. OpenAPI diff in CI. Mobile apps pin version
- force upgrade window. Feature flags for new fields optional in v1. Monitor v1 traffic
- retire when zero.
Follow-up probe
GraphQL alternative?
Hands-on exercise
Lab: REST API layer
- Create TransferController with POST and GET — use record DTOs.
- Add @Valid constraints — verify 400 response with field errors.
- Implement @RestControllerAdvice with ProblemDetail handlers.
- Add @Operation annotations — browse Swagger UI locally.
- Write @WebMvcTest asserting 201 on valid POST and 400 on invalid body.
JavaREST APIs: Controllers, DTOs, Validation, Exception Handling, OpenAPI
Press Run to execute. Real javac + JVM via remote API. Falls back to simulator on network failure.Architecture trade-offs
- Records vs classes for DTOs: Records immutable and concise; classes if inheritance needed.
- Code-first vs contract-first OpenAPI: Code-first faster; contract-first for strict partner governance.
- Monolithic controller vs split by resource: One controller per aggregate root.
- Synchronous MVC vs WebFlux: MVC + virtual threads simpler for most CRUD APIs.
Summary
Enterprise REST APIs are contracts — controllers, DTOs, validation, centralized exception handling, and OpenAPI together define what clients depend on. Design the HTTP boundary deliberately: map to DTOs, return proper status codes, and treat spec changes like schema migrations. Next: SQL fundamentals every Java backend engineer must know.
Key takeaways
- Thin @RestController — validate DTOs, delegate to service, return response DTOs.
- Never expose JPA entities — prevents N+1, leakage, and tight coupling.
- @ControllerAdvice + ProblemDetail — consistent error contract for all clients.
- OpenAPI documents your API — generate from springdoc, diff in CI on changes.
- Production: idempotency keys, pagination, versioning, secure Swagger, no stack trace leaks.