From ac25843c64f6bb7f46494a4967444aff24f71020 Mon Sep 17 00:00:00 2001 From: Sandip Kumar Roy Date: Tue, 25 Nov 2025 13:50:04 +0530 Subject: [PATCH] Add spring-boot-api-versioning module demonstrating multiple API versioning strategies --- .../spring-boot-api-versioning/.gitignore | 25 +++++ .../spring-boot-api-versioning/README.md | 99 +++++++++++++++++++ .../spring-boot-api-versioning/pom.xml | 73 ++++++++++++++ .../ApiVersioningDemoApplication.java | 11 +++ .../controller/UserParamController.java | 27 +++++ .../header/UserHeaderController.java | 27 +++++ .../controller/mime/UserMimeController.java | 36 +++++++ .../UserContentNegotiationController.java | 20 ++++ .../controller/v1/UserV1Controller.java | 14 +++ .../controller/v2/UserV2Controller.java | 14 +++ .../example/apiversioning/model/UserV1.java | 9 ++ .../example/apiversioning/model/UserV2.java | 18 ++++ .../ApiVersioningDemoApplicationTests.java | 12 +++ 13 files changed, 385 insertions(+) create mode 100644 spring-boot-modules/spring-boot-api-versioning/.gitignore create mode 100644 spring-boot-modules/spring-boot-api-versioning/README.md create mode 100644 spring-boot-modules/spring-boot-api-versioning/pom.xml create mode 100644 spring-boot-modules/spring-boot-api-versioning/src/main/java/com/baeldung/example/apiversioning/ApiVersioningDemoApplication.java create mode 100644 spring-boot-modules/spring-boot-api-versioning/src/main/java/com/baeldung/example/apiversioning/controller/UserParamController.java create mode 100644 spring-boot-modules/spring-boot-api-versioning/src/main/java/com/baeldung/example/apiversioning/controller/header/UserHeaderController.java create mode 100644 spring-boot-modules/spring-boot-api-versioning/src/main/java/com/baeldung/example/apiversioning/controller/mime/UserMimeController.java create mode 100644 spring-boot-modules/spring-boot-api-versioning/src/main/java/com/baeldung/example/apiversioning/controller/negotiation/UserContentNegotiationController.java create mode 100644 spring-boot-modules/spring-boot-api-versioning/src/main/java/com/baeldung/example/apiversioning/controller/v1/UserV1Controller.java create mode 100644 spring-boot-modules/spring-boot-api-versioning/src/main/java/com/baeldung/example/apiversioning/controller/v2/UserV2Controller.java create mode 100644 spring-boot-modules/spring-boot-api-versioning/src/main/java/com/baeldung/example/apiversioning/model/UserV1.java create mode 100644 spring-boot-modules/spring-boot-api-versioning/src/main/java/com/baeldung/example/apiversioning/model/UserV2.java create mode 100644 spring-boot-modules/spring-boot-api-versioning/src/test/java/com/baeldung/example/apiversioning/ApiVersioningDemoApplicationTests.java diff --git a/spring-boot-modules/spring-boot-api-versioning/.gitignore b/spring-boot-modules/spring-boot-api-versioning/.gitignore new file mode 100644 index 000000000000..312265b8c1f4 --- /dev/null +++ b/spring-boot-modules/spring-boot-api-versioning/.gitignore @@ -0,0 +1,25 @@ +# Maven build +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties + +# IDE files +.idea/ +*.iml +*.ipr +*.iws +.vscode/ +.project +.classpath +.settings/ + +# OS files +.DS_Store +Thumbs.db + +# Logs +logs/ +*.log \ No newline at end of file diff --git a/spring-boot-modules/spring-boot-api-versioning/README.md b/spring-boot-modules/spring-boot-api-versioning/README.md new file mode 100644 index 000000000000..c633e58cd08a --- /dev/null +++ b/spring-boot-modules/spring-boot-api-versioning/README.md @@ -0,0 +1,99 @@ +# spring-boot-api-versioning +Demonstrating API versioning strategies in Spring Boot +# API Versioning in Spring Boot + +[![Spring Boot](https://img.shields.io/badge/Spring%20Boot-3.3+-brightgreen)](https://spring.io/projects/spring-boot) +[![Java](https://img.shields.io/badge/Java-17+-blue)](https://openjdk.org/) +[![Build](https://img.shields.io/badge/Build-Maven-orange)](https://maven.apache.org/) + +A demo project showcasing **different strategies for API versioning in Spring Boot**, including: + +- URI Versioning +- Request Parameter Versioning +- Header Versioning +- Content Negotiation (MIME Type) +- Native Spring Boot 4 Annotation Support + +--- + +## Project Setup + +Clone the repository: + +```bash +git clone https://github.com/your-org/api-versioning-spring-boot +cd api-versioning-spring-boot +``` + +Build and run: + +```bash +./mvnw spring-boot:run +``` + +--- + +## Usage Examples + +### 1. URI Versioning +```bash +curl http://localhost:8080/api/v1/users +curl http://localhost:8080/api/v2/users +``` + +### 2. Request Parameter Versioning +```bash +curl http://localhost:8080/api/users?version=1 +curl http://localhost:8080/api/users?version=2 +``` + +### 3. Header Versioning +```bash +curl -H "API-VERSION: 1" http://localhost:8080/api/users +curl -H "API-VERSION: 2" http://localhost:8080/api/users +``` + +### 4. Content Negotiation +```bash +curl -H "Accept: application/vnd.company.v1+json" http://localhost:8080/api/users +curl -H "Accept: application/vnd.company.v2+json" http://localhost:8080/api/users +``` + +### 5. Native Annotation Support (Spring Boot 4) +```bash +curl http://localhost:8080/api/users --header "API-VERSION: 1" +curl http://localhost:8080/api/users --header "API-VERSION: 2" +``` + +--- + +## Tutorial Reference + +This project accompanies the post: +**[API Versioning in Spring](#)** +- (link will be added once published) +--- + +## Running Tests + +```bash +./mvnw test +``` + +--- + +## Best Practices + +- Version only when necessary +- Document changes clearly +- Deprecate gracefully +- Automate testing across versions +- Align with business needs + +--- + +## 📜 License + +This project is licensed under the MIT License. + +--- diff --git a/spring-boot-modules/spring-boot-api-versioning/pom.xml b/spring-boot-modules/spring-boot-api-versioning/pom.xml new file mode 100644 index 000000000000..1660c504e96e --- /dev/null +++ b/spring-boot-modules/spring-boot-api-versioning/pom.xml @@ -0,0 +1,73 @@ + + 4.0.0 + + com.baeldung.example + spring-boot-api-versioning + 0.0.1-SNAPSHOT + spring-boot-api-versioning + API versioning strategies in Spring Boot + + + 17 + 3.3.4 + + + + + + + org.springframework.boot + spring-boot-dependencies + ${spring.boot.version} + pom + import + + + + + + + + org.springframework.boot + spring-boot-starter-web + + + + + org.springframework.boot + spring-boot-starter-actuator + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + ${java.version} + ${java.version} + + -parameters + + + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring.boot.version} + + + + \ No newline at end of file diff --git a/spring-boot-modules/spring-boot-api-versioning/src/main/java/com/baeldung/example/apiversioning/ApiVersioningDemoApplication.java b/spring-boot-modules/spring-boot-api-versioning/src/main/java/com/baeldung/example/apiversioning/ApiVersioningDemoApplication.java new file mode 100644 index 000000000000..ab63019c76b5 --- /dev/null +++ b/spring-boot-modules/spring-boot-api-versioning/src/main/java/com/baeldung/example/apiversioning/ApiVersioningDemoApplication.java @@ -0,0 +1,11 @@ +package com.baeldung.example.apiversioning; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class ApiVersioningDemoApplication { + public static void main(String[] args) { + SpringApplication.run(ApiVersioningDemoApplication.class, args); + } +} \ No newline at end of file diff --git a/spring-boot-modules/spring-boot-api-versioning/src/main/java/com/baeldung/example/apiversioning/controller/UserParamController.java b/spring-boot-modules/spring-boot-api-versioning/src/main/java/com/baeldung/example/apiversioning/controller/UserParamController.java new file mode 100644 index 000000000000..3fe6bd40671c --- /dev/null +++ b/spring-boot-modules/spring-boot-api-versioning/src/main/java/com/baeldung/example/apiversioning/controller/UserParamController.java @@ -0,0 +1,27 @@ +package com.baeldung.example.apiversioning.controller; + +import com.baeldung.example.apiversioning.model.UserV2; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +public class UserParamController { + +@GetMapping("/api/users") +public Object getUsers(@RequestParam(name = "version", defaultValue = "1") String version) { + if ("1".equals(version)) { + return List.of("Alice", "Bob"); + } else if ("2".equals(version)) { + return List.of( + new UserV2("Alice", "alice@example.com", 30), + new UserV2("Bob", "bob@example.com", 25) + ); + } else { + return "Unsupported API version"; + } +} + +} \ No newline at end of file diff --git a/spring-boot-modules/spring-boot-api-versioning/src/main/java/com/baeldung/example/apiversioning/controller/header/UserHeaderController.java b/spring-boot-modules/spring-boot-api-versioning/src/main/java/com/baeldung/example/apiversioning/controller/header/UserHeaderController.java new file mode 100644 index 000000000000..07383e76ec97 --- /dev/null +++ b/spring-boot-modules/spring-boot-api-versioning/src/main/java/com/baeldung/example/apiversioning/controller/header/UserHeaderController.java @@ -0,0 +1,27 @@ +// src/main/java/.../controller/header/UserHeaderController.java +package com.baeldung.example.apiversioning.controller.header; + +import com.baeldung.example.apiversioning.model.UserV1; +import com.baeldung.example.apiversioning.model.UserV2; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +public class UserHeaderController { + + @GetMapping(value = "/api/users", headers = "X-API-VERSION=1") +public List getUsersV1() { + return List.of(new UserV1("Alice"), new UserV1("Bob")); +} + +@GetMapping(value = "/api/users", headers = "X-API-VERSION=2") +public List getUsersV2() { + return List.of( + new UserV2("Alice", "alice@example.com", 30), + new UserV2("Bob", "bob@example.com", 25) + ); +} +} \ No newline at end of file diff --git a/spring-boot-modules/spring-boot-api-versioning/src/main/java/com/baeldung/example/apiversioning/controller/mime/UserMimeController.java b/spring-boot-modules/spring-boot-api-versioning/src/main/java/com/baeldung/example/apiversioning/controller/mime/UserMimeController.java new file mode 100644 index 000000000000..9d2550b50b8a --- /dev/null +++ b/spring-boot-modules/spring-boot-api-versioning/src/main/java/com/baeldung/example/apiversioning/controller/mime/UserMimeController.java @@ -0,0 +1,36 @@ +// src/main/java/com/baeldung/example/apiversioning/controller/mime/UserMimeController.java +package com.baeldung.example.apiversioning.controller.mime; + +import com.baeldung.example.apiversioning.model.UserV1; +import com.baeldung.example.apiversioning.model.UserV2; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +public class UserMimeController { + + public static final String V1_MEDIA = "application/vnd.example.users-v1+json"; + public static final String V2_MEDIA = "application/vnd.example.users-v2+json"; + + @GetMapping(value = "/api/users", produces = V1_MEDIA) + public List usersV1() { + return List.of(new UserV1("Alice"), new UserV1("Bob")); + } + + @GetMapping(value = "/api/users", produces = V2_MEDIA) + public List usersV2() { + return List.of( + new UserV2("Alice", "alice@example.com", 30), + new UserV2("Bob", "bob@example.com", 25) + ); + } + + // Optional fallback + @GetMapping(value = "/api/users", produces = MediaType.APPLICATION_JSON_VALUE) + public List defaultUsers() { + return List.of(new UserV1("Alice"), new UserV1("Bob")); + } +} \ No newline at end of file diff --git a/spring-boot-modules/spring-boot-api-versioning/src/main/java/com/baeldung/example/apiversioning/controller/negotiation/UserContentNegotiationController.java b/spring-boot-modules/spring-boot-api-versioning/src/main/java/com/baeldung/example/apiversioning/controller/negotiation/UserContentNegotiationController.java new file mode 100644 index 000000000000..39d7752e3291 --- /dev/null +++ b/spring-boot-modules/spring-boot-api-versioning/src/main/java/com/baeldung/example/apiversioning/controller/negotiation/UserContentNegotiationController.java @@ -0,0 +1,20 @@ +package com.baeldung.example.apiversioning.controller.negotiation; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/users") +public class UserContentNegotiationController { + + @GetMapping(value = "/negotiation", produces = "application/vnd.col.users.v1+json") + public String getUsersV1() { + return "User list v1"; + } + + @GetMapping(value = "/negotiation", produces = "application/vnd.col.users.v2+json") + public String getUsersV2() { + return "User list v2"; + } +} \ No newline at end of file diff --git a/spring-boot-modules/spring-boot-api-versioning/src/main/java/com/baeldung/example/apiversioning/controller/v1/UserV1Controller.java b/spring-boot-modules/spring-boot-api-versioning/src/main/java/com/baeldung/example/apiversioning/controller/v1/UserV1Controller.java new file mode 100644 index 000000000000..a17913d62adb --- /dev/null +++ b/spring-boot-modules/spring-boot-api-versioning/src/main/java/com/baeldung/example/apiversioning/controller/v1/UserV1Controller.java @@ -0,0 +1,14 @@ +package com.baeldung.example.apiversioning.controller.v1; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/v1/users") +public class UserV1Controller { + @GetMapping + public String getUsersV1() { + return "User list from API v1"; + } +} \ No newline at end of file diff --git a/spring-boot-modules/spring-boot-api-versioning/src/main/java/com/baeldung/example/apiversioning/controller/v2/UserV2Controller.java b/spring-boot-modules/spring-boot-api-versioning/src/main/java/com/baeldung/example/apiversioning/controller/v2/UserV2Controller.java new file mode 100644 index 000000000000..f08158304d87 --- /dev/null +++ b/spring-boot-modules/spring-boot-api-versioning/src/main/java/com/baeldung/example/apiversioning/controller/v2/UserV2Controller.java @@ -0,0 +1,14 @@ +package com.baeldung.example.apiversioning.controller.v2; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/v2/users") +public class UserV2Controller { + @GetMapping + public String getUsersV2() { + return "User list from API v2 with extra fields"; + } +} \ No newline at end of file diff --git a/spring-boot-modules/spring-boot-api-versioning/src/main/java/com/baeldung/example/apiversioning/model/UserV1.java b/spring-boot-modules/spring-boot-api-versioning/src/main/java/com/baeldung/example/apiversioning/model/UserV1.java new file mode 100644 index 000000000000..43fffe0a02f1 --- /dev/null +++ b/spring-boot-modules/spring-boot-api-versioning/src/main/java/com/baeldung/example/apiversioning/model/UserV1.java @@ -0,0 +1,9 @@ +package com.baeldung.example.apiversioning.model; + +public class UserV1 { + private String name; + public UserV1() {} + public UserV1(String name) { this.name = name; } + public String getName() { return name; } + public void setName(String name) { this.name = name; } +} \ No newline at end of file diff --git a/spring-boot-modules/spring-boot-api-versioning/src/main/java/com/baeldung/example/apiversioning/model/UserV2.java b/spring-boot-modules/spring-boot-api-versioning/src/main/java/com/baeldung/example/apiversioning/model/UserV2.java new file mode 100644 index 000000000000..55aef7081ce0 --- /dev/null +++ b/spring-boot-modules/spring-boot-api-versioning/src/main/java/com/baeldung/example/apiversioning/model/UserV2.java @@ -0,0 +1,18 @@ +package com.baeldung.example.apiversioning.model; + +public class UserV2 { + private String name; + private String email; + private int age; + + public UserV2(String name, String email, int age) { + this.name = name; + this.email = email; + this.age = age; + } + + // getters + public String getName() { return name; } + public String getEmail() { return email; } + public int getAge() { return age; } +} \ No newline at end of file diff --git a/spring-boot-modules/spring-boot-api-versioning/src/test/java/com/baeldung/example/apiversioning/ApiVersioningDemoApplicationTests.java b/spring-boot-modules/spring-boot-api-versioning/src/test/java/com/baeldung/example/apiversioning/ApiVersioningDemoApplicationTests.java new file mode 100644 index 000000000000..f8f1809ccb66 --- /dev/null +++ b/spring-boot-modules/spring-boot-api-versioning/src/test/java/com/baeldung/example/apiversioning/ApiVersioningDemoApplicationTests.java @@ -0,0 +1,12 @@ +package com.baeldung.example.apiversioning; + +import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; + +class ApiVersioningDemoApplicationTests { + + @Test + void contextLoads() { + assertThat(true).isTrue(); + } +}