docs
reference
tessl install tessl/maven-io-quarkus--quarkus-resteasy-reactive@3.15.0A Jakarta REST implementation utilizing build time processing and Vert.x for high-performance REST endpoints with reactive capabilities in cloud-native environments.
Real-world examples of typical Quarkus REST use cases.
Complete CRUD implementation with validation and error handling:
@Path("/api/products")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class ProductResource {
@Inject
ProductService productService;
@GET
public Uni<List<Product>> list(
@RestQuery Optional<String> category,
@RestQuery @DefaultValue("0") int page,
@RestQuery @DefaultValue("20") int size
) {
return productService.findAll(category, page, size);
}
@GET
@Path("/{id}")
public Uni<RestResponse<Product>> get(@RestPath Long id) {
return productService.findById(id)
.onItem().transform(product ->
product != null
? RestResponse.ok(product)
: RestResponse.notFound()
);
}
@POST
public Uni<RestResponse<Product>> create(@Valid CreateProductRequest request) {
return productService.create(request)
.onItem().transform(product ->
RestResponse.status(201, product)
);
}
@PUT
@Path("/{id}")
public Uni<RestResponse<Product>> update(
@RestPath Long id,
@Valid UpdateProductRequest request
) {
return productService.update(id, request)
.onItem().transform(product ->
product != null
? RestResponse.ok(product)
: RestResponse.notFound()
);
}
@DELETE
@Path("/{id}")
public Uni<RestResponse<Void>> delete(@RestPath Long id) {
return productService.delete(id)
.onItem().transform(deleted ->
deleted
? RestResponse.noContent()
: RestResponse.notFound()
);
}
@ServerExceptionMapper
public RestResponse<ErrorResponse> handleValidation(ValidationException ex) {
return RestResponse.status(400, new ErrorResponse(ex.getMessage()));
}
}Handle file uploads with validation:
@Path("/api/files")
public class FileResource {
@Inject
FileService fileService;
@POST
@Path("/upload")
@Consumes(MediaType.MULTIPART_FORM_DATA)
@Blocking
public Response upload(@RestForm FileUpload file) throws IOException {
// Validate file size
if (file.size() > 10 * 1024 * 1024) {
return Response.status(413)
.entity("File too large (max 10MB)")
.build();
}
// Validate content type
if (!file.contentType().startsWith("image/")) {
return Response.status(400)
.entity("Only image files allowed")
.build();
}
// Save file
byte[] content = Files.readAllBytes(file.uploadedFile());
String fileId = fileService.save(file.fileName(), content);
return Response.status(201)
.entity(Map.of("fileId", fileId))
.build();
}
@GET
@Path("/download/{id}")
@Produces(MediaType.APPLICATION_OCTET_STREAM)
public Response download(@RestPath String id) {
FileData file = fileService.getFile(id);
if (file == null) {
return Response.status(404).build();
}
return Response.ok(file.getContent())
.header("Content-Disposition",
"attachment; filename=\"" + file.getName() + "\"")
.header("Content-Type", file.getContentType())
.build();
}
}Implement paginated API with filtering:
public class PagedResult<T> {
public List<T> items;
public int page;
public int pageSize;
public long totalItems;
public int totalPages;
}
@Path("/api/orders")
public class OrderResource {
@Inject
OrderService orderService;
@GET
public Uni<PagedResult<Order>> list(
@RestQuery Optional<String> status,
@RestQuery Optional<Long> customerId,
@RestQuery Optional<LocalDate> fromDate,
@RestQuery Optional<LocalDate> toDate,
@RestQuery @DefaultValue("0") int page,
@RestQuery @DefaultValue("20") int size,
@RestQuery @DefaultValue("createdAt") String sortBy,
@RestQuery @DefaultValue("desc") String sortOrder
) {
OrderFilter filter = new OrderFilter(
status, customerId, fromDate, toDate
);
return orderService.findPaginated(filter, page, size, sortBy, sortOrder);
}
}Real-time notifications with SSE:
@Path("/api/notifications")
public class NotificationResource {
@Inject
NotificationService notificationService;
@GET
@Path("/stream")
@Produces(MediaType.SERVER_SENT_EVENTS)
@RestStreamElementType(MediaType.APPLICATION_JSON)
@Authenticated
public Multi<Notification> stream(@Context SecurityContext ctx) {
String userId = ctx.getUserPrincipal().getName();
return notificationService.streamForUser(userId);
}
@POST
@Path("/send")
@RolesAllowed("admin")
public Uni<Response> send(SendNotificationRequest request) {
return notificationService.sendToUser(
request.userId,
request.message
).onItem().transform(sent -> Response.ok().build());
}
}Call external APIs:
@Path("/api/weather")
public class WeatherResource {
@Inject
@RestClient
WeatherApiClient weatherClient;
@GET
@Path("/{city}")
public Uni<WeatherResponse> getWeather(@RestPath String city) {
return weatherClient.getWeather(city)
.onFailure().retry().atMost(3)
.onFailure().recoverWithItem(
ex -> new WeatherResponse("unavailable")
);
}
}
@RegisterRestClient(configKey = "weather-api")
public interface WeatherApiClient {
@GET
@Path("/weather")
@ClientQueryParam(name = "appid", value = "${weather.api.key}")
Uni<WeatherResponse> getWeather(@QueryParam("q") String city);
}Implement HTTP caching:
@Path("/api/articles")
public class ArticleResource {
@Inject
ArticleService articleService;
@GET
@Path("/{id}")
@Cache(maxAge = 3600, mustRevalidate = true)
public Response get(@RestPath Long id, @Context Request request) {
Article article = articleService.findById(id);
if (article == null) {
return Response.status(404).build();
}
// ETag support
EntityTag etag = new EntityTag(article.getVersion());
Response.ResponseBuilder builder = request.evaluatePreconditions(etag);
if (builder != null) {
// Client has current version
return builder.build();
}
return Response.ok(article)
.tag(etag)
.lastModified(article.getUpdatedAt())
.build();
}
}Handle batch requests:
@Path("/api/products")
public class ProductResource {
@Inject
ProductService productService;
@POST
@Path("/batch")
public Uni<BatchResult> createBatch(List<CreateProductRequest> requests) {
List<Uni<Product>> unis = requests.stream()
.map(req -> productService.create(req)
.onFailure().recoverWithItem((Product) null))
.toList();
return Uni.combine().all().unis(unis)
.combinedWith(products -> {
List<Product> created = products.stream()
.filter(Objects::nonNull)
.toList();
int failed = products.size() - created.size();
return new BatchResult(created, failed);
});
}
}
public class BatchResult {
public List<Product> created;
public int failed;
public BatchResult(List<Product> created, int failed) {
this.created = created;
this.failed = failed;
}
}Receive and process webhooks:
@Path("/api/webhooks")
public class WebhookResource {
@Inject
WebhookService webhookService;
@POST
@Path("/github")
public Uni<Response> handleGitHub(
@RestHeader("X-GitHub-Event") String event,
@RestHeader("X-Hub-Signature-256") String signature,
String payload
) {
// Verify signature
if (!webhookService.verifySignature(payload, signature)) {
return Uni.createFrom().item(Response.status(401).build());
}
// Process event
return webhookService.processGitHubEvent(event, payload)
.onItem().transform(processed -> Response.ok().build())
.onFailure().recoverWithItem(
ex -> Response.status(500).build()
);
}
}Implement rate limiting with filters:
@ApplicationScoped
public class RateLimitFilter {
private final Map<String, RateLimiter> limiters = new ConcurrentHashMap<>();
@ServerRequestFilter
public Optional<Response> rateLimit(
UriInfo uriInfo,
@Context SecurityContext ctx
) {
String userId = ctx.getUserPrincipal().getName();
RateLimiter limiter = limiters.computeIfAbsent(
userId,
k -> RateLimiter.create(100.0) // 100 requests per second
);
if (!limiter.tryAcquire()) {
return Optional.of(
Response.status(429)
.entity("Rate limit exceeded")
.build()
);
}
return Optional.empty();
}
}Custom health checks:
@ApplicationScoped
public class ApiHealthCheck implements HealthCheck {
@Inject
@RestClient
ExternalApiClient apiClient;
@Override
public HealthCheckResponse call() {
try {
apiClient.ping().await().atMost(Duration.ofSeconds(5));
return HealthCheckResponse.up("external-api");
} catch (Exception e) {
return HealthCheckResponse.down("external-api");
}
}
}