Java Fundamentals Tutorial 0/33 lessons ~6 min read Lesson 22

    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…

    Course progress0%
    Focus
    21 guided sections
    Practice signal
    Examples included
    Career prep
    Interview Q&A included

    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:

    text
    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 Created
    Exception path:
    InsufficientFundsException
    GlobalExceptionHandler (@ControllerAdvice)
    ProblemDetail (RFC 7807) → 409 Conflict + application/problem+json

    Controller layer, DTO boundary, validation, and error handling:

    REST request pipeline
    HTTP client
    Mobile / BFF
    @RestController
    Route
    @Valid DTO
    Validate
    Service
    Domain
    Controller stays thin — delegate to service after validation.
    DTO vs Entity boundary
    CreateTransferRequest
    API input
    TransferService
    Maps to domain
    Transfer entity
    JPA / DB
    TransferResponse
    API output
    Never return @Entity — map to response DTO.
    Exception → HTTP status
    Validation fail
    400
    Not found
    404
    Business rule
    409
    @ControllerAdvice
    Maps all
    Global handler — consistent ProblemDetail body.
    OpenAPI documentation
    Controller
    @Operation
    springdoc
    Scans
    /v3/api-docs
    JSON spec
    Swagger UI
    Interactive
    Spec generated from code — stays in sync with deploy.

    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.
    java
    // ── 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")
    @RequiredArgsConstructor
    public 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) ──
    @RestControllerAdvice
    public 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.
    java
    // API versioning — URL path (explicit, cache-friendly)
    @RequestMapping("/v1/transfers")
    // Idempotency — reject duplicate keys
    @Service
    public 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
    @GetMapping
    public Page<TransferResponse> list(
    @PageableDefault(size = 20, sort = "createdAt", direction = DESC) Pageable page) {
    return service.findAll(page).map(TransferResponse::from);
    }
    // springdoc — application.yml
    springdoc:
    api-docs:
    path: /v3/api-docs
    swagger-ui:
    path: /swagger-ui.html
    enabled: ${SWAGGER_ENABLED:false} # off in prod by default
    // Security — don't document internal admin paths
    @Bean
    GroupedOpenApi 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.
    bash
    # Test create transfer
    curl -s -X POST localhost:8080/v1/transfers \
    -H 'Content-Type: application/json' \
    -d '{"fromAccountId":"","amountCents":-1,"idempotencyKey":"k1"}' | jq
    # OpenAPI spec
    curl -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

    5
    1. 1What is @RestController vs @Controller?
      Beginner

      Model 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?

    2. 2Why use DTOs instead of JPA entities in REST?
      Beginner

      Model 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?

    3. 3How does @Valid work in Spring?
      Beginner

      Model 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?

    4. 4What is @ControllerAdvice?
      Beginner

      Model 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?

    5. 5Explain HTTP status codes for REST.
      Beginner

      Model 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

    5
    1. 6What is OpenAPI and springdoc?
      Intermediate

      Model 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?

    2. 7Design error response for validation failures.
      Intermediate

      Model 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?

    3. 8How implement API versioning?
      Intermediate

      Model 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?

    4. 9Explain idempotency for POST /transfers.
      Intermediate

      Model 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?

    5. 10Difference @RequestBody, @RequestParam, @PathVariable?
      Intermediate

      Model 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

    5
    1. 11Prevent N+1 in REST responses.
      Advanced

      Model 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?

    2. 12Secure REST API for external partners.
      Advanced

      Model 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?

    3. 13Test REST controller without full context.
      Advanced

      Model 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?

    4. 14Design pagination response envelope.
      Advanced

      Model answer

      Spring Page serializes 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?

    5. 15Architect breaking API change for 200 mobile clients.
      Advanced

      Model 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

    Starter Templates
    OutputRemote JVM (Piston · Java 15)
    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.
    Ready to mark this lesson complete?Track your journey across the entire course.