Mastering HTTP Responses in Quarkus: A Java Developer’s Guide to Fine-Grained API Control
From Response.ok() to structured error handling. Learn how Quarkus gives you full control over your REST APIs, with parallels to Spring’s ResponseEntity for easy adoption.
In REST APIs, it’s not just about returning data. It’s about speaking HTTP fluently and responding with the right status codes, headers, and structured bodies. Clients depend on this. Quarkus gives you full control using jakarta.ws.rs.core.Response
, the Jakarta REST implementation (formerly known as JAX-RS) equivalent to Spring’s ResponseEntity
.
Whether you're building a clean REST interface, handling errors gracefully, or adding metadata via headers, mastering Response
makes you a better API designer. And unlike the Spring way, you’ll see how Quarkus makes this lean, reactive, and elegant.
Project Setup: Bring in the Quarkus Arsenal
You can use either the Quarkus CLI or Maven to bootstrap your project.
With the Quarkus CLI:
quarkus create app org.acme:quarkus-response-tutorial \
--extension=rest-jackson,smallrye-openapi \
--no-code
cd quarkus-response-tutorial
Or using Maven directly:
mvn io.quarkus.platform:quarkus-maven-plugin:3.22.2:create \
-DprojectGroupId=org.acme \
-DprojectArtifactId=quarkus-response-tutorial \
-Dextensions="rest-jackson,smallrye-openapi" \
-DnoCode
cd quarkus-response-tutorial
This setup gives you:
Quarkus REST with Jackson support (
quarkus-rest-jackson
)Swagger UI/OpenAPI (
quarkus-smallrye-openapi
)
The Payload: Define a Simple Item
Let’s start with a plain old Java object.
package org.acme;
public class Item {
public String id;
public String name;
public String description;
public Item() {} // Jackson needs this
public Item(String id, String name, String description) {
this.id = id;
this.name = name;
this.description = description;
}
}
This will be the JSON body we return in responses.
Serving A basic Respons with Response.ok()
Let's create our first JAX-RS resource that returns items. Create src/main/java/org/acme/ItemResource.java
::
package org.acme;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
@Path("/items")
@Produces(MediaType.APPLICATION_JSON)
public class ItemResource {
private static final Map<String, Item> items = new ConcurrentHashMap<>();
static {
items.put("1", new Item("1", "Sample Item", "This is a sample item."));
items.put("2", new Item("2", "Another Item", "This is another item."));
}
@GET
@Path("/simple/{id}")
public Response getItem(@PathParam("id") String id) {
Item item = items.get(id);
return (item != null)
? Response.ok(item).build()
: Response.status(Response.Status.NOT_FOUND).build();
}
@GET
@Path("/simple-list")
public Response getAllItems() {
return Response.ok(new ArrayList<>(items.values())).build();
}
@GET
@Path("/direct/{id}")
public Item getDirect(@PathParam("id") String id) {
Item item = items.get(id);
if (item == null) {
throw new NotFoundException("Item with id " + id + " not found.");
}
return item;
}
}
Quarkus gives you flexibility:
@Path("/items")
: Defines the base path for this resource.@Produces(MediaType.APPLICATION_JSON)
: Specifies that methods in this class will, by default, produce JSON responses.quarkus-rest-jackson
handles the conversion.Response.ok(entity)
: This is a convenient static method onResponse
that creates aResponseBuilder
pre-configured with a200 OK
status and the givenentity
(ourItem
orList<Item>
) as the response body..build()
: This method is called on theResponseBuilder
to construct the finalResponse
object.Direct POJO Return: JAX-RS (and Quarkus) allows you to return a POJO directly. If the method is annotated with
@Produces(MediaType.APPLICATION_JSON)
(or the class is), the framework automatically wraps it in a200 OK
response and serializes the POJO to JSON. However, for any status other than200 OK
, or if you need custom headers, you must use theResponse
object.
Start your Quarkus application in dev mode:
./mvnw quarkus:dev
curl -i http://localhost:8080/items/simple/1
You should see:
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
content-length: 70
{"id":"1","name":"Sample Item","description":"This is a sample item."}
You can also navigate to http://localhost:8080/q/swagger-ui
to explore and test your API endpoints visually.
Crafting Better Status Codes and POST
Responses
Let's add methods that return different HTTP status codes.
Update ItemResource.java
: to send a 201 created response.
@POST
@Consumes(MediaType.APPLICATION_JSON)
public Response createItem(Item item, @Context UriInfo uriInfo) {
// Simple ID generation for the example
item.id = UUID.randomUUID().toString();
items.put(item.id, item);
// Build URI for the newly created resource for the Location header
URI location = uriInfo.getAbsolutePathBuilder().path(item.id).build();
// 201 Created with location header and the created item in the body
return Response.created(location).entity(item).build();
}
Want to return an empty body? Useful for operations that succeed but don't need to return data (e.g., some DELETE operations). Send a 204 No Content
:
@GET
@Path("/empty")
public Response getEmpty() {
return Response.noContent().build();
}
Return a 404 Not Found
:
@GET
@Path("/notfound-example/{id}")
public Response getItemByIdWithManualNotFound(@PathParam("id") String id) {
Item item = items.get(id);
if (item != null) {
return Response.ok(item).build();
} else {
// Explicitly return 404 Not Found
// The entity here is a simple String, but it will be wrapped as JSON
// because of the class-level @Produces. For a plain text error,
// you'd override .type(MediaType.TEXT_PLAIN).
return Response.status(Response.Status.NOT_FOUND)
.entity("{\"error\":\"Item with id " + id + " not found.\"}")
// .type(MediaType.APPLICATION_JSON) // Redundant if class @Produces is JSON and
// entity is complex enough or a String that looks like JSON
.build();
}
}
You can test the post endpoint with curl:
curl -X POST -H "Content-Type: application/json" \
-d '{"name":"New Awesome Item","description":"From curl"}' \
http://localhost:8080/items -v
Adding Custom Headers and Cookies
You can add any custom headers to your response using the ResponseBuilder.header()
method.
Add this method to ItemResource.java
:
@GET
@Path("/with-headers")
public Response getItemWithCustomHeaders() {
Item item = new Item("id789", "Header Item", "Item with custom headers.");
return Response.ok(item) // Start with 200 OK and the item body
.header("X-Custom-Header", "MyValue123")
.header("X-Another-Header", "AnotherCoolValue")
.cookie(new NewCookie.Builder("session-id") // Example: setting a cookie
.value("abcxyz789")
.path("/items")
.domain("localhost") // Be careful with domain in real apps
.maxAge(3600) // 1 hour
.secure(false) // true in production over HTTPS
.httpOnly(true)
.build())
.build();
}
This shows how you can set headers, cookies, and other metadata in your response easily with the fluent builder API:
.header("Header-Name", "Header-Value")
: Adds a header to the response. You can call this multiple times for multiple headers. If you call it with the same header name multiple times, it usually appends the values or creates a comma-separated list, depending on the header..cookie(NewCookie)
: Adds aSet-Cookie
header.NewCookie
is a Jakarta REST helper for constructing cookies.Other useful
ResponseBuilder
methods related to headers include:.type(MediaType)
: Sets theContent-Type
header (overriding@Produces
)..language(String language)
: Sets theContent-Language
header..encoding(String encoding)
: Sets theContent-Encoding
header.
Test the endpoint:
curl -v -i http://localhost:8080/items/with-headers
You should see:
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
content-length: 77
Set-Cookie: session-id=abcxyz789;Version=1;Domain=localhost;Path=/items;Max-Age=3600;HttpOnly
X-Another-Header: AnotherCoolValue
X-Custom-Header: MyValue123
{"id":"id789","name":"Header Item","description":"Item with custom headers."}
Structuring Error Responses with POJOs
Instead of returning a simple string for errors, it's better to return a structured error response, typically as a JSON object. Let's create a POJO for error responses.
Create src/main/java/org/acme/ErrorResponse.java
::
package org.acme;
public class ErrorResponse {
public String errorCode;
public String message;
public long timestamp;
public ErrorResponse() {}
public ErrorResponse(String errorCode, String message) {
this.errorCode = errorCode;
this.message = message;
this.timestamp = System.currentTimeMillis();
}
}
Now, let's add a getItemById
method in ItemResource.java
to use this ErrorResponse
POJO:
@GET
@Path("/{id}") // This will be our main GET by ID endpoint
public Response getItemById(@PathParam("id") String id) {
Item item = items.get(id);
if (item != null) {
return Response.ok(item).build();
} else {
ErrorResponse error = new ErrorResponse("E404_ITEM_NOT_FOUND", "Item with id '" + id + "' not found.");
return Response.status(Response.Status.NOT_FOUND)
.entity(error) // Jackson will serialize this to JSON
.build();
}
}
Now clients get a structured JSON error instead of a plain string:
When an item is not found, we now create an
ErrorResponse
object.This
ErrorResponse
object is passed to.entity()
.Because our resource class is annotated with
@Produces(MediaType.APPLICATION_JSON)
and we havequarkus-rest-jackson
on the classpath, Quarkus automatically serializes theErrorResponse
POJO into a JSON string for the response body.
Request an item that does not exist:
curl -i http://localhost:8080/items/nonexistent-id-123
You should see:
HTTP/1.1 404 Not Found
Content-Type: application/json;charset=UTF-8
content-length: 118
{"errorCode":"E404_ITEM_NOT_FOUND","message":"Item with id 'nonexistent-id-123' not found.","timestamp":1747282087234}
Leveling Up with ExceptionMappers
While explicitly building Response
objects for errors in each method works, it can become repetitive. JAX-RS provides ExceptionMapper<T extends Throwable>
for a more centralized and cleaner way to handle specific exceptions and map them to HTTP Response
objects.
package org.acme;
public class ItemNotFoundException extends RuntimeException {
private final String itemId;
public ItemNotFoundException(String itemId) {
super("Item with ID '" + itemId + "' was not found.");
this.itemId = itemId;
}
public String getItemId() {
return itemId;
}
}
package org.acme;
package org.acme;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.ExceptionMapper;
import jakarta.ws.rs.ext.Provider;
@Provider // This annotation registers the mapper with JAX-RS
public class ItemNotFoundMapper implements ExceptionMapper<ItemNotFoundException> {
@Override
public Response toResponse(ItemNotFoundException exception) {
ErrorResponse error = new ErrorResponse(
"ITEM_NOT_FOUND_MAPPED",
exception.getMessage() // Message from the exception
);
return Response.status(Response.Status.NOT_FOUND)
.entity(error)
.build();
}
}
Finally, let’s add a new ItemResource#getItemByIdMappedException
method to throw this exception:
@GET
@Path("/mapped/{id}")
public Response getItemByIdMappedException(@PathParam("id") String id) {
Item item = items.get(id);
if (item != null) {
return Response.ok(item).build();
} else {
// Instead of building the Response here, throw the custom exception
throw new ItemNotFoundException(id);
}
}
Request an item that does not exist:
curl -i http://localhost:8080/items/mapped/nonexistent-id-123
You should see:
HTTP/1.1 404 Not Found
Content-Type: application/json;charset=UTF-8
content-length: 124
{"errorCode":"ITEM_NOT_FOUND_MAPPED","message":"Item with ID 'nonexistent-id-123' was not found.","timestamp":1747282411437}
Much cleaner. Your API stays lean, and error handling stays consistent.
Final Thoughts
Open http://localhost:8080/q/swagger-ui
in your browser to see a list of all available endpoints, their expected parameters, and responses. You can also execute requests directly from the Swagger UI.
You now know how to:
Use
Response
to control status codes and headers.Return POJOs for simple cases and switch to
Response
for power-user flows.Customize error responses with structured JSON.
Centralize exception handling with
ExceptionMapper
.
This is the foundation of building robust, professional REST APIs in Quarkus.
For Spring Developers: ResponseEntity
vs. Quarkus JAX-RS Response
If you're coming from a Spring Boot background, you're likely familiar with org.springframework.http.ResponseEntity
for fine-grained control over HTTP responses. In Quarkus, using Jakarta REST, the jakarta.ws.rs.core.Response
object serves the equivalent purpose. Both allow you to specify the status code, headers, and body of an HTTP response.
1. Returning 200 OK
with a Body
As a Spring Developer, you would use
ResponseEntity.ok()
:
// Spring MVC
// import org.springframework.http.ResponseEntity;
// import com.example.Item; // Your POJO
public ResponseEntity<Item> getItem() {
Item item = itemService.findItem();
return ResponseEntity.ok(item);
}
With Quarkus, you do this with
Response.ok()
:
// Quarkus
// import jakarta.ws.rs.core.Response;
// import com.example.Item; // Your POJO
public Response getItem() {
Item item = itemService.findItem();
return Response.ok(item).build(); // Don't forget .build()
}
Note: Quarkus also allows direct POJO return for
200 OK
, likepublic Item getItem() { return item; }
, if no custom headers or specific status details are needed beyond what@Produces
defines.
2. Returning 201 Created
with a Location
Header and Body
As a Spring Developer, you would use
ResponseEntity.created()
:
// Spring MVC
// import org.springframework.http.ResponseEntity;
// import java.net.URI;
public ResponseEntity<Item> createItem(Item newItem, URI locationUri) {
Item createdItem = itemService.save(newItem);
return ResponseEntity.created(locationUri).body(createdItem);
}
With Quarkus (JAX-RS), you do this with
Response.created()
:
// Quarkus
// import jakarta.ws.rs.core.Response;
// import java.net.URI;
public Response createItem(Item newItem, URI locationUri) {
Item createdItem = itemService.save(newItem);
// .entity() is JAX-RS equivalent of Spring's .body() for ResponseEntity builder
return Response.created(locationUri).entity(createdItem).build();
}
3. Returning 204 No Content
As a Spring Developer, you would use
ResponseEntity.noContent()
:
// Spring MVC
public ResponseEntity<Void> deleteItem() {
itemService.delete();
return ResponseEntity.noContent().build();
}
With Quarkus, you do this with
Response.noContent()
:
// Quarkus
public Response deleteItem() {
itemService.delete();
return Response.noContent().build();
}
Both are very similar here.
4. Returning 404 Not Found
with an Error Body
As a Spring Developer, you might use
ResponseEntity.status()
orResponseEntity.notFound()
:
// Spring MVC
// import org.springframework.http.HttpStatus;
// import com.example.ErrorDetails; // Your error POJO
public ResponseEntity<ErrorDetails> getItemNotFound() {
ErrorDetails errorDetails = new ErrorDetails("Item not found");
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(errorDetails);
// Or, if no body: return ResponseEntity.notFound().build();
}
With Quarkus, you do this with
Response.status()
andResponse.Status
enum:
// Quarkus
// import jakarta.ws.rs.core.Response;
// import com.example.ErrorResponse; // Your error POJO
public Response getItemNotFound() {
ErrorResponse errorResponse = new ErrorResponse("E404", "Item not found");
return Response.status(Response.Status.NOT_FOUND).entity(errorResponse).build();
// Or, if no body: return Response.status(Response.Status.NOT_FOUND).build();
}
Key difference:
HttpStatus
enum in Spring vs.Response.Status
enum in Jakarta REST.
5. Returning a Response with Custom Headers
As a Spring Developer, you would use the
header()
method on the builder:
// Spring MVC
public ResponseEntity<Item> getItemWithCustomHeader() {
Item item = itemService.findItem();
return ResponseEntity.ok()
.header("X-Custom-Info", "SomeValue")
.body(item);
}
With Quarkus (JAX-RS), you also use
header()
on the builder:
// Quarkus
public Response getItemWithCustomHeader() {
Item item = itemService.findItem();
return Response.ok(item) // entity can be passed to ok() directly
.header("X-Custom-Info", "SomeValue")
.build();
}
The pattern is very similar.
Response.ok()
can take the entity directly, or you can chain.entity(item)
.
6. Returning a Specific Status (e.g., 202 Accepted
) with a Body
As a Spring Developer, you would use
ResponseEntity.status()
:
// Spring MVC
public ResponseEntity<ProcessingStatus> processAsync() {
ProcessingStatus status = asyncService.startProcessing();
return ResponseEntity.status(HttpStatus.ACCEPTED).body(status);
}
With Quarkus, you do this with
Response.status()
:
// Quarkus
public Response processAsync() {
ProcessingStatus status = asyncService.startProcessing();
return Response.status(Response.Status.ACCEPTED).entity(status).build();
}
The transition is generally straightforward. The main things to remember are:
The package names and class names (
ResponseEntity
vs.Response
).The enum for status codes (
HttpStatus
vs.Response.Status
).The method to set the body (
.body()
vs..entity()
).JAX-RS
ResponseBuilder
always requires a final.build()
call.
Both frameworks offer powerful and flexible ways to construct HTTP responses, and quarkus-rest-jackson
ensures that your POJOs are easily converted to JSON, just as Spring MVC does with its Jackson integration.
What's Next?
Explore more:
Use
quarkus-security
to protect endpoints.Add OpenAPI annotations for better docs.
Wire in a persistence layer with Panache and PostgreSQL.