Implementing a Versioned REST API with Spring Boot and URL Versioning
A versioned API lets clients evolve without breaking existing integrations. Spring Boot makes the mechanics straightforward, but the difficult work is defining stable resources, documenting migration paths and ensuring old releases continue to work safely.
URL versioning suits public web APIs because the version is visible in the path, easy to cache and simple for SDKs. This guide builds a practical order endpoint, then covers validation, errors, tests and rollout practices for teams serving Australian customers.
Choosing URL versioning for public contracts
Place the version before resource paths, such as /api/v1/orders and /api/v2/orders. Keep the resource model and HTTP semantics consistent across releases so /orders/{id} always identifies an order and POST /orders creates one. Header- or media-type-based versioning can suit controlled internal clients, but a visible path works well for public APIs and generated SDKs.
Australian integrations should treat AUD amounts, GST treatment and local payment methods such as PayID as part of the contract rather than burying those rules in client code. Teams seeking an outside perspective can also use Java API consulting to review the proposed boundaries.
Defining versioned Spring Boot endpoints
Use separate Spring controllers for each major contract. This keeps mappings and DTOs explicit, avoids version checks scattered through methods and lets each release be tested independently.
@RestController
@RequestMapping("/api/v1/orders")
class OrderController {
private final OrderService orderService;
OrderController(OrderService orderService) {
this.orderService = orderService;
}
@PostMapping
ResponseEntity<OrderResponse> create(
@Valid @RequestBody CreateOrderRequest request) {
OrderResponse body = orderService.create(request);
return ResponseEntity.created(
URI.create("/api/v1/orders/" + body.id())).body(body);
}
}
The service can reuse application logic while adapters translate between version-specific requests and responses. Add /api/v2 with a new controller and DTO package only when v2 requires incompatible fields or semantics. Spring Boot’s content negotiation remains available, but the URL must still identify the contract when the path itself is the versioning mechanism.
Protecting contracts with DTOs and validation
Map incoming JSON into request records instead of exposing entities. Bean Validation then rejects malformed quantities, unknown currencies or missing customer references before business logic runs. Include explicit limits such as @Positive, @Min and @Max, and preserve currency codes as ISO values.
Return a purpose-built response containing an order identifier, status, AUD total and creation timestamp, but omit internal notes or fraud signals. This separation prevents accidental data leakage and makes an incompatible response model a reason to publish another URL version rather than silently changing v1.
Protect each mapping with Spring Security’s OAuth 2.0 resource-server support and require scopes such as orders:read and orders:write. Keep token validation consistent across versions so a policy adjustment does not accidentally split the client base.
Handling errors and backward compatibility
Return consistent failures using ProblemDetail, extending it with a stable error code, human-readable detail and request trace identifier. A global @RestControllerAdvice can convert validation exceptions, missing resources and unexpected failures into JSON responses without exposing stack traces.
Only additive, backward-compatible changes should remain inside v1. For a breaking release, provide a migration window, publish Deprecation and Sunset headers, and link the replacement endpoint with an HTTP Link header. Under Australia’s Privacy Act 1988 and Australian Privacy Principles, assess whether changed fields affect personal information before retiring old behaviour.
Testing and documenting each release
Exercise each URL with MockMvc or an integration test that posts realistic Australian data, including GST and AUD amounts. Contract tests can compare status codes, headers and JSON shapes against the OpenAPI document, catching drift before partner teams deploy it.
Generate an OpenAPI file for every version and keep its examples separate. Include daylight-saving transition tests for Sydney and Melbourne, plus boundary cases for GST and currency rounding. In CI, run the maintained v1 contract suite against its implementation while adding v2 scenarios, then exercise partner callbacks in staging.
Applying a reliable rollout checklist
Operate versions as separate compatibility surfaces. Tag deployed images by API release, monitor traffic by the first path segment and alert when clients still call a deprecated version. Store timestamps in UTC, format them for users separately and retain audit records needed by Australian privacy and security practices.
Teams can apply this release checklist:
- Define v1 behaviour and publish an OpenAPI contract before coding.
- Use separate controllers and DTO packages for incompatible releases.
- Automate compatibility tests for every maintained URL version.
- Announce deprecation dates through headers, release notes and direct client outreach.
- Remove a version only after usage falls to an agreed threshold.