← Spring Boot®: applications, contracts and operations
03 / 10 · 60 MIN

HTTP routing and request contracts

Read the HTTP contract from mapping through arguments and response; distinguish method, representation and conversion failures.

The contract starts with mapping

A route is more than a path. The class declaration can define /shipments while the method adds /{id} and restricts the operation to GET. GET /shipments/42 can match; POST to the same path does not acquire that contract by sharing the URL.

Before reviewing business logic, write down the method, effective path and relevant conditions. Include context or proxy prefixes when present. This lesson assumes the request has reached MVC and no security layer or customization changes resolution.

Each argument has a source

PathVariable obtains a value captured in the path. RequestParam resolves Servlet parameters, including query and form data. RequestBody requests message reading by a compatible converter. Swapping these annotations changes the input contract; it is not equivalent syntax for the same endpoint.

For /quotes/BOX-A?units=3, sku comes from the path and units from the query. The int requires numeric conversion but does not require positivity. The next lesson adds explicit constraints. Correctly reading a value does not yet validate the business rule.

Omission, repetition and forms

A RequestParam without an optional setting or default is required. Decide whether omission is an error, should produce a default, or has a distinct meaning represented by Optional. Do not let that decision depend on an assumption about a primitive type’s initial value.

If the client sends tag=urgent&tag=fragile, use a collection to preserve multiplicity. A scalar-value map does not express the same need. With Servlet forms, parameter access can consume the body during parsing; do not plan to freely read it afterward as a fresh JSON body.

Negotiating input and output

Content-Type identifies the submitted representation. Accept expresses representations the client can receive. A POST can send JSON and accept JSON, but the two headers still answer different questions. consumes restricts input and produces restricts output. Changing Accept does not fix a body incorrectly identified as text/plain.

An important composition rule applies: method-level consumes and produces override their class-level values for that method. A JSON-consuming class can have a text-consuming method; a body need not simultaneously use both formats.

Status, headers and representation

ResponseEntity lets the application declare status, headers and body. Once creation has completed, created(uri).body(dto) expresses 201 with Location and a DTO. A converter must still be able to write that DTO in the negotiated format. Returning the literal "201" in the body does not mean the same thing.

The builder does not perform creation. If work was only accepted for processing, the contract should describe that actual state. Response writing can also fail after work has occurred; effect confirmation and retry policy require their own design.

Read the rejection stage

In an illustrative record, PUT /shipments receives 405 and the POST creation method is not called. The first difference to correct is the HTTP method. In another request, path and method match but text/plain meets a JSON-only contract: normal rejection is 415. When the conflict concerns Accept and available output, it is 406.

These statuses help form a hypothesis; they do not replace exception type and actual configuration. Do not attribute a SQL wait to a request rejected before the handler. Also establish that a gateway or custom handler did not produce the observed status.

Guided controller reading

In the example, consider three requests: units=3, omitted units, and units=three. The first allows QuoteInput to be built; the second fails the requirement; the third fails numeric conversion. The signature contains no constraint preventing units=-2. That request can be readable and still unsuitable for the business.

Also compare Accept: application/xml with the JSON declaration. With no other applicable handler, that output preference cannot be met. The exercise asks you to select and justify these interpretations; it requires no code execution and does not demonstrate a complete quotation service.

// Separate source file: QuoteController.java, inside the application scan root.
// Servlet Spring MVC; compatible JSON converter available.
package com.acme.shipping;
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.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping(path = "/quotes", produces = "application/json")
public class QuoteController {
  public record QuoteInput(String sku, int units) {}

  @GetMapping("/{sku}")
  public QuoteInput inspect(
      @PathVariable("sku") String sku,
      @RequestParam("units") int units) {
    return new QuoteInput(sku, units);
  }
}
IN PRACTICE

Interpret GET /quotes/BOX-A?units=3 with Accept: application/json: the path supplies BOX-A and the query supplies 3. The DTO only shows the arguments. It calculates no price, reserves no stock, and does not yet require positive units.

Common pitfalls

Confusing Accept with Content-Type; treating query parameters as a JSON body; losing repeated values; claiming creation merely by choosing 201; concluding that every 400 represents the same failure.

Related topics: Validation, errors and response contracts · Application context and dependencies · Test scope and integration evidence

Take this idea with you

First identify requests that can reach the method, then data that can be converted, and finally responses that can be represented. Mapping does not establish authorization, persistence or delivery.

Create account

Reference: Request mapping conditions · Spring Boot 4.1.1 / Spring Framework 7.0.9 / Java 25