Menu

Quarkus Topic

Building RESTful APIs with JAX-RS in Quarkus

Overview of RESTEasy Reactive

RESTEasy Reactive is Quarkus's implementation of JAX-RS that provides:

  • Reactive programming model - Non-blocking I/O

  • Excellent performance - Optimized for Quarkus

  • Unified programming model - Imperative and reactive endpoints

  • Automatic JSON serialization - Built-in Jackson/JSON-B support


1. Basic REST Endpoints

Project Setup

bash

# Create new Quarkus project with RESTEasy Reactive
mvn io.quarkus.platform:quarkus-maven-plugin:3.2.0.Final:create \
    -DprojectGroupId=com.example \
    -DprojectArtifactId=quarkus-rest-api \
    -Dextensions="resteasy-reactive-jackson,quarkus-hibernate-validator"

cd quarkus-rest-api

Basic Resource Class

java

package com.example;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import java.time.LocalDateTime;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;

@Path("/api")
public class BasicResource {

    // Simple GET endpoint
    @GET
    @Path("/hello")
    @Produces(MediaType.TEXT_PLAIN)
    public String hello() {
        return "Hello from RESTEasy Reactive!";
    }

    // JSON response
    @GET
    @Path("/info")
    @Produces(MediaType.APPLICATION_JSON)
    public ApiInfo getInfo() {
        return new ApiInfo("Quarkus REST API", "1.0.0", LocalDateTime.now());
    }

    // Reactive endpoint
    @GET
    @Path("/async")
    @Produces(MediaType.APPLICATION_JSON)
    public CompletionStage<ApiInfo> getAsyncInfo() {
        return CompletableFuture.supplyAsync(() -> 
            new ApiInfo("Async API", "1.0.0", LocalDateTime.now())
        );
    }

    // Response with path parameter
    @GET
    @Path("/greet/{name}")
    @Produces(MediaType.APPLICATION_JSON)
    public Greeting greetUser(@PathParam("name") String name) {
        return new Greeting("Hello, " + name + "!", LocalDateTime.now());
    }

    public static class ApiInfo {
        public String name;
        public String version;
        public LocalDateTime timestamp;

        public ApiInfo() {}

        public ApiInfo(String name, String version, LocalDateTime timestamp) {
            this.name = name;
            this.version = version;
            this.timestamp = timestamp;
        }
    }

    public static class Greeting {
        public String message;
        public LocalDateTime timestamp;

        public Greeting() {}

        public Greeting(String message, LocalDateTime timestamp) {
            this.message = message;
            this.timestamp = timestamp;
        }
    }
}

2. Comprehensive CRUD API

Entity Classes

java

package com.example.entity;

import jakarta.persistence.*;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import java.time.LocalDateTime;

@Entity
@Table(name = "users")
public class User {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @NotBlank(message = "Username is required")
    @Size(min = 3, max = 50, message = "Username must be between 3 and 50 characters")
    @Column(unique = true, nullable = false)
    private String username;
    
    @NotBlank(message = "Email is required")
    @Email(message = "Email should be valid")
    @Column(unique = true, nullable = false)
    private String email;
    
    private String firstName;
    private String lastName;
    
    @Enumerated(EnumType.STRING)
    private UserRole role = UserRole.USER;
    
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
    
    // Lifecycle callbacks
    @PrePersist
    protected void onCreate() {
        createdAt = LocalDateTime.now();
        updatedAt = LocalDateTime.now();
    }
    
    @PreUpdate
    protected void onUpdate() {
        updatedAt = LocalDateTime.now();
    }
    
    // Constructors
    public User() {}
    
    public User(String username, String email, String firstName, String lastName) {
        this.username = username;
        this.email = email;
        this.firstName = firstName;
        this.lastName = lastName;
    }
    
    // Getters and Setters
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    
    public String getUsername() { return username; }
    public void setUsername(String username) { this.username = username; }
    
    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }
    
    public String getFirstName() { return firstName; }
    public void setFirstName(String firstName) { this.firstName = firstName; }
    
    public String getLastName() { return lastName; }
    public void setLastName(String lastName) { this.lastName = lastName; }
    
    public UserRole getRole() { return role; }
    public void setRole(UserRole role) { this.role = role; }
    
    public LocalDateTime getCreatedAt() { return createdAt; }
    public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
    
    public LocalDateTime getUpdatedAt() { return updatedAt; }
    public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
}

enum UserRole {
    USER, ADMIN, MODERATOR
}

DTO Classes

java

package com.example.dto;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import java.time.LocalDateTime;

// Request DTO for creating users
public class CreateUserRequest {
    
    @NotBlank(message = "Username is required")
    @Size(min = 3, max = 50, message = "Username must be between 3 and 50 characters")
    private String username;
    
    @NotBlank(message = "Email is required")
    @Email(message = "Email should be valid")
    private String email;
    
    private String firstName;
    private String lastName;
    
    // Default constructor
    public CreateUserRequest() {}
    
    // All args constructor
    public CreateUserRequest(String username, String email, String firstName, String lastName) {
        this.username = username;
        this.email = email;
        this.firstName = firstName;
        this.lastName = lastName;
    }
    
    // Getters and Setters
    public String getUsername() { return username; }
    public void setUsername(String username) { this.username = username; }
    
    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }
    
    public String getFirstName() { return firstName; }
    public void setFirstName(String firstName) { this.firstName = firstName; }
    
    public String getLastName() { return lastName; }
    public void setLastName(String lastName) { this.lastName = lastName; }
}

// Response DTO for user data
public class UserResponse {
    private Long id;
    private String username;
    private String email;
    private String firstName;
    private String lastName;
    private String role;
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
    
    public UserResponse() {}
    
    // Getters and Setters
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    
    public String getUsername() { return username; }
    public void setUsername(String username) { this.username = username; }
    
    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }
    
    public String getFirstName() { return firstName; }
    public void setFirstName(String firstName) { this.firstName = firstName; }
    
    public String getLastName() { return lastName; }
    public void setLastName(String lastName) { this.lastName = lastName; }
    
    public String getRole() { return role; }
    public void setRole(String role) { this.role = role; }
    
    public LocalDateTime getCreatedAt() { return createdAt; }
    public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
    
    public LocalDateTime getUpdatedAt() { return updatedAt; }
    public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
}

// DTO for updating users
public class UpdateUserRequest {
    @Email(message = "Email should be valid")
    private String email;
    
    private String firstName;
    private String lastName;
    
    // Getters and Setters
    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }
    
    public String getFirstName() { return firstName; }
    public void setFirstName(String firstName) { this.firstName = firstName; }
    
    public String getLastName() { return lastName; }
    public void setLastName(String lastName) { this.lastName = lastName; }
}

Service Layer

java

package com.example.service;

import com.example.entity.User;
import com.example.entity.UserRole;
import com.example.dto.CreateUserRequest;
import com.example.dto.UpdateUserRequest;
import com.example.dto.UserResponse;
import com.example.exception.UserNotFoundException;
import com.example.exception.DuplicateUserException;
import io.quarkus.hibernate.orm.panache.PanacheRepository;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.persistence.EntityManager;
import jakarta.transaction.Transactional;
import jakarta.validation.Valid;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

@ApplicationScoped
public class UserService {
    
    @Inject
    EntityManager entityManager;
    
    @Transactional
    public UserResponse createUser(@Valid CreateUserRequest request) {
        // Check if username already exists
        if (User.find("username", request.getUsername()).firstResultOptional().isPresent()) {
            throw new DuplicateUserException("Username already exists: " + request.getUsername());
        }
        
        // Check if email already exists
        if (User.find("email", request.getEmail()).firstResultOptional().isPresent()) {
            throw new DuplicateUserException("Email already exists: " + request.getEmail());
        }
        
        // Create new user
        User user = new User();
        user.setUsername(request.getUsername());
        user.setEmail(request.getEmail());
        user.setFirstName(request.getFirstName());
        user.setLastName(request.getLastName());
        user.setRole(UserRole.USER);
        
        user.persist();
        
        return mapToResponse(user);
    }
    
    public List<UserResponse> getAllUsers() {
        return User.listAll()
                  .stream()
                  .map(user -> mapToResponse((User) user))
                  .collect(Collectors.toList());
    }
    
    public UserResponse getUserById(Long id) {
        User user = User.findById(id);
        if (user == null) {
            throw new UserNotFoundException("User not found with id: " + id);
        }
        return mapToResponse(user);
    }
    
    public UserResponse getUserByUsername(String username) {
        Optional<User> user = User.find("username", username).firstResultOptional();
        if (user.isEmpty()) {
            throw new UserNotFoundException("User not found with username: " + username);
        }
        return mapToResponse(user.get());
    }
    
    @Transactional
    public UserResponse updateUser(Long id, @Valid UpdateUserRequest request) {
        User user = User.findById(id);
        if (user == null) {
            throw new UserNotFoundException("User not found with id: " + id);
        }
        
        // Update fields if provided
        if (request.getEmail() != null && !request.getEmail().isBlank()) {
            // Check if new email is already taken by another user
            Optional<User> existingUser = User.find("email = ?1 and id != ?2", 
                                                   request.getEmail(), id).firstResultOptional();
            if (existingUser.isPresent()) {
                throw new DuplicateUserException("Email already taken: " + request.getEmail());
            }
            user.setEmail(request.getEmail());
        }
        
        if (request.getFirstName() != null) {
            user.setFirstName(request.getFirstName());
        }
        
        if (request.getLastName() != null) {
            user.setLastName(request.getLastName());
        }
        
        // Entity is automatically updated due to @Transactional
        
        return mapToResponse(user);
    }
    
    @Transactional
    public void deleteUser(Long id) {
        User user = User.findById(id);
        if (user == null) {
            throw new UserNotFoundException("User not found with id: " + id);
        }
        user.delete();
    }
    
    private UserResponse mapToResponse(User user) {
        UserResponse response = new UserResponse();
        response.setId(user.getId());
        response.setUsername(user.getUsername());
        response.setEmail(user.getEmail());
        response.setFirstName(user.getFirstName());
        response.setLastName(user.getLastName());
        response.setRole(user.getRole().name());
        response.setCreatedAt(user.getCreatedAt());
        response.setUpdatedAt(user.getUpdatedAt());
        return response;
    }
}

3. Comprehensive REST Resource

java

package com.example.resource;

import com.example.dto.CreateUserRequest;
import com.example.dto.UpdateUserRequest;
import com.example.dto.UserResponse;
import com.example.service.UserService;
import com.example.exception.UserNotFoundException;
import com.example.exception.DuplicateUserException;
import jakarta.inject.Inject;
import jakarta.validation.Valid;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.UriInfo;
import org.jboss.resteasy.reactive.RestPath;
import org.jboss.resteasy.reactive.RestQuery;

import java.net.URI;
import java.util.List;

@Path("/api/v1/users")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class UserResource {

    @Inject
    UserService userService;

    @Context
    UriInfo uriInfo;

    // GET /api/v1/users - Get all users
    @GET
    public Response getAllUsers(
            @RestQuery @DefaultValue("0") int page,
            @RestQuery @DefaultValue("20") int size) {
        
        List<UserResponse> users = userService.getAllUsers();
        
        // Simple pagination (in real app, use Panache pagination)
        int fromIndex = Math.min(page * size, users.size());
        int toIndex = Math.min((page + 1) * size, users.size());
        
        List<UserResponse> paginatedUsers = users.subList(fromIndex, toIndex);
        
        return Response.ok(paginatedUsers)
                .header("X-Total-Count", users.size())
                .header("X-Page", page)
                .header("X-Size", size)
                .build();
    }

    // GET /api/v1/users/{id} - Get user by ID
    @GET
    @Path("/{id}")
    public Response getUserById(@RestPath Long id) {
        try {
            UserResponse user = userService.getUserById(id);
            return Response.ok(user).build();
        } catch (UserNotFoundException e) {
            throw new NotFoundException(e.getMessage());
        }
    }

    // GET /api/v1/users/username/{username} - Get user by username
    @GET
    @Path("/username/{username}")
    public Response getUserByUsername(@RestPath String username) {
        try {
            UserResponse user = userService.getUserByUsername(username);
            return Response.ok(user).build();
        } catch (UserNotFoundException e) {
            throw new NotFoundException(e.getMessage());
        }
    }

    // POST /api/v1/users - Create new user
    @POST
    public Response createUser(@Valid CreateUserRequest request) {
        try {
            UserResponse createdUser = userService.createUser(request);
            
            URI location = uriInfo.getAbsolutePathBuilder()
                    .path(createdUser.getId().toString())
                    .build();
            
            return Response.created(location)
                    .entity(createdUser)
                    .build();
                    
        } catch (DuplicateUserException e) {
            return Response.status(Response.Status.CONFLICT)
                    .entity(new ErrorResponse("CONFLICT", e.getMessage()))
                    .build();
        }
    }

    // PUT /api/v1/users/{id} - Update user
    @PUT
    @Path("/{id}")
    public Response updateUser(@RestPath Long id, @Valid UpdateUserRequest request) {
        try {
            UserResponse updatedUser = userService.updateUser(id, request);
            return Response.ok(updatedUser).build();
            
        } catch (UserNotFoundException e) {
            throw new NotFoundException(e.getMessage());
        } catch (DuplicateUserException e) {
            return Response.status(Response.Status.CONFLICT)
                    .entity(new ErrorResponse("CONFLICT", e.getMessage()))
                    .build();
        }
    }

    // DELETE /api/v1/users/{id} - Delete user
    @DELETE
    @Path("/{id}")
    public Response deleteUser(@RestPath Long id) {
        try {
            userService.deleteUser(id);
            return Response.noContent().build();
        } catch (UserNotFoundException e) {
            throw new NotFoundException(e.getMessage());
        }
    }

    // PATCH /api/v1/users/{id}/role - Update user role
    @PATCH
    @Path("/{id}/role")
    public Response updateUserRole(@RestPath Long id, String role) {
        try {
            // Implementation for role update
            return Response.ok().entity(new MessageResponse("Role updated successfully")).build();
        } catch (UserNotFoundException e) {
            throw new NotFoundException(e.getMessage());
        }
    }

    // Utility classes for responses
    public static class ErrorResponse {
        public String error;
        public String message;
        public long timestamp;

        public ErrorResponse() {
            this.timestamp = System.currentTimeMillis();
        }

        public ErrorResponse(String error, String message) {
            this();
            this.error = error;
            this.message = message;
        }
    }

    public static class MessageResponse {
        public String message;
        public long timestamp;

        public MessageResponse() {
            this.timestamp = System.currentTimeMillis();
        }

        public MessageResponse(String message) {
            this();
            this.message = message;
        }
    }
}

4. Advanced REST Features

Reactive Endpoints

java

package com.example.resource;

import io.smallrye.mutiny.Multi;
import io.smallrye.mutiny.Uni;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import org.jboss.resteasy.reactive.RestStreamElementType;

import java.time.Duration;
import java.util.List;

@Path("/api/reactive")
public class ReactiveResource {

    // Reactive endpoint returning Uni (single item)
    @GET
    @Path("/user/{id}")
    @Produces(MediaType.APPLICATION_JSON)
    public Uni<UserResponse> getUserReactive(@PathParam("id") Long id) {
        return Uni.createFrom().item(() -> {
            // Simulate database call
            try { Thread.sleep(100); } catch (InterruptedException e) {}
            return new UserResponse(id, "reactive_user", "reactive@example.com");
        });
    }

    // Reactive endpoint returning Multi (stream)
    @GET
    @Path("/users")
    @Produces(MediaType.SERVER_SENT_EVENTS)
    @RestStreamElementType(MediaType.APPLICATION_JSON)
    public Multi<UserResponse> streamUsers() {
        return Multi.createFrom().ticks().every(Duration.ofSeconds(1))
                .onItem().transform(n -> 
                    new UserResponse(n, "user_" + n, "user" + n + "@example.com")
                )
                .select().first(10); // Stream 10 users
    }

    // Batch processing with reactive
    @GET
    @Path("/batch-process")
    public Uni<String> processBatch() {
        return Multi.createFrom().items(1, 2, 3, 4, 5)
                .onItem().transform(i -> "Processing item " + i)
                .collect().asList()
                .onItem().transform(list -> String.join(", ", list));
    }
}

File Upload/Download

java

package com.example.resource;

import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.StreamingOutput;
import org.jboss.resteasy.reactive.multipart.FileUpload;

import java.io.*;
import java.nio.file.Files;
import java.nio.file.Paths;

@Path("/api/files")
public class FileResource {

    private final String UPLOAD_DIR = "uploads/";

    @POST
    @Path("/upload")
    @Consumes(MediaType.MULTIPART_FORM_DATA)
    @Produces(MediaType.APPLICATION_JSON)
    public Response uploadFile(FileUpload file) {
        try {
            String fileName = file.fileName();
            File uploadDir = new File(UPLOAD_DIR);
            if (!uploadDir.exists()) {
                uploadDir.mkdirs();
            }
            
            File destination = new File(UPLOAD_DIR + fileName);
            Files.copy(file.uploadedFile(), destination.toPath());
            
            return Response.ok(new FileUploadResponse(fileName, destination.getAbsolutePath(), file.size()))
                    .build();
                    
        } catch (IOException e) {
            return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
                    .entity(new ErrorResponse("UPLOAD_ERROR", e.getMessage()))
                    .build();
        }
    }

    @GET
    @Path("/download/{filename}")
    @Produces(MediaType.APPLICATION_OCTET_STREAM)
    public Response downloadFile(@PathParam("filename") String filename) {
        File file = new File(UPLOAD_DIR + filename);
        
        if (!file.exists()) {
            throw new NotFoundException("File not found: " + filename);
        }
        
        StreamingOutput stream = output -> {
            try (FileInputStream input = new FileInputStream(file)) {
                byte[] buffer = new byte[4096];
                int bytesRead;
                while ((bytesRead = input.read(buffer)) != -1) {
                    output.write(buffer, 0, bytesRead);
                }
            }
        };
        
        return Response.ok(stream)
                .header("Content-Disposition", "attachment; filename=\"" + filename + "\"")
                .build();
    }

    public static class FileUploadResponse {
        public String filename;
        public String filepath;
        public long size;
        public String message;

        public FileUploadResponse(String filename, String filepath, long size) {
            this.filename = filename;
            this.filepath = filepath;
            this.size = size;
            this.message = "File uploaded successfully";
        }
    }
}

5. Exception Handling

Custom Exceptions

java

package com.example.exception;

public class UserNotFoundException extends RuntimeException {
    public UserNotFoundException(String message) {
        super(message);
    }
}

package com.example.exception;

public class DuplicateUserException extends RuntimeException {
    public DuplicateUserException(String message) {
        super(message);
    }
}

package com.example.exception;

public class ValidationException extends RuntimeException {
    public ValidationException(String message) {
        super(message);
    }
}

Exception Mapper

java

package com.example.exception;

import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.ExceptionMapper;
import jakarta.ws.rs.ext.Provider;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
import jakarta.ws.rs.NotFoundException;

import java.util.List;
import java.util.stream.Collectors;

@Provider
public class GlobalExceptionMapper implements ExceptionMapper<Exception> {

    @Override
    public Response toResponse(Exception exception) {
        
        if (exception instanceof UserNotFoundException || exception instanceof NotFoundException) {
            return Response.status(Response.Status.NOT_FOUND)
                    .entity(new ErrorResponse("NOT_FOUND", exception.getMessage()))
                    .build();
        }
        
        if (exception instanceof DuplicateUserException) {
            return Response.status(Response.Status.CONFLICT)
                    .entity(new ErrorResponse("CONFLICT", exception.getMessage()))
                    .build();
        }
        
        if (exception instanceof ConstraintViolationException) {
            ConstraintViolationException cve = (ConstraintViolationException) exception;
            List<String> violations = cve.getConstraintViolations()
                    .stream()
                    .map(ConstraintViolation::getMessage)
                    .collect(Collectors.toList());
            
            return Response.status(Response.Status.BAD_REQUEST)
                    .entity(new ValidationErrorResponse("VALIDATION_ERROR", 
                            "Validation failed", violations))
                    .build();
        }
        
        // Generic exception
        return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
                .entity(new ErrorResponse("INTERNAL_ERROR", 
                        "An unexpected error occurred"))
                .build();
    }

    public static class ErrorResponse {
        public String error;
        public String message;
        public long timestamp;

        public ErrorResponse(String error, String message) {
            this.error = error;
            this.message = message;
            this.timestamp = System.currentTimeMillis();
        }
    }

    public static class ValidationErrorResponse extends ErrorResponse {
        public List<String> violations;

        public ValidationErrorResponse(String error, String message, List<String> violations) {
            super(error, message);
            this.violations = violations;
        }
    }
}

6. JSON Serialization Configuration

Custom JSON Serialization

java

package com.example.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import io.quarkus.jackson.ObjectMapperCustomizer;
import jakarta.inject.Singleton;

@Singleton
public class JacksonConfig implements ObjectMapperCustomizer {

    @Override
    public void customize(ObjectMapper objectMapper) {
        // Configure Jackson for better JSON handling
        objectMapper
            .registerModule(new JavaTimeModule())
            .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
            .enable(SerializationFeature.INDENT_OUTPUT)
            .disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
    }
}

Custom JSON Views

java

package com.example.views;

public class JsonViews {
    // Public view (excludes sensitive data)
    public static class Public {}
    
    // Internal view (includes all data)
    public static class Internal extends Public {}
    
    // Admin view (includes sensitive admin data)
    public static class Admin extends Internal {}
}

// Usage in entity
public class User {
    @JsonView(JsonViews.Public.class)
    private Long id;
    
    @JsonView(JsonViews.Public.class)
    private String username;
    
    @JsonView(JsonViews.Internal.class)
    private String email;
    
    @JsonView(JsonViews.Admin.class)
    private String internalNotes;
    
    // ... getters and setters
}

// Usage in resource
@GET
@Path("/{id}")
@JsonView(JsonViews.Public.class)
public UserResponse getUserPublic(@PathParam("id") Long id) {
    return userService.getUserById(id);
}

@GET
@Path("/{id}/internal")
@JsonView(JsonViews.Internal.class)
public UserResponse getUserInternal(@PathParam("id") Long id) {
    return userService.getUserById(id);
}

7. Testing REST Endpoints

Unit Tests

java

package com.example.resource;

import io.quarkus.test.junit.QuarkusTest;
import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import org.junit.jupiter.api.Test;

import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.*;

@QuarkusTest
class UserResourceTest {

    @Test
    void testGetAllUsers() {
        given()
          .when().get("/api/v1/users")
          .then()
             .statusCode(200)
             .contentType(ContentType.JSON)
             .body("$.size()", greaterThanOrEqualTo(0));
    }

    @Test
    void testCreateUser() {
        String userJson = """
            {
                "username": "testuser",
                "email": "test@example.com",
                "firstName": "Test",
                "lastName": "User"
            }
            """;

        given()
          .contentType(ContentType.JSON)
          .body(userJson)
          .when().post("/api/v1/users")
          .then()
             .statusCode(201)
             .contentType(ContentType.JSON)
             .body("username", is("testuser"))
             .body("email", is("test@example.com"))
             .header("Location", notNullValue());
    }

    @Test
    void testCreateUserValidation() {
        String invalidUserJson = """
            {
                "username": "ab",  // too short
                "email": "invalid-email"
            }
            """;

        given()
          .contentType(ContentType.JSON)
          .body(invalidUserJson)
          .when().post("/api/v1/users")
          .then()
             .statusCode(400)
             .contentType(ContentType.JSON)
             .body("error", is("VALIDATION_ERROR"));
    }

    @Test
    void testGetNonExistentUser() {
        given()
          .when().get("/api/v1/users/9999")
          .then()
             .statusCode(404)
             .contentType(ContentType.JSON)
             .body("error", is("NOT_FOUND"));
    }
}

Integration Test with Test Profile

java

package com.example.resource;

import io.quarkus.test.TestTransaction;
import io.quarkus.test.junit.QuarkusTest;
import io.restassured.http.ContentType;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;

import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.*;

@QuarkusTest
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@TestTransaction
class UserResourceIntegrationTest {

    private static Long createdUserId;

    @Test
    @Order(1)
    void testCreateUser() {
        String userJson = """
            {
                "username": "integrationuser",
                "email": "integration@example.com",
                "firstName": "Integration",
                "lastName": "Test"
            }
            """;

        createdUserId = given()
          .contentType(ContentType.JSON)
          .body(userJson)
          .when().post("/api/v1/users")
          .then()
             .statusCode(201)
             .contentType(ContentType.JSON)
             .body("username", is("integrationuser"))
             .extract().path("id");
    }

    @Test
    @Order(2)
    void testGetCreatedUser() {
        given()
          .when().get("/api/v1/users/{id}", createdUserId)
          .then()
             .statusCode(200)
             .contentType(ContentType.JSON)
             .body("username", is("integrationuser"))
             .body("email", is("integration@example.com"));
    }

    @Test
    @Order(3)
    void testUpdateUser() {
        String updateJson = """
            {
                "email": "updated@example.com",
                "firstName": "Updated"
            }
            """;

        given()
          .contentType(ContentType.JSON)
          .body(updateJson)
          .when().put("/api/v1/users/{id}", createdUserId)
          .then()
             .statusCode(200)
             .contentType(ContentType.JSON)
             .body("email", is("updated@example.com"))
             .body("firstName", is("Updated"));
    }

    @Test
    @Order(4)
    void testDeleteUser() {
        given()
          .when().delete("/api/v1/users/{id}", createdUserId)
          .then()
             .statusCode(204);
    }
}

8. Configuration

application.properties

properties

# RESTEasy Reactive Configuration
quarkus.resteasy-reactive.path=/api
quarkus.resteasy-reactive.json.default-charset=UTF-8

# Jackson Configuration
quarkus.jackson.fail-on-unknown-properties=false
quarkus.jackson.write-dates-as-timestamps=false
quarkus.jackson.time-zone=UTC

# Validation
quarkus.hibernate-validator.fail-fast=true

# CORS (if needed)
quarkus.http.cors=true
quarkus.http.cors.origins=http://localhost:3000
quarkus.http.cors.methods=GET,POST,PUT,DELETE,OPTIONS

# Logging
quarkus.log.category."com.example".level=DEBUG

# Database (for full application)
quarkus.datasource.db-kind=h2
quarkus.datasource.username=quarkus
quarkus.datasource.password=quarkus
quarkus.datasource.jdbc.url=jdbc:h2:mem:quarkusdb
quarkus.hibernate-orm.database.generation=drop-and-create

This comprehensive guide covers building RESTful APIs with JAX-RS in Quarkus using RESTEasy Reactive, including CRUD operations, validation, exception handling, reactive programming, testing, and advanced features like file upload and JSON customization.

 

Comments (0)

No comments yet. Be the first to comment!

LecturreAI Assistant

Hello! I'm LecturreAI

What would you like to learn today?