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.
Handling corner cases, errors, and exceptional scenarios.
@GET
public Uni<List<Item>> list(
@RestQuery Optional<String> category,
@RestQuery Optional<Integer> minPrice
) {
// Handle missing parameters
String cat = category.orElse("all");
int price = minPrice.orElse(0);
return itemService.findFiltered(cat, price);
}@GET
public Uni<RestResponse<List<Item>>> list() {
return itemService.findAll()
.onItem().transform(items -> {
if (items.isEmpty()) {
return RestResponse.status(200, Collections.emptyList());
}
return RestResponse.ok(items);
});
}@GET
@Path("/{id}")
public Uni<RestResponse<Item>> get(@RestPath Long id) {
return itemService.findById(id)
.onItem().ifNull().continueWith(() ->
RestResponse.<Item>notFound()
)
.onItem().ifNotNull().transform(item ->
RestResponse.ok(item)
);
}@GET
@Path("/{id}")
public Uni<RestResponse<Item>> get(@RestPath Long id) {
return itemService.findById(id)
.ifNoItem().after(Duration.ofSeconds(5))
.failWith(new TimeoutException("Request timeout"))
.onItem().transform(item -> RestResponse.ok(item))
.onFailure(TimeoutException.class)
.recoverWithItem(RestResponse.status(504, "Gateway Timeout"));
}@GET
@Path("/{id}/full")
public Uni<FullData> getFull(@RestPath Long id) {
Uni<Item> itemUni = itemService.findById(id)
.ifNoItem().after(Duration.ofSeconds(2)).fail();
Uni<Reviews> reviewsUni = reviewService.findByItemId(id)
.ifNoItem().after(Duration.ofSeconds(2)).fail();
return Uni.combine().all().unis(itemUni, reviewsUni)
.asTuple()
.ifNoItem().after(Duration.ofSeconds(5)).fail()
.onItem().transform(tuple ->
new FullData(tuple.getItem1(), tuple.getItem2())
)
.onFailure(TimeoutException.class)
.recoverWithItem(new FullData(null, null));
}public class CreateItemRequest {
@NotBlank(message = "Name is required")
@Size(min = 3, max = 100, message = "Name must be 3-100 characters")
public String name;
@NotNull(message = "Price is required")
@Min(value = 0, message = "Price must be positive")
public BigDecimal price;
}
@ServerExceptionMapper
public RestResponse<ValidationErrorResponse> handleValidation(
ConstraintViolationException ex
) {
List<FieldError> errors = ex.getConstraintViolations().stream()
.map(violation -> new FieldError(
violation.getPropertyPath().toString(),
violation.getMessage(),
violation.getInvalidValue()
))
.toList();
return RestResponse.status(400,
new ValidationErrorResponse("Validation failed", errors)
);
}@POST
public Uni<RestResponse<Item>> create(CreateItemRequest request) {
// Custom validation
if (request.price.compareTo(BigDecimal.ZERO) < 0) {
return Uni.createFrom().item(
RestResponse.status(400, "Price cannot be negative")
);
}
if (request.name.contains("invalid")) {
return Uni.createFrom().item(
RestResponse.status(400, "Invalid name")
);
}
return itemService.create(request)
.onItem().transform(item -> RestResponse.status(201, item));
}@PUT
@Path("/{id}")
public Uni<RestResponse<Item>> update(
@RestPath Long id,
@RestHeader("If-Match") Optional<String> ifMatch,
UpdateItemRequest request
) {
return itemService.findById(id)
.onItem().ifNull().failWith(new NotFoundException())
.chain(existing -> {
// Check version
if (ifMatch.isPresent() &&
!existing.getVersion().equals(ifMatch.get())) {
return Uni.createFrom().item(
RestResponse.<Item>status(412, "Item was modified")
);
}
return itemService.update(id, request)
.onItem().transform(updated -> RestResponse.ok(updated));
});
}@POST
@Path("/upload")
@Consumes(MediaType.MULTIPART_FORM_DATA)
@Blocking
public Response upload(@RestForm FileUpload file) {
// Check file size
if (file.size() > 10 * 1024 * 1024) {
return Response.status(413)
.entity(Map.of(
"error", "File too large",
"maxSize", "10MB",
"actualSize", file.size()
))
.build();
}
// Check content type
List<String> allowedTypes = List.of("image/jpeg", "image/png", "image/gif");
if (!allowedTypes.contains(file.contentType())) {
return Response.status(415)
.entity(Map.of(
"error", "Unsupported media type",
"allowed", allowedTypes,
"received", file.contentType()
))
.build();
}
try {
byte[] content = Files.readAllBytes(file.uploadedFile());
String fileId = fileService.save(file.fileName(), content);
return Response.status(201)
.entity(Map.of("fileId", fileId))
.build();
} catch (IOException e) {
return Response.status(500)
.entity(Map.of("error", "Failed to process file"))
.build();
}
}@ServerRequestFilter
public Optional<Response> rateLimit(
@Context SecurityContext ctx,
UriInfo uriInfo
) {
String userId = ctx.getUserPrincipal().getName();
String endpoint = uriInfo.getPath();
if (!rateLimiter.tryAcquire(userId, endpoint)) {
return Optional.of(
Response.status(429)
.header("Retry-After", "60")
.entity(Map.of(
"error", "Rate limit exceeded",
"retryAfter", 60
))
.build()
);
}
return Optional.empty();
}@GET
@Path("/{id}")
public Uni<Item> get(@RestPath Long id) {
return externalClient.getItem(id)
.onFailure(IOException.class).retry()
.withBackOff(Duration.ofSeconds(1), Duration.ofSeconds(10))
.atMost(3)
.onFailure().recoverWithItem(() -> {
logger.error("Failed to fetch item after retries: {}", id);
return null;
});
}@GET
@Path("/external")
public Uni<RestResponse<Data>> getExternal() {
return externalClient.getData()
.onFailure().transform(ex -> {
if (circuitBreaker.isOpen()) {
return new ServiceUnavailableException("Circuit breaker open");
}
return ex;
})
.onItem().invoke(() -> circuitBreaker.recordSuccess())
.onFailure().invoke(() -> circuitBreaker.recordFailure())
.onItem().transform(data -> RestResponse.ok(data))
.onFailure(ServiceUnavailableException.class)
.recoverWithItem(RestResponse.status(503, "Service unavailable"));
}@GET
public Uni<List<Item>> list() {
return itemService.findAll()
.onFailure(PersistenceException.class).retry()
.withBackOff(Duration.ofMillis(100))
.atMost(3)
.onFailure().recoverWithItem(Collections.emptyList());
}@POST
@Transactional
public Uni<RestResponse<Order>> createOrder(CreateOrderRequest request) {
return orderService.create(request)
.chain(order -> inventoryService.reserve(order.getItems())
.onItem().transform(() -> order)
)
.onItem().transform(order -> RestResponse.status(201, order))
.onFailure().invoke(ex -> {
logger.error("Order creation failed, transaction will rollback", ex);
})
.onFailure().recoverWithItem(
RestResponse.status(500, "Failed to create order")
);
}@ServerExceptionMapper
public RestResponse<ErrorResponse> handleExpiredToken(
ExpiredJwtException ex
) {
return RestResponse.status(401, new ErrorResponse(
"Token expired",
"AUTH_TOKEN_EXPIRED"
));
}
@ServerExceptionMapper
public RestResponse<ErrorResponse> handleInvalidToken(
InvalidJwtException ex
) {
return RestResponse.status(401, new ErrorResponse(
"Invalid token",
"AUTH_TOKEN_INVALID"
));
}@ServerExceptionMapper
public RestResponse<ErrorResponse> handleJsonError(
JsonProcessingException ex
) {
return RestResponse.status(400, new ErrorResponse(
"Invalid JSON: " + ex.getMessage(),
"JSON_PARSE_ERROR"
));
}@ServerExceptionMapper
public RestResponse<ErrorResponse> handleDateError(
DateTimeParseException ex
) {
return RestResponse.status(400, new ErrorResponse(
"Invalid date format. Expected: yyyy-MM-dd",
"INVALID_DATE_FORMAT"
));
}@GET
@Path("/download/{id}")
public Response download(@RestPath String id, @Context Closer closer) {
try {
InputStream stream = fileService.openStream(id);
closer.add(stream); // Ensure cleanup
return Response.ok(stream)
.type("application/octet-stream")
.build();
} catch (IOException e) {
return Response.status(500)
.entity("Failed to open file")
.build();
}
}