java
42 lines · 7 steps
Content-negotiated API versioning in Spring
One URL serves two response shapes by matching each request's Accept header to a versioned media type.
Explained by
highlit
1@RestController
2@RequestMapping("/api/orders")
3public class OrderController {
4
5 private final OrderService orderService;
6
7 public OrderController(OrderService orderService) {
8 this.orderService = orderService;
9 }
10
11 @GetMapping(value = "/{id}", produces = "application/vnd.acme.order.v1+json")
12 public ResponseEntity<OrderV1Response> getOrderV1(@PathVariable Long id) {
13 Order order = orderService.findById(id);
14 OrderV1Response body = new OrderV1Response(
15 order.getId(),
16 order.getCustomerName(),
17 order.getTotal()
18 );
19 return ResponseEntity.ok()
20 .contentType(MediaType.valueOf("application/vnd.acme.order.v1+json"))
21 .body(body);
22 }
23
24 @GetMapping(value = "/{id}", produces = "application/vnd.acme.order.v2+json")
25 public ResponseEntity<OrderV2Response> getOrderV2(@PathVariable Long id) {
26 Order order = orderService.findById(id);
27 OrderV2Response body = new OrderV2Response(
28 order.getId(),
29 new CustomerSummary(order.getCustomerId(), order.getCustomerName()),
30 Money.of(order.getTotal(), order.getCurrency()),
31 order.getStatus()
32 );
33 return ResponseEntity.ok()
34 .contentType(MediaType.valueOf("application/vnd.acme.order.v2+json"))
35 .body(body);
36 }
37
38 @ExceptionHandler(OrderNotFoundException.class)
39 public ResponseEntity<Void> handleNotFound() {
40 return ResponseEntity.notFound().build();
41 }
42}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Custom vendor media types let one endpoint expose multiple response versions without polluting the URL path.
- 2Spring routes to the handler whose produces value matches the client's Accept header, so version selection is negotiated per request.
- 3A single @ExceptionHandler centralizes error mapping for every method in the controller.
Related explainers
python
from flask import Blueprint, jsonify, request, abort v1 = Blueprint("users_v1", __name__) v2 = Blueprint("users_v2", __name__)
Versioning a Flask API with Blueprints
api versioning
blueprints
rest
Intermediate
7 steps
java
public class SlidingLogRateLimiter { private final int maxRequests; private final long windowMillis;
How a sliding-log rate limiter works
rate-limiting
concurrency
sliding-window
Advanced
8 steps
typescript
import { Injectable, UnauthorizedException } from '@nestjs/common'; import { PassportStrategy } from '@nestjs/passport'; import { ExtractJwt, Strategy } from 'passport-jwt'; import { ConfigService } from '@nestjs/config';
How a JWT strategy authenticates in NestJS
authentication
jwt
passport
Intermediate
7 steps
java
public final class Slugifier { private static final Pattern NON_LATIN = Pattern.compile("[^\\w-]"); private static final Pattern WHITESPACE = Pattern.compile("[\\s]+");
Building a URL slugifier in Java
regex
unicode-normalization
string-processing
Intermediate
8 steps
java
@Repository public interface OrderRepository extends JpaRepository<Order, Long> { @Query("""
Keyset pagination with Spring Data JPA
pagination
cursor
jpa
Advanced
8 steps
python
import os from datetime import timedelta
Class-based config in a Flask app factory
configuration
app-factory
inheritance
Intermediate
7 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/content-negotiated-api-versioning-in-spring-explained-java-6956/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.