Course outline

Spring Boot / Basic: composition and HTTP contracts / Lesson 4

HTTP controllers and response contracts

Adapt HTTP input into application calls and map explicit result states into stable methods, status codes, headers, and serialized DTOs.

About 5 min readContent reviewed
On this page

Lesson goals

  • Separate route mapping from domain behavior.
  • Distinguish binding failure, missing resource, and rejected operation.
  • Define representations without exposing persistence internals.

A controller is an adapter, not the whole application

A servlet MVC request passes through filters, mapping, conversion, controller invocation, and response handling. The controller should translate transport concerns and delegate domain policy rather than own every database and security decision. @RestController combines controller discovery with response-body semantics; it does not make arbitrary fields safe to serialize or prove the caller may access a requested identifier.

Choose HTTP methods from a declared contract. GET retrieves a representation without a requested business mutation, POST commonly creates or submits work, and PUT, PATCH, and DELETE have different semantics. Idempotent HTTP semantics do not mean no logging or that any failed external action can be repeated safely. Retry-sensitive business identity is an additional design developed later.

Map typed application results explicitly

The controller fragment maps /products/{id} to a lookup capability and deliberately returns200 or404 according to its Optional result. It requires the contextual ProductService contract and managed bean. It is an unexecuted framework fragment, not a running endpoint. A malformed numeric path can fail binding before this method; a well-formed identifier with no record reaches a different result path.

Authorization must occur under a trusted identity and scope, with a consistent disclosure policy for absent versus forbidden resources. Returning404 can be a deliberate non-disclosure choice, but it must not substitute for actually checking access. Keep these states visible in the application contract instead of returning null and relying on incidental framework handling to communicate the business outcome.

java Illustrative lookup mapping with an explicit missing result
package training.catalog;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

record ProductView(long id, String name) {}

@RestController
@RequestMapping("/products")
class ProductController {
    private final ProductService products;
    ProductController(ProductService products) { this.products = products; }
    @GetMapping("/{id}")
    ResponseEntity<ProductView> find(@PathVariable long id) {
        return products.find(id).map(ResponseEntity::ok)
            .orElseGet(() -> ResponseEntity.notFound().build());
    }
}

The service is a contextual capability returning Optional<ProductView>. No controller mapping, security filter, or serialization was executed.

Write an intended request/response fixture

The HTTP block is a synthetic intended exchange for a known authorized product. It demonstrates path, Accept, Content-Type, status, and DTO fields. It is not a captured network response and is marked non-runnable. A MockMvc fixture can later verify the contract without a listener, while other integration tests check filters and the real application boundary under controlled dependencies.

Use a DTO that exposes only approved fields. Direct entity serialization can leak internal columns, traverse lazy relationships, create cycles, or trigger extra database work. Define names, numeric units, date conventions, nullability, and collection order deliberately. A valid JSON document alone does not prove that those semantics or the status code are correct.

http Intended synthetic HTTP contract, not an observed exchange
GET /products/42 HTTP/1.1
Accept: application/json

HTTP/1.1 200 OK
Content-Type: application/json

{"id":42,"name":"Training notebook"}

This fixture states the selected successful representation. Missing, forbidden, and binding-failure contracts require separate cases.

Use status and headers as semantic output

400 communicates malformed or invalid input under the chosen API contract;401 and403 concern authentication and authorization boundaries;404 indicates the selected missing/non-disclosure policy;409 can represent a defined state conflict.500 is not a convenient substitute for every rejected business request. Define successful creation and Location behavior where applicable, and avoid returning200 with an undocumented error-shaped body.

Content-Type describes the representation sent, while Accept describes requested response types. Unsupported input media and unacceptable output can fail at different stages. ETags or version fields can support an explicit optimistic-concurrency contract, but they require real comparison and update semantics. A header's presence alone does not protect against lost updates.

Test the adapter with precise cases

Test mapped and unmapped paths, malformed conversion, absent and authorized resources, denied access, media types, and the exact exposed fields. Keep request validation, domain rejection, and infrastructure failure distinguishable. Do not use a direct Java method call as proof of MVC binding or security behavior: it bypasses filters, argument resolvers, message conversion, and advice.

Choose a web slice or mock-based application fixture appropriate to the question, and retain separate unit tests for domain policy. Keep synthetic identities and no real credentials. The controller remains maintainable when its route, input contract, application call, and response mapping can be explained independently, with no hidden database mutation embedded in a supposedly read-only retrieval path.

Common errors and fixes

A direct controller unit test missed a binding failure

Why it happens
The test bypassed MVC argument conversion.
How to fix it
Use a suitable mock MVC fixture for the transport boundary.
Prevent it next time
Keep unit and web-slice evidence distinct.

A response exposed internal persistence fields

Why it happens
The entity became the public representation without review.
How to fix it
Map an approved DTO and test its exact field surface.
Prevent it next time
Review serialization and lazy relationships.

Every rejected request became500

Why it happens
Domain and input outcomes were not mapped explicitly.
How to fix it
Define stable status/error contracts for each boundary.
Prevent it next time
Test expected rejection paths.

Check your understanding

1. Why does calling find(42) directly not prove GET /products/42 is secured or serialized correctly?

Show worked solution

The call bypasses routing, filters, principal resolution, binding, response conversion, and exception advice. It can test application delegation only. Add scoped framework tests for HTTP and security behavior with synthetic data and an explicit configuration.

2. An amount is returned as price:12. What must the API clarify?

Show worked solution

Currency, scale or minor-unit convention, numeric precision, nullability, and whether it is a unit or total amount. A JSON number and a200 status are insufficient to establish business meaning. Choose a representation and verify it through the response contract.

Key takeaways

  • Controllers adapt transport and delegate policy.
  • Status, headers, DTO shape, and authorization are explicit contracts.
  • A synthetic HTTP fixture is intended behavior, not a captured response.

Official references