{"id":3940,"date":"2025-12-17T09:40:35","date_gmt":"2025-12-17T14:40:35","guid":{"rendered":"https:\/\/www.mymiller.name\/wordpress\/?p=3940"},"modified":"2025-12-17T09:40:38","modified_gmt":"2025-12-17T14:40:38","slug":"spring-boot-4-mastering-rest-api-versioning-with-apiversion","status":"publish","type":"post","link":"https:\/\/www.mymiller.name\/wordpress\/spring\/spring4\/spring-boot-4-mastering-rest-api-versioning-with-apiversion\/","title":{"rendered":"Spring Boot 4: Mastering REST API Versioning with @ApiVersion"},"content":{"rendered":"\n<p>As software architects, we often face the challenge of evolving our APIs without breaking the contracts relied upon by existing clients. For years, Spring developers had to rely on manual workarounds\u2014custom <code>RequestCondition<\/code> implementations, path pattern matching, or third-party libraries\u2014to handle versioning.<\/p>\n\n\n\n<p>With the release of <strong>Spring Boot 4<\/strong> (built on <strong>Spring Framework 7<\/strong>), native support for API versioning has finally arrived. The new <code>@ApiVersion<\/code> annotation standardizes how we manage the lifecycle of our endpoints, making our codebases cleaner and more maintainable.<\/p>\n\n\n\n<p>In this post, we will explore how to configure and implement this new feature using a clean, architectural approach.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">1. The Strategy: Header vs. Path vs. Media Type<\/h2>\n\n\n\n<p>Before writing code, we must choose a versioning strategy. Spring Boot 4 supports multiple strategies, but three stand out for different architectural needs:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Header Versioning (Recommended):<\/strong> Clients send a custom header (e.g., <code>X-API-VERSION: 1<\/code>). This keeps URLs clean and separates metadata from the resource identifier.<\/li>\n\n\n\n<li><strong>Media Type Versioning (The Purist&#8217;s Choice):<\/strong> Clients specify the version in the <code>Accept<\/code> header (e.g., <code>Accept: application\/json;version=1<\/code>). This strictly adheres to REST principles: the resource URI is constant, but the <em>representation<\/em> changes.<\/li>\n\n\n\n<li><strong>Path Versioning:<\/strong> The version is embedded in the URI (<code>\/api\/v1\/customers<\/code>). This is pragmatic and cache-friendly, though it technically changes the resource identity.<\/li>\n<\/ol>\n\n\n\n<p><em>Note: Spring also supports Query Parameter versioning (<code>?version=1<\/code>), but we strictly avoid this to keep request parameters reserved for filtering\/sorting logic, especially on <code>GET<\/code> requests.<\/em><\/p>\n\n\n\n<h2 class=\"wp-block-heading\">2. Configuration<\/h2>\n\n\n\n<p>First, ensure you are using the latest Spring Boot 4 starter in your <code>build.gradle<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>dependencies {\n    implementation 'org.springframework.boot:spring-boot-starter-web'\n}\n<\/code><\/pre>\n\n\n\n<p>Next, we need to configure the <code>ApiVersionStrategy<\/code>. You can choose <strong>one<\/strong> of the following strategies based on your architecture.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Option A: Header Configuration (Standard)<\/h3>\n\n\n\n<p>This configuration keeps your URLs clean.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import org.springframework.context.annotation.Configuration;\nimport org.springframework.web.servlet.config.annotation.ApiVersionConfigurer;\nimport org.springframework.web.servlet.config.annotation.WebMvcConfigurer;\n\n@Configuration\npublic class WebConfig implements WebMvcConfigurer {\n\n    @Override\n    public void configureApiVersioning(ApiVersionConfigurer configurer) {\n        configurer\n            \/\/ Use a custom header for versioning\n            .useRequestHeader(\"X-API-VERSION\")\n            .setDefaultVersion(\"1\")\n            .useDefaultVersionWhenMissing(); \n    }\n}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Option B: Media Type Configuration (Architect&#8217;s Choice)<\/h3>\n\n\n\n<p>This uses standard content negotiation.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import org.springframework.http.MediaType;\n\n@Configuration\npublic class WebConfig implements WebMvcConfigurer {\n\n    @Override\n    public void configureApiVersioning(ApiVersionConfigurer configurer) {\n        configurer\n            \/\/ Client sends: Accept: application\/json;version=1\n            .useMediaTypeParameter(MediaType.APPLICATION_JSON, \"version\")\n            .setDefaultVersion(\"1\");\n    }\n}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Option C: Path Configuration (Pragmatic Choice)<\/h3>\n\n\n\n<p>If you prefer versions in the URL (e.g., <code>\/api\/v1\/customers<\/code>), use this configuration.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>@Configuration\npublic class WebConfig implements WebMvcConfigurer {\n\n    @Override\n    public void configureApiVersioning(ApiVersionConfigurer configurer) {\n        configurer\n            \/\/ Extract version from the 2nd path segment (index 1)\n            \/\/ Example: \/api\/v1\/customers -&gt; \"v1\" is at index 1\n            .usePathSegment(1) \n            .setDefaultVersion(\"1\");\n    }\n}\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">3. Implementation with <code>@ApiVersion<\/code><\/h2>\n\n\n\n<p>The <code>@ApiVersion<\/code> annotation works the same way regardless of your configuration, but your Controller&#8217;s <code>@RequestMapping<\/code> must align with your strategy.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Implementation for Header &amp; Media Type Strategies<\/h3>\n\n\n\n<p>Both of these strategies keep the URL path clean.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import org.springframework.web.bind.annotation.*;\nimport org.springframework.core.annotation.ApiVersion;\n\n@RestController\n@RequestMapping(\"\/api\/customers\")\npublic class CustomerController {\n\n    \/\/ ---------------------------------------------------------\n    \/\/ VERSION 1: Legacy Structure\n    \/\/ ---------------------------------------------------------\n    @ApiVersion(\"1\")\n    @GetMapping(\"\/{id}\")\n    public CustomerV1 getCustomerV1(@PathVariable String id) {\n        return customerService.findV1(id);\n    }\n\n    \/\/ ---------------------------------------------------------\n    \/\/ VERSION 2: Modern Structure\n    \/\/ ---------------------------------------------------------\n    @ApiVersion(\"2\")\n    @GetMapping(\"\/{id}\")\n    public CustomerV2 getCustomerV2(@PathVariable String id) {\n        return customerService.findV2(id);\n    }\n}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Implementation for Path Strategy<\/h3>\n\n\n\n<p>If you chose <strong>Option C (Path)<\/strong>, you <strong>must<\/strong> include the version variable in your route path.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>@RestController\n\/\/ Notice the {version} placeholder in the path\n@RequestMapping(\"\/api\/{version}\/customers\") \npublic class CustomerController {\n\n    @ApiVersion(\"1\")\n    @GetMapping(\"\/{id}\")\n    public CustomerV1 getCustomerV1(@PathVariable String id) {\n        return customerService.findV1(id);\n    }\n\n    @ApiVersion(\"2\")\n    @GetMapping(\"\/{id}\")\n    public CustomerV2 getCustomerV2(@PathVariable String id) {\n        return customerService.findV2(id);\n    }\n}\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">4. Testing the API<\/h2>\n\n\n\n<p>Here is how you request specific versions depending on your chosen strategy.<\/p>\n\n\n\n<p><strong>Testing Header Strategy:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>curl -X GET http:\/\/localhost:8080\/api\/customers\/101 \\\n     -H \"X-API-VERSION: 2\"\n<\/code><\/pre>\n\n\n\n<p><strong>Testing Media Type Strategy:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>curl -X GET http:\/\/localhost:8080\/api\/customers\/101 \\\n     -H \"Accept: application\/json;version=2\"\n<\/code><\/pre>\n\n\n\n<p><strong>Testing Path Strategy:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>curl -X GET http:\/\/localhost:8080\/api\/2\/customers\/101\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Conclusion<\/h2>\n\n\n\n<p>Native versioning in Spring Boot 4 removes the need for complex AOP wrappers. Whether you prefer the cleanliness of <strong>Header\/Media Type Versioning<\/strong> or the explicitness of <strong>Path Versioning<\/strong>, the <code>@ApiVersion<\/code> annotation provides a unified way to manage your API&#8217;s lifecycle.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>As software architects, we often face the challenge of evolving our APIs without breaking the contracts relied upon by existing clients. For years, Spring developers had to rely on manual workarounds\u2014custom RequestCondition implementations, path pattern matching, or third-party libraries\u2014to handle versioning. With the release of Spring Boot 4 (built on Spring Framework 7), native support [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":3942,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_coblocks_attr":"","_coblocks_dimensions":"","_coblocks_responsive_height":"","_coblocks_accordion_ie_support":"","jetpack_post_was_ever_published":false,"_jetpack_newsletter_access":"","_jetpack_dont_email_post_to_subs":false,"_jetpack_newsletter_tier_id":0,"_jetpack_memberships_contains_paywalled_content":false,"_jetpack_memberships_contains_paid_content":false,"footnotes":"","jetpack_publicize_message":"","jetpack_publicize_feature_enabled":true,"jetpack_social_post_already_shared":true,"jetpack_social_options":{"image_generator_settings":{"template":"highway","default_image_id":0,"font":"","enabled":false},"version":2}},"categories":[483],"tags":[319,484],"series":[],"class_list":["post-3940","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-spring4","tag-spring","tag-spring4"],"jetpack_publicize_connections":[],"jetpack_featured_media_url":"https:\/\/www.mymiller.name\/wordpress\/wp-content\/uploads\/2025\/12\/brain-2029391_1280.avif","jetpack-related-posts":[{"id":3947,"url":"https:\/\/www.mymiller.name\/wordpress\/spring\/spring4\/goodbye-boilerplate-mastering-declarative-http-clients-in-spring-boot\/","url_meta":{"origin":3940,"position":0},"title":"Goodbye Boilerplate: Mastering Declarative HTTP Clients in Spring Boot","author":"Jeffery Miller","date":"December 19, 2025","format":false,"excerpt":"For years, calling remote REST APIs in Spring Boot meant one of two things: wrestling with the aging, blocking RestTemplate, or writing verbose, reactive boilerplate with WebClient. While libraries like Spring Cloud Feign offered a cleaner, declarative approach, they required extra dependencies and configuration. With the arrival of Spring Framework\u2026","rel":"","context":"In &quot;Spring4&quot;","block_context":{"text":"Spring4","link":"https:\/\/www.mymiller.name\/wordpress\/category\/spring\/spring4\/"},"img":{"alt_text":"","src":"https:\/\/www.mymiller.name\/wordpress\/wp-content\/uploads\/2025\/12\/putty-3678638_1280.avif","width":350,"height":200,"srcset":"https:\/\/www.mymiller.name\/wordpress\/wp-content\/uploads\/2025\/12\/putty-3678638_1280.avif 1x, https:\/\/www.mymiller.name\/wordpress\/wp-content\/uploads\/2025\/12\/putty-3678638_1280.avif 1.5x, https:\/\/www.mymiller.name\/wordpress\/wp-content\/uploads\/2025\/12\/putty-3678638_1280.avif 2x, https:\/\/www.mymiller.name\/wordpress\/wp-content\/uploads\/2025\/12\/putty-3678638_1280.avif 3x"},"classes":[]},{"id":3944,"url":"https:\/\/www.mymiller.name\/wordpress\/spring\/spring4\/goodbye-resilience4j-native-fault-tolerance-in-spring-boot-4\/","url_meta":{"origin":3940,"position":1},"title":"Goodbye Resilience4j? Native Fault Tolerance in Spring Boot 4","author":"Jeffery Miller","date":"December 18, 2025","format":false,"excerpt":"For years, the standard advice for building resilient Spring Boot microservices was simple: add Resilience4j. It became the Swiss Army knife for circuit breakers, rate limiters, and retries. However, with the release of Spring Boot 4, the landscape has shifted. The framework now promotes a \"batteries-included\" philosophy for fault tolerance.\u2026","rel":"","context":"In &quot;Spring4&quot;","block_context":{"text":"Spring4","link":"https:\/\/www.mymiller.name\/wordpress\/category\/spring\/spring4\/"},"img":{"alt_text":"","src":"https:\/\/www.mymiller.name\/wordpress\/wp-content\/uploads\/2025\/12\/iduino-uno-r3b-1699990_1280.avif","width":350,"height":200,"srcset":"https:\/\/www.mymiller.name\/wordpress\/wp-content\/uploads\/2025\/12\/iduino-uno-r3b-1699990_1280.avif 1x, https:\/\/www.mymiller.name\/wordpress\/wp-content\/uploads\/2025\/12\/iduino-uno-r3b-1699990_1280.avif 1.5x, https:\/\/www.mymiller.name\/wordpress\/wp-content\/uploads\/2025\/12\/iduino-uno-r3b-1699990_1280.avif 2x, https:\/\/www.mymiller.name\/wordpress\/wp-content\/uploads\/2025\/12\/iduino-uno-r3b-1699990_1280.avif 3x"},"classes":[]},{"id":3961,"url":"https:\/\/www.mymiller.name\/wordpress\/spring\/spring4\/architecting-spring-boot-4-with-official-spring-grpc-support\/","url_meta":{"origin":3940,"position":2},"title":"Architecting Spring Boot 4 with Official Spring gRPC Support","author":"Jeffery Miller","date":"January 15, 2026","format":false,"excerpt":"For years, the Spring community relied on excellent third-party starters (like net.devh) to bridge the gap between Spring Boot and gRPC. With the evolution of Spring Boot 4 and the official Spring gRPC project, we now have native support that aligns perfectly with Spring's dependency injection, observability, and configuration models.\u2026","rel":"","context":"In &quot;Spring4&quot;","block_context":{"text":"Spring4","link":"https:\/\/www.mymiller.name\/wordpress\/category\/spring\/spring4\/"},"img":{"alt_text":"","src":"https:\/\/www.mymiller.name\/wordpress\/wp-content\/uploads\/2026\/01\/Gemini_Generated_Image_3yqio33yqio33yqi.avif","width":350,"height":200,"srcset":"https:\/\/www.mymiller.name\/wordpress\/wp-content\/uploads\/2026\/01\/Gemini_Generated_Image_3yqio33yqio33yqi.avif 1x, https:\/\/www.mymiller.name\/wordpress\/wp-content\/uploads\/2026\/01\/Gemini_Generated_Image_3yqio33yqio33yqi.avif 1.5x, https:\/\/www.mymiller.name\/wordpress\/wp-content\/uploads\/2026\/01\/Gemini_Generated_Image_3yqio33yqio33yqi.avif 2x, https:\/\/www.mymiller.name\/wordpress\/wp-content\/uploads\/2026\/01\/Gemini_Generated_Image_3yqio33yqio33yqi.avif 3x"},"classes":[]},{"id":3744,"url":"https:\/\/www.mymiller.name\/wordpress\/spring_config\/3744\/","url_meta":{"origin":3940,"position":3},"title":"Spring Cloud Config: Choosing the Right Backend Storage","author":"Jeffery Miller","date":"December 23, 2025","format":false,"excerpt":"Spring Cloud Config offers a flexible way to manage your application\u2019s configuration. A crucial step is selecting the right backend to store your configuration data. Let\u2019s explore popular options, their pros and cons, configuration details, and the necessary dependencies for Maven and Gradle. 1. Git Pros: Version Control: Leverage Git\u2019s\u2026","rel":"","context":"In &quot;Spring Config&quot;","block_context":{"text":"Spring Config","link":"https:\/\/www.mymiller.name\/wordpress\/category\/spring_config\/"},"img":{"alt_text":"","src":"https:\/\/i0.wp.com\/www.mymiller.name\/wordpress\/wp-content\/uploads\/2024\/04\/woman-8696271_640.jpg?fit=438%2C640&ssl=1&resize=350%2C200","width":350,"height":200},"classes":[]},{"id":3951,"url":"https:\/\/www.mymiller.name\/wordpress\/java\/scaling-streams-mastering-virtual-threads-in-spring-boot-4-and-java-25\/","url_meta":{"origin":3940,"position":4},"title":"Scaling Streams: Mastering Virtual Threads in Spring Boot 4 and Java 25","author":"Jeffery Miller","date":"December 22, 2025","format":false,"excerpt":"As a software architect, I\u2019ve seen the industry shift from heavy platform threads to reactive streams, and finally to the \"best of both worlds\": Virtual Threads. With the recent release of Spring Boot 4.0 and Java 25 (LTS), Project Loom's innovations have officially become the bedrock of high-concurrency enterprise Java.\u2026","rel":"","context":"In &quot;JAVA&quot;","block_context":{"text":"JAVA","link":"https:\/\/www.mymiller.name\/wordpress\/category\/java\/"},"img":{"alt_text":"","src":"https:\/\/www.mymiller.name\/wordpress\/wp-content\/uploads\/2025\/12\/Gemini_Generated_Image_wqijejwqijejwqij-scaled.avif","width":350,"height":200,"srcset":"https:\/\/www.mymiller.name\/wordpress\/wp-content\/uploads\/2025\/12\/Gemini_Generated_Image_wqijejwqijejwqij-scaled.avif 1x, https:\/\/www.mymiller.name\/wordpress\/wp-content\/uploads\/2025\/12\/Gemini_Generated_Image_wqijejwqijejwqij-scaled.avif 1.5x, https:\/\/www.mymiller.name\/wordpress\/wp-content\/uploads\/2025\/12\/Gemini_Generated_Image_wqijejwqijejwqij-scaled.avif 2x, https:\/\/www.mymiller.name\/wordpress\/wp-content\/uploads\/2025\/12\/Gemini_Generated_Image_wqijejwqijejwqij-scaled.avif 3x"},"classes":[]},{"id":3954,"url":"https:\/\/www.mymiller.name\/wordpress\/spring-batch\/architecting-batch-systems-with-spring-boot-4-0-and-spring-framework-7-0\/","url_meta":{"origin":3940,"position":5},"title":"Architecting Batch Systems with Spring Boot 4.0 and Spring Framework 7.0","author":"Jeffery Miller","date":"December 23, 2025","format":false,"excerpt":"With the release of Spring Boot 4.0 and Spring Framework 7.0, the batch processing landscape has evolved to embrace Java 25, Jakarta EE 11, and built-in resilience patterns. This guide provides a professional architectural blueprint for setting up a high-performance Spring Batch server. 1. Technical Baseline Java: 17 (Baseline) \/\u2026","rel":"","context":"In &quot;Spring Batch&quot;","block_context":{"text":"Spring Batch","link":"https:\/\/www.mymiller.name\/wordpress\/category\/spring-batch\/"},"img":{"alt_text":"","src":"https:\/\/www.mymiller.name\/wordpress\/wp-content\/uploads\/2025\/12\/Gemini_Generated_Image_mmtkyammtkyammtk.avif","width":350,"height":200,"srcset":"https:\/\/www.mymiller.name\/wordpress\/wp-content\/uploads\/2025\/12\/Gemini_Generated_Image_mmtkyammtkyammtk.avif 1x, https:\/\/www.mymiller.name\/wordpress\/wp-content\/uploads\/2025\/12\/Gemini_Generated_Image_mmtkyammtkyammtk.avif 1.5x, https:\/\/www.mymiller.name\/wordpress\/wp-content\/uploads\/2025\/12\/Gemini_Generated_Image_mmtkyammtkyammtk.avif 2x, https:\/\/www.mymiller.name\/wordpress\/wp-content\/uploads\/2025\/12\/Gemini_Generated_Image_mmtkyammtkyammtk.avif 3x"},"classes":[]}],"jetpack_sharing_enabled":true,"jetpack_likes_enabled":true,"_links":{"self":[{"href":"https:\/\/www.mymiller.name\/wordpress\/wp-json\/wp\/v2\/posts\/3940","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.mymiller.name\/wordpress\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.mymiller.name\/wordpress\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.mymiller.name\/wordpress\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.mymiller.name\/wordpress\/wp-json\/wp\/v2\/comments?post=3940"}],"version-history":[{"count":1,"href":"https:\/\/www.mymiller.name\/wordpress\/wp-json\/wp\/v2\/posts\/3940\/revisions"}],"predecessor-version":[{"id":3943,"href":"https:\/\/www.mymiller.name\/wordpress\/wp-json\/wp\/v2\/posts\/3940\/revisions\/3943"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.mymiller.name\/wordpress\/wp-json\/wp\/v2\/media\/3942"}],"wp:attachment":[{"href":"https:\/\/www.mymiller.name\/wordpress\/wp-json\/wp\/v2\/media?parent=3940"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.mymiller.name\/wordpress\/wp-json\/wp\/v2\/categories?post=3940"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.mymiller.name\/wordpress\/wp-json\/wp\/v2\/tags?post=3940"},{"taxonomy":"series","embeddable":true,"href":"https:\/\/www.mymiller.name\/wordpress\/wp-json\/wp\/v2\/series?post=3940"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}