<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/"><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://mehmandarov.com/tag/jax-rs/feed.xml" rel="self" type="application/atom+xml"/><link href="https://mehmandarov.com/tag/jax-rs/" rel="alternate" type="text/html"/><updated>2026-05-25T10:00:00+02:00</updated><id>https://mehmandarov.com/tag/jax-rs/feed.xml</id><title type="html">Rustam Mehmandarov - tag: jax-rs</title><subtitle type="text">Posts tagged &quot;jax-rs&quot; on Rustam Mehmandarov.</subtitle><author><name>Rustam Mehmandarov</name></author><entry><title type="html">Sane API error handling with RFC 9457 Problem Details in Jakarta EE</title><link href="https://mehmandarov.com/rfc-9457-problem-details-jakarta-ee/" rel="alternate" type="text/html" title="Sane API error handling with RFC 9457 Problem Details in Jakarta EE"/><published>2026-05-25T10:00:00+02:00</published><updated>2026-05-25T10:00:00+02:00</updated><id>https://mehmandarov.com/rfc-9457-problem-details-jakarta-ee</id><content type="html" xml:base="https://mehmandarov.com/rfc-9457-problem-details-jakarta-ee/"><![CDATA[<p><em>When APIs end up with their own error format, it quickly gets annoying for anyone who has to consume more than one API. <a href="https://www.rfc-editor.org/rfc/rfc9457">RFC 9457</a> defines a standard envelope for HTTP API errors. Let&#8217;s have a look at how to do it in Jakarta EE: a small hand-made <code class="language-plaintext highlighter-rouge">ProblemDetail</code> plus one <code class="language-plaintext highlighter-rouge">ExceptionMapper</code> per error category; with the <a href="https://github.com/zalando/problem">Zalando Problem</a> library; followed by quick notes on Quarkus and Spring as alternatives.</em></p>

<ul>
  <li><a href="#introduction">Introduction</a></li>
  <li><a href="#tldr-why-rfc-9457">TL;DR: Why RFC 9457?</a></li>
  <li><a href="#lets-write-some-code">Let&#8217;s write some code!</a>
    <ul>
      <li><a href="#1-hand-made-problemdetail--exceptionmapper">1. Hand-made <code class="language-plaintext highlighter-rouge">ProblemDetail</code> + <code class="language-plaintext highlighter-rouge">ExceptionMapper</code></a></li>
      <li><a href="#2-zalando-problem">2. Zalando Problem</a></li>
      <li><a href="#3-quarkus-quarkus-http-problem">3. Quarkus: <code class="language-plaintext highlighter-rouge">quarkus-http-problem</code></a></li>
      <li><a href="#4-spring-boot--a-short-note">4. Spring Boot &#8211; a short note</a></li>
    </ul>
  </li>
  <li><a href="#conclusion">Conclusion</a></li>
  <li><a href="#whats-next">What&#8217;s Next?</a></li>
</ul>

<hr />

<h2 id="introduction">Introduction</h2>

<p>If you&#8217;ve consumed more than one or two REST APIs, you&#8217;ve seen the pattern. One service returns <code class="language-plaintext highlighter-rouge">{"error": "..."}</code>, another <code class="language-plaintext highlighter-rouge">{"message": "...", "code": 42}</code>, a third returns <code class="language-plaintext highlighter-rouge">200 OK</code> with an error hidden somewhere deep in the response. Your REST client code fills up with special cases for each one. Sounds familiar?</p>

<p><a href="https://www.rfc-editor.org/rfc/rfc9457">RFC 9457 &#8211; Problem Details for HTTP APIs</a> (the successor to RFC 7807) defines a single JSON envelope for errors, served as <code class="language-plaintext highlighter-rouge">application/problem+json</code> MIME type. It is a small spec: five well-defined bits of information and an <code class="language-plaintext highlighter-rouge">extensions</code> map for anything else you might need.</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"urn:problem-type:validation-error"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"title"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Validation Failed"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"status"</span><span class="p">:</span><span class="w"> </span><span class="mi">400</span><span class="p">,</span><span class="w">
  </span><span class="nl">"detail"</span><span class="p">:</span><span class="w"> </span><span class="s2">"The request body or parameters failed validation."</span><span class="p">,</span><span class="w">
  </span><span class="nl">"extensions"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"violations"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
      </span><span class="p">{</span><span class="w"> </span><span class="nl">"field"</span><span class="p">:</span><span class="w"> </span><span class="s2">"title"</span><span class="p">,</span><span class="w"> </span><span class="nl">"message"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Title is required"</span><span class="w"> </span><span class="p">}</span><span class="w">
    </span><span class="p">]</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<h2 id="tldr-why-rfc-9457">TL;DR: Why RFC 9457?</h2>

<p>Why not keep creating your own?</p>

<ul>
  <li><strong>Consumers already know the shape.</strong> Generated SDKs, gateways, log pipelines, and tracing tools can parse <code class="language-plaintext highlighter-rouge">application/problem+json</code> without extra work.</li>
  <li><strong>You can extend it without breaking clients.</strong> The <code class="language-plaintext highlighter-rouge">extensions</code> map is part of the spec &#8211; put what you need in there.</li>
  <li><strong>It separates the <em>category</em> from the <em>instance</em>.</strong> <code class="language-plaintext highlighter-rouge">type</code> says &#8220;this is a validation error&#8221; (stable, machine-readable); <code class="language-plaintext highlighter-rouge">detail</code> and <code class="language-plaintext highlighter-rouge">instance</code> describe what happened <em>this</em> time.</li>
</ul>

<p>&#128161; <em><strong>Note:</strong> RFC 9457 is just a JSON structure and a content type. No library or framework is required. That&#8217;s why there are so many implementations &#8211; and why a hand-made one is often a reasonable choice.</em></p>

<hr />

<h2 id="lets-write-some-code">Let&#8217;s write some code!</h2>

<p>I have created a repository called <a href="https://github.com/mehmandarov/api-guide-java">API Guide for Java</a> to showcase the patterns for one of my talks. For this post, have a look at <a href="https://github.com/mehmandarov/api-guide-java/blob/main/src/main/java/com/mehmandarov/confapi/error/ProblemDetail.java"><code class="language-plaintext highlighter-rouge">ProblemDetail.java</code></a> and the mappers next to it under <a href="https://github.com/mehmandarov/api-guide-java/tree/main/src/main/java/com/mehmandarov/confapi/error"><code class="language-plaintext highlighter-rouge">com/mehmandarov/confapi/error/</code></a>.</p>

<h3 id="1-hand-made-problemdetail--exceptionmapper">1. Hand-made <code class="language-plaintext highlighter-rouge">ProblemDetail</code> + <code class="language-plaintext highlighter-rouge">ExceptionMapper</code></h3>

<h4 id="what-it-looks-like">What it looks like</h4>

<p>Imagine you have a REST interface looking like this:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@GET</span>
<span class="nd">@Path</span><span class="o">(</span><span class="s">"/{id}"</span><span class="o">)</span>
<span class="nd">@Operation</span><span class="o">(</span><span class="n">summary</span> <span class="o">=</span> <span class="s">"Get room by ID"</span><span class="o">)</span>
<span class="nd">@APIResponse</span><span class="o">(</span><span class="n">responseCode</span> <span class="o">=</span> <span class="s">"200"</span><span class="o">,</span> <span class="n">description</span> <span class="o">=</span> <span class="s">"Room found"</span><span class="o">)</span>
<span class="nd">@APIResponse</span><span class="o">(</span><span class="n">responseCode</span> <span class="o">=</span> <span class="s">"404"</span><span class="o">,</span> <span class="n">description</span> <span class="o">=</span> <span class="s">"Room not found"</span><span class="o">)</span>
<span class="kd">public</span> <span class="nc">Room</span> <span class="nf">getById</span><span class="o">(</span>
        <span class="nd">@Parameter</span><span class="o">(</span><span class="n">description</span> <span class="o">=</span> <span class="s">"Room ID"</span><span class="o">,</span> <span class="n">required</span> <span class="o">=</span> <span class="kc">true</span><span class="o">)</span>
        <span class="nd">@PathParam</span><span class="o">(</span><span class="s">"id"</span><span class="o">)</span> <span class="nc">String</span> <span class="n">id</span><span class="o">)</span> <span class="o">{</span>
    <span class="k">return</span> <span class="n">repo</span><span class="o">.</span><span class="na">findById</span><span class="o">(</span><span class="n">id</span><span class="o">)</span>
            <span class="o">.</span><span class="na">orElseThrow</span><span class="o">(()</span> <span class="o">-&gt;</span> <span class="k">new</span> <span class="nc">NotFoundException</span><span class="o">(</span><span class="s">"Room not found: "</span> <span class="o">+</span> <span class="n">id</span><span class="o">));</span>
<span class="o">}</span>
</code></pre></div></div>

<p>Now, you can add a single <code class="language-plaintext highlighter-rouge">ProblemDetail</code> class &#8211; built around the five RFC 9457 elements and an <code class="language-plaintext highlighter-rouge">extensions</code> map &#8211; and one <a href="https://jakarta.ee/specifications/restful-ws/4.0/apidocs/jakarta.ws.rs/jakarta/ws/rs/ext/exceptionmapper"><code class="language-plaintext highlighter-rouge">ExceptionMapper</code></a> per error category.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">ProblemDetail</span> <span class="o">{</span>
    <span class="kd">private</span> <span class="no">URI</span> <span class="n">type</span> <span class="o">=</span> <span class="no">URI</span><span class="o">.</span><span class="na">create</span><span class="o">(</span><span class="s">"about:blank"</span><span class="o">);</span>
    <span class="kd">private</span> <span class="nc">String</span> <span class="n">title</span><span class="o">;</span>
    <span class="kd">private</span> <span class="kt">int</span> <span class="n">status</span><span class="o">;</span>
    <span class="kd">private</span> <span class="nc">String</span> <span class="n">detail</span><span class="o">;</span>
    <span class="kd">private</span> <span class="no">URI</span> <span class="n">instance</span><span class="o">;</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">Map</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">Object</span><span class="o">&gt;</span> <span class="n">extensions</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">LinkedHashMap</span><span class="o">&lt;&gt;();</span>

    <span class="kd">public</span> <span class="kd">static</span> <span class="nc">ProblemDetail</span> <span class="nf">of</span><span class="o">(</span><span class="kt">int</span> <span class="n">status</span><span class="o">,</span> <span class="nc">String</span> <span class="n">title</span><span class="o">)</span> <span class="o">{</span> <span class="cm">/* ... */</span> <span class="o">}</span>
    <span class="kd">public</span> <span class="nc">ProblemDetail</span> <span class="nf">withType</span><span class="o">(</span><span class="nc">String</span> <span class="n">typeUri</span><span class="o">)</span>            <span class="o">{</span> <span class="cm">/* ... */</span> <span class="o">}</span>
    <span class="kd">public</span> <span class="nc">ProblemDetail</span> <span class="nf">withExtension</span><span class="o">(</span><span class="nc">String</span> <span class="n">key</span><span class="o">,</span> <span class="nc">Object</span> <span class="n">v</span><span class="o">)</span> <span class="o">{</span> <span class="cm">/* ... */</span> <span class="o">}</span>
    <span class="c1">// + getters/setters</span>
<span class="o">}</span>
</code></pre></div></div>

<p>The interesting part is how it gets used. As you can see from the resource code above, there is <strong>no <code class="language-plaintext highlighter-rouge">try/catch</code> in resources, ever</strong> &#8211; every exception is turned into a Problem Details response by an <code class="language-plaintext highlighter-rouge">ExceptionMapper</code>:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Provider</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">ConstraintViolationExceptionMapper</span>
        <span class="kd">implements</span> <span class="nc">ExceptionMapper</span><span class="o">&lt;</span><span class="nc">ConstraintViolationException</span><span class="o">&gt;</span> <span class="o">{</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="nc">Response</span> <span class="nf">toResponse</span><span class="o">(</span><span class="nc">ConstraintViolationException</span> <span class="n">ex</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">List</span><span class="o">&lt;</span><span class="nc">Map</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">String</span><span class="o">&gt;&gt;</span> <span class="n">violations</span> <span class="o">=</span> <span class="n">ex</span><span class="o">.</span><span class="na">getConstraintViolations</span><span class="o">()</span>
                <span class="o">.</span><span class="na">stream</span><span class="o">().</span><span class="na">map</span><span class="o">(</span><span class="k">this</span><span class="o">::</span><span class="n">toMap</span><span class="o">).</span><span class="na">toList</span><span class="o">();</span>

        <span class="nc">ProblemDetail</span> <span class="n">problem</span> <span class="o">=</span> <span class="nc">ProblemDetail</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="mi">400</span><span class="o">,</span> <span class="s">"Validation Failed"</span><span class="o">)</span>
            <span class="o">.</span><span class="na">withType</span><span class="o">(</span><span class="s">"urn:problem-type:validation-error"</span><span class="o">)</span>
            <span class="o">.</span><span class="na">withExtension</span><span class="o">(</span><span class="s">"violations"</span><span class="o">,</span> <span class="n">violations</span><span class="o">);</span>

        <span class="k">return</span> <span class="nc">Response</span><span class="o">.</span><span class="na">status</span><span class="o">(</span><span class="mi">400</span><span class="o">)</span>
                <span class="o">.</span><span class="na">type</span><span class="o">(</span><span class="s">"application/problem+json"</span><span class="o">)</span>
                <span class="o">.</span><span class="na">entity</span><span class="o">(</span><span class="n">problem</span><span class="o">).</span><span class="na">build</span><span class="o">();</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>One mapper per category keeps each file small and obvious: <code class="language-plaintext highlighter-rouge">ConstraintViolationExceptionMapper</code> &#8594; 400, <code class="language-plaintext highlighter-rouge">NotFoundExceptionMapper</code> &#8594; 404, <code class="language-plaintext highlighter-rouge">NotAuthorizedExceptionMapper</code> &#8594; 401, <code class="language-plaintext highlighter-rouge">ForbiddenExceptionMapper</code> &#8594; 403, and a <code class="language-plaintext highlighter-rouge">CatchAllExceptionMapper</code> &#8594; 500 that <strong>never leaks stack traces</strong> to clients.</p>

<p>&#9888;&#65039;<em><strong>A word of caution:</strong> The catch-all mapper is the safety net for everything you forgot to handle. Without one, an uncaught exception ends up in the server&#8217;s default error page, which often includes stack traces, server versions, and sometimes filesystem paths. However, it might be a good idea to handle most of the common exceptions explicitly, and leave the generic catch-all for something truly unexpected.</em></p>

<p><strong>&#9989; Pros:</strong></p>

<ul>
  <li><strong>Portable across runtimes.</strong> The same code runs on Quarkus, Helidon, and Open Liberty. No runtime-specific extension.</li>
  <li><strong>No extra dependencies.</strong> RFC 9457 is just a JSON structure; you don&#8217;t need a library to emit one.</li>
  <li><strong>Small, readable surface.</strong> The error model fits on one slide. When something goes wrong, you can read the source.</li>
</ul>

<p><strong>&#10060; Cons:</strong></p>

<ul>
  <li>You write the boilerplate yourself &#8211; one mapper per category.</li>
  <li>Nothing maps validation, <code class="language-plaintext highlighter-rouge">WebApplicationException</code>, or uncaught <code class="language-plaintext highlighter-rouge">Throwable</code> automatically &#8211; you wire each one up. (This can also be one of the pros, depending on the way you look at things.)</li>
  <li>No content negotiation between <code class="language-plaintext highlighter-rouge">application/json</code> and <code class="language-plaintext highlighter-rouge">application/problem+json</code> unless you add it yourself. (Spring, for example, has a built-in <code class="language-plaintext highlighter-rouge">ProblemDetail</code> that does this for you.)</li>
</ul>

<p>&#128161; <em><strong>Want to know more?</strong> The full code, including all five mappers, lives in <a href="https://github.com/mehmandarov/api-guide-java/tree/main/src/main/java/com/mehmandarov/confapi/error"><code class="language-plaintext highlighter-rouge">com/mehmandarov/confapi/error/</code></a>.</em></p>

<hr />

<h3 id="2-zalando-problem">2. Zalando Problem</h3>

<h4 id="what-it-looks-like-1">What it looks like</h4>

<p>The <a href="https://github.com/zalando/problem">Zalando Problem</a> library (<code class="language-plaintext highlighter-rouge">org.zalando:problem</code> + <code class="language-plaintext highlighter-rouge">jackson-datatype-problem</code>) gives you <code class="language-plaintext highlighter-rouge">Problem</code> and <code class="language-plaintext highlighter-rouge">ThrowableProblem</code> types and Jackson serialization. You still write an <code class="language-plaintext highlighter-rouge">ExceptionMapper</code> to bridge JAX-RS exceptions to <code class="language-plaintext highlighter-rouge">Problem</code>, but you don&#8217;t define the envelope yourself.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">org.zalando.problem.Problem</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.zalando.problem.Status</span><span class="o">;</span>

<span class="nc">Problem</span> <span class="n">problem</span> <span class="o">=</span> <span class="nc">Problem</span><span class="o">.</span><span class="na">builder</span><span class="o">()</span>
        <span class="o">.</span><span class="na">withType</span><span class="o">(</span><span class="no">URI</span><span class="o">.</span><span class="na">create</span><span class="o">(</span><span class="s">"urn:problem-type:validation-error"</span><span class="o">))</span>
        <span class="o">.</span><span class="na">withTitle</span><span class="o">(</span><span class="s">"Validation Failed"</span><span class="o">)</span>
        <span class="o">.</span><span class="na">withStatus</span><span class="o">(</span><span class="nc">Status</span><span class="o">.</span><span class="na">BAD_REQUEST</span><span class="o">)</span>
        <span class="o">.</span><span class="na">with</span><span class="o">(</span><span class="s">"violations"</span><span class="o">,</span> <span class="n">violations</span><span class="o">)</span>
        <span class="o">.</span><span class="na">build</span><span class="o">();</span>

<span class="k">return</span> <span class="nc">Response</span><span class="o">.</span><span class="na">status</span><span class="o">(</span><span class="mi">400</span><span class="o">)</span>
        <span class="o">.</span><span class="na">type</span><span class="o">(</span><span class="s">"application/problem+json"</span><span class="o">)</span>
        <span class="o">.</span><span class="na">entity</span><span class="o">(</span><span class="n">problem</span><span class="o">).</span><span class="na">build</span><span class="o">();</span>
</code></pre></div></div>

<p><strong>&#9989; Pros:</strong></p>

<ul>
  <li><strong>Cross-runtime.</strong> Works on Quarkus, Helidon, and Open Liberty &#8211; the same artifact deploys on all three.</li>
  <li><strong>Used in production at Zalando</strong> (and elsewhere); the model handles <code class="language-plaintext highlighter-rouge">cause</code> chains, stack-trace processing, and a few edge cases you probably would not have thought of upfront.</li>
  <li><strong>Jackson integration is done for you</strong> via <code class="language-plaintext highlighter-rouge">jackson-datatype-problem</code>.</li>
</ul>

<p><strong>&#10060; Cons:</strong></p>

<ul>
  <li>One more dependency to track and upgrade.</li>
  <li>You still write the <code class="language-plaintext highlighter-rouge">ExceptionMapper</code>s &#8211; the library standardises the <em>payload</em>, not the <em>wiring</em>.</li>
  <li>If your stack is JSON-B rather than Jackson, you have a bit of extra work.</li>
</ul>

<hr />

<h3 id="3-quarkus-quarkus-http-problem">3. Quarkus: <code class="language-plaintext highlighter-rouge">quarkus-http-problem</code></h3>

<p>If you&#8217;re <em>only</em> targeting Quarkus, the <a href="https://github.com/quarkiverse/quarkus-http-problem"><code class="language-plaintext highlighter-rouge">quarkus-http-problem</code></a> Quarkiverse extension is the shortest path. It auto-maps <code class="language-plaintext highlighter-rouge">ConstraintViolationException</code>, <code class="language-plaintext highlighter-rouge">WebApplicationException</code>, and uncaught <code class="language-plaintext highlighter-rouge">Throwable</code> to <code class="language-plaintext highlighter-rouge">application/problem+json</code> with no boilerplate from you.</p>

<p><strong>&#9989; Pros:</strong></p>

<ul>
  <li>Add the dependency and you get Problem Details for exceptions. No need to write a mapper for each of them.</li>
  <li>Reasonable defaults for validation and security exceptions.</li>
</ul>

<p><strong>&#10060; Cons:</strong></p>

<ul>
  <li><strong>Quarkus only.</strong> Doesn&#8217;t help on Helidon (Jersey) or Open Liberty (CXF). If &#8220;runs on every Jakarta runtime&#8221; is a requirement, this is out.</li>
  <li>Less visibility into <em>what</em> gets mapped to <em>what</em> &#8211; fine until you need to override a default.</li>
</ul>

<hr />

<h3 id="4-spring-boot--a-short-note">4. Spring Boot &#8211; a short note</h3>

<p>For completeness, we need to mention Spring Boot 3+ as well, which has Problem Details built in as <a href="https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/http/ProblemDetail.html"><code class="language-plaintext highlighter-rouge">org.springframework.http.ProblemDetail</code></a>, with content negotiation and <code class="language-plaintext highlighter-rouge">@ExceptionHandler</code> integration already wired up. If you&#8217;re on Spring, just use it. The JSON structure is the same RFC 9457; only the wiring differs.</p>

<hr />

<h2 id="conclusion">Conclusion</h2>

<p>The point of RFC 9457 is not that there&#8217;s one correct implementation &#8211; there are several reasonable ones &#8211; but that there&#8217;s one correct envelope. Once your API speaks <code class="language-plaintext highlighter-rouge">application/problem+json</code>, clients stop hand-coding error parsers for each new service they consume.</p>

<p>A few rules of thumb:</p>

<ul>
  <li>On <strong>Spring</strong>, use the built-in <code class="language-plaintext highlighter-rouge">ProblemDetail</code>.</li>
  <li>On <strong>Quarkus only</strong>, reach for <code class="language-plaintext highlighter-rouge">quarkus-http-problem</code> and move on.</li>
  <li>For <strong>cross-runtime Jakarta</strong>, choose between <strong>Zalando Problem</strong> (one dependency, more handled for you) and the <strong>hand-made</strong> approach (no dependencies, about 30 lines you fully understand).</li>
</ul>

<p>I picked the hand-made approach for the demo project because portability across Quarkus, Helidon, and Open Liberty mattered, and because the <code class="language-plaintext highlighter-rouge">ExceptionMapper</code> <em>is</em> the demo &#8211; hiding it behind a library would have defeated the point of the talk.</p>

<p>However, &#8220;hand-made&#8221; doesn&#8217;t have to mean &#8220;everyone reinvents it from scratch&#8221;. Write it once, put it in a small internal library, and reuse it across services. That&#8217;s still less code than wiring up a third-party dependency in each runtime.</p>

<h3 id="summary-comparison">Summary Comparison</h3>

<table class="bordered-table">
  <thead>
    <tr>
      <th>Option</th>
      <th>What it gives you</th>
      <th>Runtimes</th>
      <th>Dependency cost</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Hand-made</strong> <em>(this post)</em></td>
      <td>~30-line <code class="language-plaintext highlighter-rouge">ProblemDetail</code> + one mapper per error category.</td>
      <td>&#160;&#9989; Quarkus &#160;&#9989; Helidon &#160;&#9989; Open Liberty</td>
      <td>None</td>
    </tr>
    <tr>
      <td><strong>Zalando Problem</strong></td>
      <td><code class="language-plaintext highlighter-rouge">Problem</code> / <code class="language-plaintext highlighter-rouge">ThrowableProblem</code> types + Jackson serialization. You still write the mappers.</td>
      <td>&#160;&#9989; Quarkus &#160;&#9989; Helidon &#160;&#9989; Open Liberty</td>
      <td>1&#8211;2 artifacts</td>
    </tr>
    <tr>
      <td><strong><code class="language-plaintext highlighter-rouge">quarkus-http-problem</code></strong></td>
      <td>Auto-maps validation, <code class="language-plaintext highlighter-rouge">WebApplicationException</code>, and uncaught <code class="language-plaintext highlighter-rouge">Throwable</code>. No boilerplate.</td>
      <td>&#160;&#9989; Quarkus only</td>
      <td>1 extension</td>
    </tr>
    <tr>
      <td><strong>Spring <code class="language-plaintext highlighter-rouge">ProblemDetail</code></strong></td>
      <td>Built into the framework. Content negotiation and <code class="language-plaintext highlighter-rouge">@ExceptionHandler</code> integration.</td>
      <td>&#160;&#9989; Spring Boot 3+</td>
      <td>None (built in)</td>
    </tr>
  </tbody>
</table>

<h2 id="whats-next">What&#8217;s Next?</h2>

<p>Error handling is one of the bonus topics in the <a href="https://github.com/mehmandarov/api-guide-java">API Guide for Java</a>. The same repo also covers OpenAPI documentation, security (RBAC, JWT), pagination, async, and versioning strategies &#8211; see my earlier post on <a href="/api-versioning/">API versioning in Java using JAX-RS</a>.</p>

<p><strong><em>Happy shipping of well-formed error messages, folks!</em></strong></p>

<hr />]]></content><author><name>Rustam Mehmandarov</name></author><summary type="html">A practical look at RFC 9457 Problem Details for HTTP APIs in Jakarta EE &#8211; a hand-made ProblemDetail + ExceptionMapper approach, the Zalando Problem library, and a short note on Quarkus and Spring.</summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://mehmandarov.com/assets/images/posts-images/error.jpg"/><category term="blog"/><category term="english"/><category term="java"/><category term="architecture"/><category term="api"/><category term="jakarta ee"/><category term="microprofile"/><category term="jax-rs"/><category term="error handling"/><category term="quarkus"/><category term="spring"/></entry><entry><title type="html">API versioning in Java using JAX-RS with Jakarta EE and MicroProfile</title><link href="https://mehmandarov.com/api-versioning/" rel="alternate" type="text/html" title="API versioning in Java using JAX-RS with Jakarta EE and MicroProfile"/><published>2026-04-19T09:50:00+02:00</published><updated>2026-04-19T09:50:00+02:00</updated><id>https://mehmandarov.com/api-versioning</id><content type="html" xml:base="https://mehmandarov.com/api-versioning/"><![CDATA[<p><em>Creating APIs and maintaining them over time means often that we need to version them. We will be looking into several ways of doing so in Java using JAX-RS, while building our API end-points using Jakarta EE and MicroProfile. This post was inspired by my talk &#8220;API = Some REST and HTTP, right? RIGHT?!&#8221;</em></p>

<ul>
  <li><a href="#introduction">Introduction</a></li>
  <li><a href="#why-versioning">Why Versioning?</a></li>
  <li><a href="#show-me-the-code">Show Me The CODE!</a></li>
  <li><a href="#1-url-versioning">1. URL Versioning</a></li>
  <li><a href="#2-header-versioning">2. Header Versioning</a></li>
  <li><a href="#3-media-type-versioning-content-negotiation">3. Media Type Versioning</a></li>
  <li><a href="#4-request-parameter-versioning">4. Request Parameter Versioning</a></li>
  <li><a href="#5-bonus-combining-strategies---transparent-uri-rewriting-enterprise-pattern">5. Bonus: Combining Strategies</a></li>
  <li><a href="#6-end-point-deprecation">6. End-Point Deprecation</a></li>
  <li><a href="#summary-comparison">Summary Comparison</a></li>
  <li><a href="#conclusion">Conclusion</a></li>
  <li><a href="#whats-next">What&#8217;s Next?</a></li>
</ul>

<hr />

<h2 id="introduction">Introduction</h2>

<p>When working with APIs over time we would often need to make some changes to end-point definitions &#8211; like adding or deleting resources or changing the attributes for a resource. To ensure backwards compatibility, we often have to introduce <em>versioning</em> for our APIs. APIs, like all software, evolve. You might be adding optional fields or introducing a breaking change. At some point, you will need versioning to support coexistence of the old and new consumers.</p>

<p>However, versioning the API endpoint introduces a question of how this should be done. In this post, we&#8217;ll explore <strong>several common API versioning strategies</strong>, using Jakarta EE and Java.</p>

<blockquote>
  <p>&#128161; Note: There is no silver bullet &#8211; instead, we&#8217;ll explore <strong>pros, cons, and real-world fit</strong>.</p>
</blockquote>

<h2 id="why-versioning">Why Versioning?</h2>

<p>Why not just change the API?<br />
Because breaking contracts is dangerous &#8212; clients may not update in sync, and you&#8217;ll break production consumers.</p>

<p>Versioning allows you to:</p>
<ul>
  <li>Support legacy clients</li>
  <li>Introduce new features safely</li>
  <li>Deprecate responsibly</li>
</ul>

<blockquote>
  <p>&#9888;&#65039; <strong>Caution</strong>: Versioning can cause &#8220;version explosion.&#8221; Each version increases long-term maintenance cost &#8211; aka <em>technical debt</em>.</p>
</blockquote>

<p><strong>Best Practice</strong>: Prefer <em>backward-compatible changes</em> (e.g., adding fields) whenever possible. To mitigate risks, it&#8217;s important to follow best practices for versioning and provide clear documentation and migration paths for users. Also, remember to <em>deprecate</em> old versions to minimize maintenance efforts.</p>

<h2 id="show-me-the-code">Show me the CODE!</h2>

<p>I have created a repository called <a href="https://github.com/mehmandarov/randomstrings/">Random Strings</a> to showcase various concepts. For this blogpost, I would recommend having a look at <a href="https://github.com/mehmandarov/randomstrings/blob/master/src/main/java/com/mehmandarov/randomstrings/apidemo/RandomStringsAPIDemoController.java"><code class="language-plaintext highlighter-rouge">RandomStringsAPIDemoController.java</code></a> and <a href="https://github.com/mehmandarov/randomstrings/blob/master/request_examples.http"><code class="language-plaintext highlighter-rouge">request_examples.http</code></a>. You will find all the info on building and running the code in the repo&#8217;s <code class="language-plaintext highlighter-rouge">README.md</code> file. Each section below will contain &#8220;How to call it&#8221; part with an example using <code class="language-plaintext highlighter-rouge">curl</code> or HTTP-files, and will be based on this repo.</p>

<hr />

<h2 id="1-url-versioning">1. URL Versioning</h2>

<h3 id="what-it-looks-like">What it looks like</h3>
<p>A version appears directly in the URI path. If your API is at <code class="language-plaintext highlighter-rouge">https://example.com/api</code>, and the current version is version 1, the URL for a resource might look like this: <code class="language-plaintext highlighter-rouge">https://example.com/api/v1/resource</code>:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@GET</span>
<span class="nd">@Path</span><span class="o">(</span><span class="s">"/v2/"</span><span class="o">)</span>
<span class="nd">@Produces</span><span class="o">(</span><span class="nc">MediaType</span><span class="o">.</span><span class="na">APPLICATION_JSON</span><span class="o">)</span>
<span class="kd">public</span> <span class="nc">Response</span> <span class="nf">getV2</span><span class="o">()</span> <span class="o">{</span>
    <span class="k">return</span> <span class="nc">Response</span><span class="o">.</span><span class="na">status</span><span class="o">(</span><span class="nc">Response</span><span class="o">.</span><span class="na">Status</span><span class="o">.</span><span class="na">NOT_IMPLEMENTED</span><span class="o">)</span>
        <span class="o">.</span><span class="na">entity</span><span class="o">(</span><span class="s">"This v2 using *path versioning* of the API is not implemented."</span><span class="o">)</span>
        <span class="o">.</span><span class="na">build</span><span class="o">();</span>
<span class="o">}</span>
</code></pre></div></div>

<h3 id="how-to-call-it">How to call it</h3>

<p><strong>cURL:</strong></p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-X</span> GET http://localhost:8080/api/rnd/v2/ <span class="se">\</span>
  <span class="nt">-H</span> <span class="s2">"Accept: application/json"</span>
</code></pre></div></div>

<p><strong>HTTP Request (<code class="language-plaintext highlighter-rouge">.http</code> file):</strong></p>
<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">GET http://localhost:8080/api/rnd/v2/
Accept: application/json
</span></code></pre></div></div>

<p><strong>&#9989; Pros:</strong></p>
<ul>
  <li>Simple and intuitive. Visible.</li>
  <li>Easy to test (e.g., with curl or Postman directly in a browser).</li>
  <li>Plays well with gateways and reverse proxies.</li>
  <li>Clear visual distinction between versions.</li>
</ul>

<p><strong>&#10060; Cons:</strong></p>
<ul>
  <li>Pollutes the URI with versioning logic.</li>
  <li>Breaks REST&#8217;s principle of stable resource identifiers.</li>
  <li>Clients have to update URLs when migrating.</li>
  <li>Risk of accumulating too many legacy versions.</li>
  <li>Can result in cluttered and difficult-to-read URLs if there are multiple versions of the API.</li>
</ul>

<p><strong>&#128269; However:</strong> Despite its REST purism flaw, URL versioning is extremely practical and widely adopted.</p>

<h2 id="2-header-versioning">2. Header Versioning</h2>

<h3 id="what-it-looks-like-1">What it looks like</h3>
<p>Client specifies version in a custom HTTP header (e.g., <code class="language-plaintext highlighter-rouge">Accept-Version</code>, <code class="language-plaintext highlighter-rouge">X-API-Version</code>, etc.):</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Path</span><span class="o">(</span><span class="s">"/hi2"</span><span class="o">)</span>
<span class="nd">@GET</span>
<span class="nd">@Produces</span><span class="o">({</span><span class="s">"application/json"</span><span class="o">})</span>
<span class="kd">public</span> <span class="nc">String</span> <span class="nf">entryPoint2</span><span class="o">(</span><span class="nd">@HeaderParam</span><span class="o">(</span><span class="s">"Accept-Version"</span><span class="o">)</span> <span class="nc">String</span> <span class="n">apiVersion</span><span class="o">)</span> <span class="o">{</span>
    <span class="k">if</span> <span class="o">(</span><span class="n">apiVersion</span> <span class="o">==</span> <span class="kc">null</span> <span class="o">||</span> <span class="n">apiVersion</span><span class="o">.</span><span class="na">isEmpty</span><span class="o">())</span> <span class="o">{</span>
        <span class="k">return</span> <span class="s">"Default unversioned endpoint hit."</span><span class="o">;</span>
    <span class="o">}</span>
    <span class="nc">String</span> <span class="n">message</span> <span class="o">=</span> <span class="s">"Versioned: Using custom headers. Using version: "</span> <span class="o">+</span> <span class="n">apiVersion</span> <span class="o">+</span><span class="s">"."</span><span class="o">;</span>
    <span class="k">return</span> <span class="n">message</span><span class="o">;</span>
<span class="o">}</span>
</code></pre></div></div>

<h3 id="how-to-call-it-1">How to call it</h3>

<p><em>Note: This is for demo purposes only. It has to have a different URL than the regular API; otherwise, it will also intercept calls that do not contain the <code class="language-plaintext highlighter-rouge">Accept-Version</code> header.</em></p>

<p><strong>cURL:</strong></p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-X</span> GET http://localhost:8080/api/rnd/versioned/ <span class="se">\</span>
  <span class="nt">-H</span> <span class="s2">"Accept: application/json"</span> <span class="se">\</span>
  <span class="nt">-H</span> <span class="s2">"Accept-Version: 2"</span>
</code></pre></div></div>

<p><strong>HTTP Request (<code class="language-plaintext highlighter-rouge">.http</code> file):</strong></p>
<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">GET http://localhost:8080/api/rnd/versioned/
Accept: application/json
Accept-Version: 2
</span></code></pre></div></div>

<p><strong>&#9989; Pros:</strong></p>
<ul>
  <li>Keeps URL structure clean and predictable.</li>
  <li>Closer to HTTP semantics (headers = metadata).</li>
  <li>Allows centralized versioning logic in filters/interceptors.</li>
</ul>

<p><strong>&#10060; Cons:</strong></p>
<ul>
  <li>Not self-descriptive &#8212; clients must &#8220;know the secret handshake&#8221;.</li>
  <li>Poor discoverability (not visible in browser without tools).</li>
  <li>Breaks caching in some proxies/CDNs unless explicitly configured.</li>
  <li>Adds complexity to tooling and testing.</li>
</ul>

<p><strong>&#9888;&#65039; Challenge:</strong> Header versioning can feel &#8220;invisible&#8221; and cause developer confusion if not well-documented.</p>

<h2 id="3-media-type-versioning-content-negotiation">3. Media Type Versioning (Content Negotiation)</h2>

<h3 id="what-it-looks-like-2">What it looks like</h3>
<p>Client specifies version via a custom media type in the <code class="language-plaintext highlighter-rouge">Accept</code> header. This is sometimes called <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Content_negotiation">Content Negotiation</a> versioning.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Accept: application/hi.v3+json
</code></pre></div></div>

<p>In Jakarta EE:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Path</span><span class="o">(</span><span class="s">"/hi"</span><span class="o">)</span>
<span class="nd">@GET</span>
<span class="nd">@Produces</span><span class="o">({</span><span class="s">"application/hi.v3+json"</span><span class="o">,</span> <span class="s">"application/hi.v4+json"</span><span class="o">})</span>
<span class="kd">public</span> <span class="nc">String</span> <span class="nf">entryPoint</span><span class="o">()</span> <span class="kd">throws</span> <span class="nc">URISyntaxException</span> <span class="o">{</span>
    <span class="nc">String</span> <span class="n">message</span> <span class="o">=</span> <span class="s">"Versioned: Hai there!"</span><span class="o">;</span>
    <span class="k">return</span> <span class="n">message</span><span class="o">;</span>
<span class="o">}</span>
</code></pre></div></div>

<h3 id="how-to-call-it-2">How to call it</h3>

<p>You can request different versions (e.g., v3, v4, v5) by updating the media type:</p>

<p><strong>cURL:</strong></p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-X</span> GET http://localhost:8080/api/rnd/ <span class="se">\</span>
  <span class="nt">-H</span> <span class="s2">"Accept: application/rnd.v3+json"</span>
</code></pre></div></div>

<p><strong>HTTP Request (<code class="language-plaintext highlighter-rouge">.http</code> file):</strong></p>
<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">GET http://localhost:8080/api/rnd/
Accept: application/rnd.v3+json
</span></code></pre></div></div>

<p><strong>&#9989; Pros:</strong></p>
<ul>
  <li>Very REST-compliant: changes representation, not resource.</li>
  <li>URI remains stable.</li>
  <li>Supports richer format negotiation (e.g., XML, HAL, etc.).</li>
</ul>

<p><strong>&#10060; Cons:</strong></p>
<ul>
  <li>Requires strict control over media types.</li>
  <li>Not all clients/tooling handle custom media types well.</li>
  <li>Breaks with some reverse proxies and middleware that don&#8217;t forward full Accept headers.</li>
  <li>More work to configure content negotiation.</li>
</ul>

<p><strong>&#129514; Observation:</strong> Elegant in design, but rarely used consistently in real-world public APIs.</p>

<h2 id="4-request-parameter-versioning">4. Request Parameter Versioning</h2>

<h3 id="what-it-looks-like-3">What it looks like</h3>
<p><em>Technically</em>, it is also possible for the client to specify the version in a URL query parameter (e.g., <code class="language-plaintext highlighter-rouge">?version=2</code>). This, however, might not be a suggested strategy, in my opinion.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>https://example.com/api/resource?version<span class="o">=</span>2
</code></pre></div></div>

<h3 id="how-to-call-it-3">How to call it</h3>

<p><strong>cURL:</strong></p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-X</span> GET http://localhost:8080/api/rnd?version<span class="o">=</span>2 <span class="se">\</span>
  <span class="nt">-H</span> <span class="s2">"Accept: application/json"</span>
</code></pre></div></div>

<p><strong>HTTP Request (<code class="language-plaintext highlighter-rouge">.http</code> file):</strong></p>
<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">GET http://localhost:8080/api/rnd?version=2
Accept: application/json
</span></code></pre></div></div>

<p><strong>&#9989; Pros:</strong></p>
<ul>
  <li>Simplicity &amp; discoverability: Easy to test in a browser without specialized tools.</li>
  <li>Defaulting logic: Straightforward to implement &#8220;default to latest&#8221; if the parameter is omitted.</li>
  <li>Caching friendly: CDNs treat different query params as unique resources by default.</li>
</ul>

<p><strong>&#10060; Cons:</strong></p>
<ul>
  <li>URI Pollution: Mixes resource identification with technical metadata.</li>
  <li>Routing complexity: Routing based on query parameters usually requires custom middleware or manual logic inside the controller.</li>
  <li>Harder to generate clean, automated documentation (like OpenAPI) when multiple versions share the same path.</li>
</ul>

<h2 id="5-bonus-combining-strategies---transparent-uri-rewriting-enterprise-pattern">5. Bonus: Combining Strategies - Transparent URI Rewriting (Enterprise Pattern)</h2>

<p>In large enterprises, you might find that different clients have different needs. Some prefer the explicitness of URL versioning, while others require the clean URIs of Header versioning. You don&#8217;t have to choose just one&#8212;you can support both without duplicating your backend routing logic.</p>

<p>The common practice is to structure all your resource classes using <strong>URL versioning</strong> (e.g., <code class="language-plaintext highlighter-rouge">@Path("/v1/resource")</code>), but use a <strong><code class="language-plaintext highlighter-rouge">@PreMatching</code> Filter</strong> to intercept requests and transparently rewrite the URI if a client uses a header instead.</p>

<p>Here is what that looks like in Jakarta EE using a <code class="language-plaintext highlighter-rouge">ContainerRequestFilter</code>:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Provider</span>
<span class="nd">@PreMatching</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">HeaderVersionFilter</span> <span class="kd">implements</span> <span class="nc">ContainerRequestFilter</span> <span class="o">{</span>

      <span class="nd">@Override</span>
      <span class="kd">public</span> <span class="kt">void</span> <span class="nf">filter</span><span class="o">(</span><span class="nc">ContainerRequestContext</span> <span class="n">ctx</span><span class="o">)</span> <span class="o">{</span>
          <span class="nc">String</span> <span class="n">path</span> <span class="o">=</span> <span class="n">ctx</span><span class="o">.</span><span class="na">getUriInfo</span><span class="o">().</span><span class="na">getPath</span><span class="o">();</span>

          <span class="c1">// If the path is already versioned (e.g., starts with v1, v2), let it pass</span>
          <span class="k">if</span> <span class="o">(</span><span class="n">path</span><span class="o">.</span><span class="na">matches</span><span class="o">(</span><span class="s">"v\\d+(/.*)?"</span><span class="o">))</span> <span class="k">return</span><span class="o">;</span>

          <span class="c1">// Otherwise, check if the client provided a version header</span>
          <span class="nc">String</span> <span class="n">version</span> <span class="o">=</span> <span class="n">ctx</span><span class="o">.</span><span class="na">getHeaderString</span><span class="o">(</span><span class="s">"X-API-Version"</span><span class="o">);</span>

          <span class="k">if</span> <span class="o">(</span><span class="n">version</span> <span class="o">!=</span> <span class="kc">null</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="n">version</span><span class="o">.</span><span class="na">isEmpty</span><span class="o">())</span> <span class="o">{</span>
              <span class="c1">// Transparently rewrite the URI internally to match our URL-based routes</span>
              <span class="nc">String</span> <span class="n">newPath</span> <span class="o">=</span> <span class="s">"v"</span> <span class="o">+</span> <span class="n">version</span> <span class="o">+</span> <span class="s">"/"</span> <span class="o">+</span> <span class="n">path</span><span class="o">;</span>
              <span class="no">URI</span> <span class="n">baseUri</span> <span class="o">=</span> <span class="n">ctx</span><span class="o">.</span><span class="na">getUriInfo</span><span class="o">().</span><span class="na">getBaseUri</span><span class="o">();</span>
              <span class="no">URI</span> <span class="n">newUri</span> <span class="o">=</span> <span class="nc">UriBuilder</span><span class="o">.</span><span class="na">fromUri</span><span class="o">(</span><span class="n">baseUri</span><span class="o">).</span><span class="na">path</span><span class="o">(</span><span class="n">newPath</span><span class="o">).</span><span class="na">build</span><span class="o">();</span>

              <span class="n">ctx</span><span class="o">.</span><span class="na">setRequestUri</span><span class="o">(</span><span class="n">baseUri</span><span class="o">,</span> <span class="n">newUri</span><span class="o">);</span>
          <span class="o">}</span>
      <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p><strong>&#9989; Pros:</strong></p>
<ul>
  <li><strong>Ultimate Flexibility:</strong> Clients can use <code class="language-plaintext highlighter-rouge">http://api.example.com/v2/resource</code> OR <code class="language-plaintext highlighter-rouge">http://api.example.com/resource</code> with an <code class="language-plaintext highlighter-rouge">X-API-Version: 2</code> header.</li>
  <li><strong>Single Source of Truth:</strong> Your backend controllers only need to use <code class="language-plaintext highlighter-rouge">@Path("/v2/")</code>. You don&#8217;t have to write duplicate methods to handle both headers and paths.</li>
</ul>

<p><strong>&#10060; Cons:</strong></p>
<ul>
  <li><strong>Magic Routing:</strong> It introduces a layer of &#8220;magic&#8221; where the requested URI differs from the routed URI, which can briefly confuse new developers debugging the application.</li>
</ul>

<p><strong>&#128161; Want to know more?</strong> Read up on terms <em><code class="language-plaintext highlighter-rouge">Version Normalization</code></em> and <em><code class="language-plaintext highlighter-rouge">Internal Decoupling</code></em>.</p>

<h2 id="6-end-point-deprecation">6. End-Point Deprecation</h2>

<p>Eventually, you will need to retire old API versions. Remember: every old version you keep around is <em>technical debt</em> &#8212; it increases long-term maintenance cost. When deprecating an endpoint, consider the following best practices:</p>

<ol>
  <li><strong>Update the Docs:</strong> Use OpenAPI&#8217;s <code class="language-plaintext highlighter-rouge">@Operation</code> annotation to clearly mark it as deprecated.</li>
  <li><strong>Add <code class="language-plaintext highlighter-rouge">@Deprecated</code>:</strong> Use the Java <code class="language-plaintext highlighter-rouge">@Deprecated</code> annotation where necessary.</li>
  <li><strong>HTTP Redirects:</strong> Consider returning HTTP codes like <code class="language-plaintext highlighter-rouge">302 Found</code> or <code class="language-plaintext highlighter-rouge">301 Moved Permanently</code> after some time.</li>
  <li><strong>Add a Link header:</strong> Provide a link to the new version in the response headers.</li>
  <li><strong>Log / Count calls:</strong> Track usage (e.g., with MicroProfile <code class="language-plaintext highlighter-rouge">@Counted</code>) to know when it is safe to finally remove the endpoint.</li>
</ol>

<p>Here is a practical example in Jakarta EE showing how to deprecate an endpoint, add a <code class="language-plaintext highlighter-rouge">Link</code> header, and track metrics:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@GET</span>
<span class="nd">@Path</span><span class="o">(</span><span class="s">"v0.1/"</span><span class="o">)</span>
<span class="nd">@Produces</span><span class="o">(</span><span class="nc">MediaType</span><span class="o">.</span><span class="na">APPLICATION_JSON</span><span class="o">)</span>
<span class="nd">@Operation</span><span class="o">(</span><span class="n">summary</span> <span class="o">=</span> <span class="s">"DEPRECATED. Use v2 now. Returns the adjective-noun pair"</span><span class="o">,</span>
           <span class="n">description</span> <span class="o">=</span> <span class="s">"Deprecated function. The pair of one random adjective and one random noun is returned as an array."</span><span class="o">)</span>
<span class="nd">@Counted</span><span class="o">(</span><span class="n">name</span> <span class="o">=</span> <span class="s">"totalCountToRandomPairCalls_Versioned_Path_DEPRECATED"</span><span class="o">,</span>
         <span class="n">absolute</span> <span class="o">=</span> <span class="kc">true</span><span class="o">,</span>
         <span class="n">description</span> <span class="o">=</span> <span class="s">"Deprecated function call: Total number of calls to random string pairs."</span><span class="o">,</span>
         <span class="n">tags</span> <span class="o">=</span> <span class="o">{</span><span class="s">"calls=pairs"</span><span class="o">})</span>
<span class="nd">@Deprecated</span>
<span class="kd">public</span> <span class="nc">Response</span> <span class="nf">getRndStringPathDeprecated</span><span class="o">()</span> <span class="o">{</span>
    <span class="no">URI</span> <span class="n">newVersionURI</span> <span class="o">=</span> <span class="nc">UriBuilder</span><span class="o">.</span><span class="na">fromUri</span><span class="o">(</span><span class="s">"/api/rnd/v2/"</span><span class="o">).</span><span class="na">build</span><span class="o">();</span>
    <span class="nc">Link</span> <span class="n">newVersionLink</span> <span class="o">=</span> <span class="nc">Link</span><span class="o">.</span><span class="na">fromUri</span><span class="o">(</span><span class="n">newVersionURI</span><span class="o">).</span><span class="na">rel</span><span class="o">(</span><span class="s">"alternate"</span><span class="o">).</span><span class="na">build</span><span class="o">();</span>
    <span class="k">return</span> <span class="nc">Response</span><span class="o">.</span><span class="na">ok</span><span class="o">(</span><span class="s">"Deprecated response"</span><span class="o">,</span> <span class="nc">MediaType</span><span class="o">.</span><span class="na">APPLICATION_JSON</span><span class="o">)</span>
            <span class="o">.</span><span class="na">header</span><span class="o">(</span><span class="n">jakarta</span><span class="o">.</span><span class="na">ws</span><span class="o">.</span><span class="na">rs</span><span class="o">.</span><span class="na">core</span><span class="o">.</span><span class="na">HttpHeaders</span><span class="o">.</span><span class="na">LINK</span><span class="o">,</span> <span class="n">newVersionLink</span><span class="o">.</span><span class="na">toString</span><span class="o">())</span>
            <span class="o">.</span><span class="na">header</span><span class="o">(</span><span class="s">"X-API-Version"</span><span class="o">,</span> <span class="s">"0.1"</span><span class="o">)</span>
            <span class="o">.</span><span class="na">build</span><span class="o">();</span>
<span class="o">}</span>
</code></pre></div></div>

<h3 id="how-to-call-it-deprecated-endpoint">How to call it (Deprecated endpoint)</h3>

<p><strong>cURL:</strong></p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-X</span> GET http://localhost:8080/api/rnd/v0.1/ <span class="se">\</span>
  <span class="nt">-H</span> <span class="s2">"Accept: application/json"</span>
</code></pre></div></div>

<p><strong>HTTP Request (<code class="language-plaintext highlighter-rouge">.http</code> file):</strong></p>
<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">GET http://localhost:8080/api/rnd/v0.1/
Accept: application/json
</span></code></pre></div></div>

<h2 id="summary-comparison">Summary Comparison</h2>

<p>The following table summarizes all the different routing strategies implemented in the <a href="https://github.com/mehmandarov/randomstrings/">demo project</a>, illustrating how the HTTP method, path, and headers combine to invoke the correct Java method. The method names refer to the methods in <a href="https://github.com/mehmandarov/randomstrings/blob/master/src/main/java/com/mehmandarov/randomstrings/apidemo/RandomStringsAPIDemoController.java"><code class="language-plaintext highlighter-rouge">RandomStringsAPIDemoController.java</code></a> (or <code class="language-plaintext highlighter-rouge">RandomStringsController.java</code>):</p>

<table>
  <thead>
    <tr>
      <th>HTTP Method</th>
      <th>Path</th>
      <th>Headers</th>
      <th>Method Invoked</th>
      <th>Notes</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">GET</code></td>
      <td><code class="language-plaintext highlighter-rouge">/rnd</code></td>
      <td><em>None</em></td>
      <td><code class="language-plaintext highlighter-rouge">getRndString()</code></td>
      <td>Default (unversioned) endpoint</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">GET</code></td>
      <td><code class="language-plaintext highlighter-rouge">/rnd</code></td>
      <td><code class="language-plaintext highlighter-rouge">Accept: application/json</code></td>
      <td><code class="language-plaintext highlighter-rouge">getRndString()</code></td>
      <td>Standard media type</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">GET</code></td>
      <td><code class="language-plaintext highlighter-rouge">/rnd/v2/</code></td>
      <td><em>Any</em></td>
      <td><code class="language-plaintext highlighter-rouge">getRndStringV2path()</code></td>
      <td>Demo for <strong>path-based</strong> versioning</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">GET</code></td>
      <td><code class="language-plaintext highlighter-rouge">/rnd/versioned</code></td>
      <td><em>None</em></td>
      <td><code class="language-plaintext highlighter-rouge">getRndStringV2Header()</code></td>
      <td>Fallback to <code class="language-plaintext highlighter-rouge">getRndString()</code> if header is missing</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">GET</code></td>
      <td><code class="language-plaintext highlighter-rouge">/rnd/versioned</code></td>
      <td><code class="language-plaintext highlighter-rouge">Accept-Version: 2</code></td>
      <td><code class="language-plaintext highlighter-rouge">getRndStringV2Header()</code></td>
      <td><strong>Header-based</strong> versioning</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">GET</code></td>
      <td><code class="language-plaintext highlighter-rouge">/rnd</code></td>
      <td><code class="language-plaintext highlighter-rouge">Accept: application/rnd.v3+json</code></td>
      <td><code class="language-plaintext highlighter-rouge">getRndStringV3V4MediaType()</code></td>
      <td><strong>Media type versioning</strong> &#8212; v3</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">GET</code></td>
      <td><code class="language-plaintext highlighter-rouge">/rnd</code></td>
      <td><code class="language-plaintext highlighter-rouge">Accept: application/rnd.v4+json</code></td>
      <td><code class="language-plaintext highlighter-rouge">getRndStringV3V4MediaType()</code></td>
      <td><strong>Media type versioning</strong> &#8212; v4</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">GET</code></td>
      <td><code class="language-plaintext highlighter-rouge">/rnd</code></td>
      <td><code class="language-plaintext highlighter-rouge">Accept: application/rnd.v5+json</code></td>
      <td><code class="language-plaintext highlighter-rouge">getRndStringV5MediaType()</code></td>
      <td><strong>Media type versioning</strong> &#8212; v5</td>
    </tr>
  </tbody>
</table>

<h2 id="conclusion">Conclusion</h2>

<p>There is no single correct approach to API versioning. For most teams and public APIs, <strong>URL versioning</strong> is good enough&#8212;it&#8217;s visible, easy to test, and plays well with existing tooling. However, you might want to use <strong>header versioning</strong> if your APIs are primarily consumed by internal services or SDKs that can abstract away the complexity. Reserve <strong>media type versioning</strong> for hypermedia-rich or REST-purist APIs, and only if your tooling supports it end-to-end.</p>

<p>Consider who your consumers are, whether your API is public or internal, your infrastructure maturity, and your team&#8217;s ability to support multiple versions.</p>

<h2 id="whats-next">What&#8217;s Next?</h2>

<p>Versioning is just one part of building robust REST APIs. If you want to dive deeper, have a look at the <a href="https://github.com/mehmandarov/api-guide-java">API Guide for Java</a> repository and the slides in the <a href="https://github.com/mehmandarov/api-guide-java/tree/main/presentation">presentation folder</a>. They cover documentation with OpenAPI, security best practices (like RBAC and JWT integration), advanced patterns (pagination, async APIs), and going beyond REST with gRPC and GraphQL.</p>

<p><strong><em>Happy coding!</em></strong></p>

<hr />]]></content><author><name>Rustam Mehmandarov</name></author><summary type="html">Exploring common API versioning strategies in Java using JAX-RS with Jakarta EE and MicroProfile &#8211; URL, header, and media type versioning &#8211; with pros, cons, and practical code examples.</summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://mehmandarov.com/assets/images/posts-images/container-ship.jpeg"/><category term="blog"/><category term="english"/><category term="java"/><category term="architecture"/><category term="api"/><category term="jakarta ee"/><category term="microprofile"/><category term="jax-rs"/><category term="openapi"/></entry></feed>
