Introduction
Quarkus REST (formerly known as RESTEasy Reactive) is the default implementation for Jakarta REST in Quarkus. It is designed to handle requests on IO thread (non-blocking) or worker thread (blocking) depending on return type, providing high throughput without writing complex reactive code.
Jakarta REST Basic Annotations
Resource class
package com.xdev.ecommerce.product;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.net.URI;
import java.util.List;
@Path("/api/v1/products")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class ProductResource {
@GET
public List<ProductDTO> list(
@QueryParam("page") @DefaultValue("0") int page,
@QueryParam("size") @DefaultValue("20") int size,
@QueryParam("category") String category) {
// Trả về list → tự động serialize sang JSON
return productService.findAll(page, size, category);
}
@GET
@Path("/{id}")
public ProductDTO getById(@PathParam("id") Long id) {
return productService.findById(id);
}
@POST
public Response create(CreateProductRequest request) {
ProductDTO created = productService.create(request);
return Response
.created(URI.create("/api/v1/products/" + created.id()))
.entity(created)
.build();
}
@PUT
@Path("/{id}")
public ProductDTO update(@PathParam("id") Long id,
UpdateProductRequest request) {
return productService.update(id, request);
}
@DELETE
@Path("/{id}")
public Response delete(@PathParam("id") Long id) {
productService.delete(id);
return Response.noContent().build();
}
}
DTOs with Java Records
// Request DTOs
public record CreateProductRequest(
String name,
String description,
BigDecimal price,
Long categoryId,
int stockQuantity
) {}
public record UpdateProductRequest(
String name,
String description,
BigDecimal price,
int stockQuantity
) {}
// Response DTO
public record ProductDTO(
Long id,
String name,
String description,
BigDecimal price,
String categoryName,
int stockQuantity,
LocalDateTime createdAt
) {}
Jackson JSON Serialization
Jackson configuration
# application.properties
quarkus.jackson.date-format=yyyy-MM-dd'T'HH:mm:ss
quarkus.jackson.timezone=Asia/Ho_Chi_Minh
quarkus.jackson.serialization.indent-output=true
quarkus.jackson.serialization.write-dates-as-timestamps=false
Custom ObjectMapper
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import io.quarkus.jackson.ObjectMapperCustomizer;
import jakarta.inject.Singleton;
@Singleton
public class RegisterCustomModuleCustomizer implements ObjectMapperCustomizer {
@Override
public void customize(ObjectMapper mapper) {
mapper.enable(SerializationFeature.INDENT_OUTPUT);
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
}
}
Request / Response Filtering
Server Filters
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.container.ContainerResponseContext;
import org.jboss.resteasy.reactive.server.ServerRequestFilter;
import org.jboss.resteasy.reactive.server.ServerResponseFilter;
import io.vertx.core.http.HttpServerRequest;
public class LoggingFilter {
@ServerRequestFilter
public void logRequest(ContainerRequestContext ctx,
HttpServerRequest request) {
// Log incoming request
String method = ctx.getMethod();
String path = ctx.getUriInfo().getPath();
String clientIp = request.remoteAddress().host();
Log.infof("→ %s %s from %s", method, path, clientIp);
}
@ServerResponseFilter
public void logResponse(ContainerResponseContext ctx) {
int status = ctx.getStatus();
Log.infof("← Response: %d", status);
}
}
Custom Header Filter
import org.jboss.resteasy.reactive.server.ServerResponseFilter;
import jakarta.ws.rs.container.ContainerResponseContext;
public class SecurityHeadersFilter {
@ServerResponseFilter
public void addSecurityHeaders(ContainerResponseContext response) {
response.getHeaders().putSingle(
"X-Content-Type-Options", "nosniff");
response.getHeaders().putSingle(
"X-Frame-Options", "DENY");
response.getHeaders().putSingle(
"Cache-Control", "no-store");
}
}
CORS Configuration
# application.properties
quarkus.http.cors=true
quarkus.http.cors.origins=http://localhost:3000,https://ecommerce.xdev.asia
quarkus.http.cors.methods=GET,POST,PUT,DELETE,OPTIONS
quarkus.http.cors.headers=Content-Type,Authorization
quarkus.http.cors.exposed-headers=X-Total-Count
quarkus.http.cors.access-control-max-age=24H
Pagination Response Pattern
import jakarta.ws.rs.core.Response;
@GET
public Response list(@QueryParam("page") @DefaultValue("0") int page,
@QueryParam("size") @DefaultValue("20") int size) {
PanacheQuery<Product> query = Product.findAll();
List<ProductDTO> items = query
.page(Page.of(page, size))
.list()
.stream()
.map(ProductDTO::from)
.toList();
long total = query.count();
return Response.ok(items)
.header("X-Total-Count", total)
.header("X-Page", page)
.header("X-Page-Size", size)
.header("X-Total-Pages", (total + size - 1) / size)
.build();
}
OpenAPI & Swagger UI
# Swagger UI tự động có sẵn ở dev mode
# http://localhost:8080/q/swagger-ui
# Tùy chỉnh OpenAPI info
quarkus.smallrye-openapi.info-title=Product Service API
quarkus.smallrye-openapi.info-version=1.0.0
quarkus.smallrye-openapi.info-description=API quản lý sản phẩm E-Commerce
quarkus.smallrye-openapi.info-contact-name=xdev.asia
quarkus.smallrye-openapi.info-contact-url=https://blog.xdev.asia
# Cho phép Swagger UI ở production (tùy chọn)
quarkus.swagger-ui.always-include=true
OpenAPI Annotations
import org.eclipse.microprofile.openapi.annotations.*;
import org.eclipse.microprofile.openapi.annotations.media.*;
import org.eclipse.microprofile.openapi.annotations.parameters.*;
import org.eclipse.microprofile.openapi.annotations.responses.*;
@Path("/api/v1/products")
@Tag(name = "Products", description = "Quản lý sản phẩm")
public class ProductResource {
@GET
@Operation(summary = "Danh sách sản phẩm",
description = "Lấy danh sách sản phẩm với phân trang")
@APIResponse(responseCode = "200",
description = "Thành công",
content = @Content(
mediaType = "application/json",
schema = @Schema(
implementation = ProductDTO.class)))
public List<ProductDTO> list(
@Parameter(description = "Trang (bắt đầu từ 0)")
@QueryParam("page") @DefaultValue("0") int page,
@Parameter(description = "Số lượng mỗi trang")
@QueryParam("size") @DefaultValue("20") int size) {
// ...
}
@POST
@Operation(summary = "Tạo sản phẩm mới")
@APIResponse(responseCode = "201",
description = "Sản phẩm được tạo thành công")
@APIResponse(responseCode = "400",
description = "Dữ liệu không hợp lệ")
public Response create(
@RequestBody(required = true,
content = @Content(schema = @Schema(
implementation = CreateProductRequest.class)))
CreateProductRequest request) {
// ...
}
}
API Versioning Strategies
URI Versioning (recommended for microservices)
@Path("/api/v1/products")
public class ProductResourceV1 {
@GET
public List<ProductDTOv1> list() { /* ... */ }
}
@Path("/api/v2/products")
public class ProductResourceV2 {
@GET
public List<ProductDTOv2> list() { /* ... */ }
}
Header Versioning
@Path("/api/products")
public class ProductResource {
@GET
public Response list(@HeaderParam("X-API-Version")
@DefaultValue("1") int version) {
return switch (version) {
case 2 -> Response.ok(listV2()).build();
default -> Response.ok(listV1()).build();
};
}
}
Exercises
- Create
ProductResourcewith full CRUD operations - Use Java Records for Request/Response DTOs
- Add pagination support with headers
X-Total-Count,X-Page - Configure CORS for
http://localhost:3000 - Access Swagger UI and test all endpoints
- Add OpenAPI annotations describing details for each endpoint
Summary
- Quarkus REST uses Jakarta REST annotations (
@Path,@GET,@POST...) - Java Records is suitable for immutable DTOs
- Server Filters (
@ServerRequestFilter,@ServerResponseFilter) for cross-cutting concerns - CORS configured in
application.properties - OpenAPI + Swagger UI automatically generates documentation
- API Versioning via URI (
/api/v1/,/api/v2/) simple and clear
Next article: PostgreSQL & Hibernate ORM Panache — Effective Data Layer.