Chuyển đến nội dung chính

Lesson 3: Quarkus REST — Building a professional RESTful API

Quarkus REST (Jakarta REST) ​​with @Path, @GET, @POST, JSON serialization, CORS, OpenAPI & Swagger UI, API versioning.

💻 Programming — Lesson 2 Lesson 3: Quarkus REST — Building a RESTful API professional

Quarkus Microservices: From Basics to Production

Part 1: Quarkus Platform & Project Setup

xdev.asia

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

  1. Create ProductResource with full CRUD operations
  2. Use Java Records for Request/Response DTOs
  3. Add pagination support with headers X-Total-Count, X-Page
  4. Configure CORS for http://localhost:3000
  5. Access Swagger UI and test all endpoints
  6. 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.