From Spring HATEOAS to Quarkus Hypermedia: A Step-by-Step Migration Guide
Learn how to rebuild Spring Boot’s hypermedia APIs in Quarkus using REST Links and HAL, with full code examples and production notes.
Most Spring Boot developers have touched Spring HATEOAS at some point, often without realizing how much it shapes their API design. By adding navigable links into JSON responses, HATEOAS turns plain REST endpoints into self-discoverable APIs. But what happens when you migrate to Quarkus? The concept of hypermedia is still essential, yet the implementation looks different. In this tutorial we’ll take the familiar Spring “REST HATEOAS” guide and rebuild it step by step in Quarkus, showing you both lightweight Link
headers and HAL+JSON body links, complete with runnable code and migration tips.
Why this matters
Hypermedia keeps clients decoupled from server URLs. Spring developers often use Spring HATEOAS for _links
in the response body. In Quarkus, you have two clean options:
Inject links into standard HTTP
Link
headers (RFC 8288).Emit HAL+JSON with
_links
in the payload.
Both are simple, production-ready patterns.
What we’re migrating
Spring’s guide “Building a Hypermedia-Driven RESTful Web Service” returns a Greeting
plus links. We’ll recreate that in Quarkus, first with header links, then with HAL.
Prerequisites
Java 17+
Maven 3.9+
Quarkus CLI (optional but makes life easy)
Install the Quarkus CLI if you don’t have it. Because it’s cool! You can also directly start with the ready made project from my Github repository.
Bootstrap the Quarkus project
Create a new app with REST + Jackson + REST Links. This gives you JAX-RS endpoints and the link injection support.
quarkus create app org.acme:greet-hypermedia:1.0.0 \
-x rest-jackson,rest-links,smallrye-health
cd greet-hypermedia
If you prefer Maven only, you can add extensions later with ./mvnw quarkus:add-extension
.
quarkus-rest-links
is the Quarkus REST variant. If you still use RESTEasy Classic, the extension isquarkus-resteasy-links
. Stick to Quarkus REST going forward.
Implement the header-links version
Spring HATEOAS adds links into the body. Our first target keeps the payload clean and publishes links in standard HTTP Link
headers (RFC 5988). Many public APIs use this pattern.
Create a model:
package org.acme;
import io.quarkus.resteasy.reactive.links.RestLinkId;
public class Greeting {
@RestLinkId
private int id;
public String name;
public String message;
public Greeting(int id) {
}
public Greeting(String name, String message, int id) {
this.name = name;
this.message = message;
this.id = id;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
}
Create the resource. Modify the scaffolded Java class:
package org.acme;
import io.quarkus.resteasy.reactive.links.InjectRestLinks;
import io.quarkus.resteasy.reactive.links.RestLink;
import io.quarkus.resteasy.reactive.links.RestLinkType;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
@Path("/greeting")
@Produces(MediaType.APPLICATION_JSON)
public class GreetingResource {
@GET
@Path("/{name}")
@RestLink(rel = "self")
@InjectRestLinks(RestLinkType.INSTANCE)
public Greeting greet(@PathParam("name") String name) {
return new Greeting(name, "Hello, " + name + "!");
}
}
Quarkus rest-links allows you to inject web links into the response HTTP headers by just annotating your endpoint resources with the @InjectRestLinks
annotation. To declare the web links that will be returned, you must use the @RestLink
annotation in the linked methods.
Run and test:
./mvnw quarkus:dev
curl -i http://localhost:8080/greeting/Ada
You should see something like:
HTTP/1.1 200 OK
content-length: 38
Content-Type: application/json;charset=UTF-8
Link: <http://localhost:8080/greeting/Ada>; rel="self"
{"name":"Ada","message":"Hello, Ada!"}
Those Link
headers are the hypermedia controls per RFC 5988. Clients follow them just like body links. (IETF Datatracker)
Add tests
Let’s test this briefly. Modify the GreetingResourceTest that the quarkus cli scaffolded for you:
package org.acme;
import io.quarkus.test.junit.QuarkusTest;
import org.junit.jupiter.api.Test;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.containsString;
@QuarkusTest
class GreetingResourceTest {
@Test
void linkHeadersPresent() {
given()
.when().get("/greeting/Ada")
.then()
.statusCode(200)
.header("Link", containsString("rel=\"self\""))
.header("Link", containsString("/greeting/Ada"))
.body(containsString("Hello, Ada!"));
}
}
Run:
quarkus test
Optional: add HAL+JSON body links
If your Spring clients expect _links
in the response body, add the HAL extension and return a HAL representation.
Add the extension:
quarkus extension add quarkus-hal
Why HAL: a simple, widely used convention for embedding links in JSON. (quarkus.io, Stateless Group)
Expose HAL endpoints that build correct URIs. Create the GreetingHalResource.java:
package org.acme;
import org.jboss.resteasy.reactive.common.util.RestMediaType;
import io.quarkus.resteasy.reactive.links.InjectRestLinks;
import io.quarkus.resteasy.reactive.links.RestLink;
import io.quarkus.resteasy.reactive.links.RestLinkType;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
@Path("/greeting-hal")
@Produces(MediaType.APPLICATION_JSON)
public class GreetingHalResource {
@GET
@Produces({ MediaType.APPLICATION_JSON, RestMediaType.APPLICATION_HAL_JSON })
@Path("/{name}")
@RestLink(rel = "self")
@InjectRestLinks(RestLinkType.INSTANCE)
public Greeting greet(@PathParam("name") String name) {
return new Greeting(name, "Hello, " + name + "!");
}
}
Verify:
curl -H "Accept:application/hal+json" http://localhost:8080/greeting-hal/Ada | jq
You should see:
{
"name": "Ada",
"message": "Hello, Ada!",
"_links": {
"self": {
"href": "http://localhost:8080/greeting/Ada"
}
}
}
HAL conventions and rels are simple and documented. If you want _embedded
resources later, you can add them to the representation. Just inject the HalService and add what you need.
Pick a style and ship
Use Link headers if you want a lightweight, standard HTTP approach.
Use HAL if your existing clients already parse
_links
.
You can expose both in parallel during migration. Quarkus REST is the recommended stack.
Production notes
Reverse proxy awareness. If you’re behind NGINX or a load balancer, enable forwarded headers so absolute links resolve correctly when clients sit outside the cluster.
# src/main/resources/application.properties (only when behind a trusted proxy)
quarkus.http.proxy.proxy-address-forwarding=true
quarkus.http.proxy.enable-forwarded-host=true
quarkus.http.proxy.enable-forwarded-prefix=true
Read the HTTP reference and proxy guidance first; enabling these blindly can create spoofing risks if your proxy isn’t locked down. (quarkus.io)
Stable rels. Keep relation names (
self
,next
,prev
,collection
,default
) stable. RFC 8288 governsLink
semantics; HAL defines simple conventions for_links
.
Troubleshooting
No
Link
headers? Ensure you returned a type that matches the entity class marked in@LinkResource
. If your path has a{name}
parameter, annotate the entity field with@ResourceID
so the engine can fill the template.Wrong endpoint names? You’re on Quarkus REST. Use
quarkus-rest-links
, notquarkus-resteasy-links
.Clients expect
_links
but you only emit headers? Keep the/greeting-hal/**
endpoints during migration or move to HAL entirely.
References
Spring’s original HATEOAS getting started guide. (Home)
Quarkus REST Links extension page, with guide and coordinates. (quarkus.io)
Javadoc for RESTEasy Links annotations (
@AddLinks
,@LinkResource
,@ResourceID
). (docs.resteasy.dev)Quarkus HAL extension page. (quarkus.io)
Quarkus REST reference (a.k.a. RESTEasy Reactive). (quarkus.io)
RFC 8288 Web Linking (standard for the
Link
header). (IETF Datatracker)HAL specification. (Stateless Group)
Quarkus HTTP reference and reverse proxy guidance. (quarkus.io)
Pick headers or HAL, keep your rels stable, and ship.