Building REST APIs
Phase 2's live reload is most fun when there's something to hit - an endpoint you can curl, tweak, and see change live. This phase gives you one: an HTTP API for the Product you've been carrying along (an id, a name, a price).
Quarkus didn't invent a new way to write REST APIs. It uses the exact same Jakarta REST (JAX-RS) annotations you'd write on any Java server - @Path, @GET, @POST, @Produces. If you've done the Jakarta EE guide, you already know how to write a Quarkus resource. What Quarkus changes is underneath - its REST engine (Quarkus REST, historically RESTEasy Reactive) does request-matching and wiring at build time instead of startup - the "supersonic" story from Phase 1.
📝 So this phase isn't "learn REST" - it's "see the REST you know running on Quarkus," plus two genuinely Quarkus-flavored wrinkles: extensions (turning on features like JSON) and the imperative-vs-reactive return-type choice. If @Path, @PathParam, or status codes feel fuzzy, the JAX-RS phase and REST APIs Explained teach them from scratch.
It's JAX-RS (Quarkus REST / RESTEasy Reactive)
A REST endpoint in Quarkus is a resource class: a plain class marked with @Path, whose methods are marked with HTTP-verb annotations. Here's one that returns a list of products as JSON:
;
;
;
;
;
;
What just happened: @Path("/products") mapped this class to /products, @GET handles GET requests, and @Produces(APPLICATION_JSON) declares the response format, so List<Product> serializes automatically. The imports are jakarta.ws.rs.* - standard JAX-RS, nothing Quarkus-specific. (The hardcoded list is a placeholder - real data arrives in Phase 5.)
HTTP/1.1
What just happened: the List<Product> came back as a JSON array. You wrote zero serialization code. (Quarkus defaults to port 8080, same as a classic server.)
Path and query params
📝 A path param is part of the URL (/products/2 = "product with id 2"): a placeholder in @Path, bound with @PathParam. A query param is a value after ? (/products?maxPrice=50), bound with @QueryParam. Path param identifies; query param filters.
;
;
;
;
What just happened: in getOne, {id} lines up with @PathParam("id") Long id - Quarkus pulls 2 from /products/2 and converts it. In list, @QueryParam("maxPrice") is null when the client omits it, so we return everything. (service moves into a CDI bean in Phase 4.)
{"id":2,"name":"USB-C Hub","price":49.50}
[{"id":2,"name":"USB-C Hub","price":49.50}]
Request bodies and JSON
To create a product the client sends JSON in the body - and here's the first genuinely Quarkus-flavored step: JSON binding isn't on by default. You add it with an extension.
⚠️ Write a @POST taking a Product body before adding a JSON extension and it won't deserialize - Quarkus doesn't ship Jackson in the core. Add quarkus-rest-jackson:
What just happened: that edited your build file to include the extension; dev mode picked it up on the next request. From here, JSON binds both in and out - the earlier @GETs also rely on this extension being present.
Now the create endpoint. To report 201 Created instead of the default 200, return a RestResponse<Product>:
;
;
;
What just happened: the unannotated Product product parameter is the request body - @Consumes(APPLICATION_JSON) deserializes it before your code runs. RestResponse<Product> is Quarkus REST's type-safe wrapper over the classic JAX-RS Response (either works); returning a bare Product would always yield 200.
What just happened: the client posts a product with no id - the server assigns it. The extension binds name/price onto a Product before create runs, then serializes the saved product back out.
Extensions - the Quarkus way to add features
You just used an extension to add JSON - worth pausing on what one actually is, since it's how you'll add every capability from here on.
📝 An extension is a Quarkus-aware module - quarkus-rest-jackson for JSON, quarkus-hibernate-orm-panache for persistence, quarkus-jdbc-postgresql for a driver, dozens more:
What just happened: both capabilities added in one go, each wiring itself into the build with sensible defaults.
💡 Why an extension and not a plain dependency? An extension hooks into Quarkus's build-time processing (Phase 1's idea) - it contributes a build step that does scanning, reflection registration, and wiring while compiling, so startup stays instant and the feature still works in a native image (Phase 9), where runtime reflection isn't available. Each feature pays its setup cost once, at build time, instead of on every boot.
Imperative vs reactive (a preview)
Every handler above returned a value directly - the imperative style: the method blocks until it has the answer and returns it. Quarkus REST also lets a handler return a reactive type - a Uni<Product> (a promise) or Multi<Product> (a stream) - which hands the request's thread back while waiting on I/O, then resumes when data is ready. Both styles run on the same stack; you mix them per endpoint. Phase 7 covers Uni/Multi properly - for now, just know the door exists.
💡 Whichever style you pick: keep the resource thin. A handler reads the request, hands the real work to a CDI bean (Phase 4) backed by Panache (Phase 5), and shapes the response. That separation is what lets you swap imperative for reactive without touching the doorway.
Recap
- It's standard JAX-RS, build-time optimized. Quarkus REST (RESTEasy Reactive) runs the same
@Path/@GET/@POST/@Producesannotations you'd write on any Jakarta server - but does the wiring at compile time, which is what makes startup nearly instant. - Path params identify, query params filter.
@PathParambinds{id}from the path (one specific product);@QueryParambinds?maxPrice=...and isnullwhen the client omits it. - JSON comes from the
quarkus-rest-jacksonextension. Add it, and the engine deserializes the unannotated@POSTbody into a Java object and serializes return values back to JSON. RestResponsecontrols status and headers. Return a bare object for the default 200, or aRestResponse<Product>for 201 Created and other correct status codes (the classicResponseworks too).- Extensions are how you add features. An extension is a build-time-aware module added with
quarkus extension add; it hooks Quarkus's build-time processing, which is why features stay fast and work in native images. - Imperative or reactive, same stack. A handler can return a plain
Productor aUni<Product>- both work; reactive comes in Phase 7. Keep the resource thin and push logic into a CDI bean.
Quick check
Make sure the Quarkus-flavored bits stuck:
[
{
"q": "Your @POST endpoint takes a Product body, but the incoming JSON isn't being deserialized into the object. What's the most likely cause?",
"choices": [
"The quarkus-rest-jackson extension isn't added - Quarkus doesn't ship JSON binding in the core, you turn it on with an extension",
"JAX-RS annotations don't work in Quarkus; you need Quarkus-specific ones",
"@POST methods can't accept a request body",
"You must annotate the body parameter with @QueryParam"
],
"answer": 0,
"explain": "JSON binding is a capability you add via an extension. Without quarkus-rest-jackson, the engine has no JSON mapper, so the body won't deserialize. The annotations are standard JAX-RS, and the unannotated parameter IS the body - adding the extension is what's missing."
},
{
"q": "Why does Quarkus use 'extensions' instead of plain Maven/Gradle dependencies for features like JSON and persistence?",
"choices": [
"An extension hooks into Quarkus's build-time processing, doing scanning and wiring at compile time so startup stays fast and the feature works in native images",
"Extensions are just a rebrand of dependencies with no technical difference",
"Extensions run only at runtime and skip the build entirely",
"Plain dependencies aren't allowed in a Quarkus project"
],
"answer": 0,
"explain": "An extension contributes a build step that moves scanning, reflection registration, and wiring to compile time - the core Quarkus idea. That keeps boot nearly instant and makes the feature survive native compilation, where runtime reflection isn't available."
},
{
"q": "Your create endpoint returns a plain Product and clients always get HTTP 200, even though a resource was created. How do you report 201 Created?",
"choices": [
"Return a RestResponse<Product>, e.g. RestResponse.status(RestResponse.Status.CREATED, saved), which carries the status alongside the body",
"Add @POST(status = 201) to the method",
"Throw an exception after saving so the server picks a different code",
"Nothing can change it - Quarkus REST methods only return 200"
],
"answer": 0,
"explain": "Returning a bare object gives the default 200. To set the status (and headers), return a RestResponse - RestResponse.status(Status.CREATED, saved) reports 201 Created with the body. The classic JAX-RS Response works the same way."
}
]
Before the quiz: without looking back, say (or jot down) the core idea of this phase in your own words.
Check your understanding 3 questions
1. Your @POST endpoint takes a Product body, but the incoming JSON isn't being deserialized into the object. What's the most likely cause?
2. Why does Quarkus use 'extensions' instead of plain Maven/Gradle dependencies for features like JSON and persistence?
3. Your create endpoint returns a plain Product and clients always get HTTP 200, even though a resource was created. How do you report 201 Created?