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!