docs
reference
tessl install tessl/maven-io-quarkus--quarkus-rest@3.15.0A Jakarta REST implementation utilizing build time processing and Vert.x for high-performance REST endpoints with reactive programming support, security integration, and cloud-native features.
This guide shows how to build reactive, non-blocking REST endpoints with Mutiny.
Reactive programming allows handling many concurrent requests efficiently by:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-reactive-panache</artifactId>
</dependency>Return Uni<T> for operations that produce one value asynchronously:
import io.smallrye.mutiny.Uni;
@Path("/users")
public class UserResource {
@Inject
UserRepository repository;
@GET
@Path("/{id}")
public Uni<User> get(@PathParam("id") Long id) {
return repository.findById(id);
}
@POST
public Uni<Response> create(User user) {
return repository.persist(user)
.onItem().transform(created ->
Response.created(URI.create("/users/" + created.id))
.entity(created)
.build()
);
}
}Return Multi<T> for streaming multiple values:
import io.smallrye.mutiny.Multi;
@GET
@Path("/stream")
@Produces(MediaType.APPLICATION_JSON)
public Multi<User> streamUsers() {
return repository.streamAll();
}Transform items in the reactive pipeline:
@GET
@Path("/{id}/dto")
public Uni<UserDTO> getDTO(@PathParam("id") Long id) {
return repository.findById(id)
.onItem().transform(user -> new UserDTO(user));
}@GET
@Path("/{id}")
public Uni<User> get(@PathParam("id") Long id) {
return repository.findById(id)
.onFailure().recoverWithItem(User.getDefaultUser());
}@GET
@Path("/{id}")
public Uni<User> get(@PathParam("id") Long id) {
return repository.findById(id)
.onFailure().retry().atMost(3);
}@GET
@Path("/{id}")
public Uni<Response> get(@PathParam("id") Long id) {
return repository.findById(id)
.onItem().transform(user -> Response.ok(user).build())
.onFailure().recoverWithItem(error ->
Response.status(500)
.entity("Error: " + error.getMessage())
.build()
);
}@POST
@Path("/{id}/activate")
public Uni<User> activate(@PathParam("id") Long id) {
return repository.findById(id)
.onItem().transformToUni(user -> {
user.active = true;
return repository.persist(user);
});
}@GET
@Path("/{id}/full")
public Uni<UserFullDetails> getFullDetails(@PathParam("id") Long id) {
Uni<User> userUni = userRepository.findById(id);
Uni<List<Post>> postsUni = postRepository.findByUserId(id);
Uni<List<Comment>> commentsUni = commentRepository.findByUserId(id);
return Uni.combine().all()
.unis(userUni, postsUni, commentsUni)
.asTuple()
.onItem().transform(tuple ->
new UserFullDetails(
tuple.getItem1(),
tuple.getItem2(),
tuple.getItem3()
)
);
}Stream events to clients:
@GET
@Path("/events")
@Produces(MediaType.SERVER_SENT_EVENTS)
public Multi<String> streamEvents() {
return Multi.createFrom().ticks().every(Duration.ofSeconds(1))
.onItem().transform(tick -> "Event " + tick);
}
@GET
@Path("/updates")
@Produces(MediaType.SERVER_SENT_EVENTS)
public Multi<User> streamUserUpdates() {
return userUpdatePublisher.getUpdates();
}Add timeouts to prevent hanging:
@GET
@Path("/{id}")
public Uni<User> get(@PathParam("id") Long id) {
return repository.findById(id)
.ifNoItem().after(Duration.ofSeconds(5))
.fail()
.onFailure().recoverWithItem(User.getDefaultUser());
}Control flow when consumer is slower than producer:
@GET
@Path("/stream")
@Produces(MediaType.APPLICATION_JSON)
public Multi<User> streamUsers() {
return repository.streamAll()
.onBackPressure().drop() // Drop items if consumer is slow
.select().first(100); // Limit total items
}Use Panache transactions with reactive:
import io.quarkus.hibernate.reactive.panache.Panache;
@POST
public Uni<Response> create(User user) {
return Panache.withTransaction(() ->
repository.persist(user)
).onItem().transform(created ->
Response.created(URI.create("/users/" + created.id))
.entity(created)
.build()
);
}Uni/Multi methods run on I/O threads (event loop)@GET
@Path("/{id}")
@Blocking // Force worker thread
public Uni<User> get(@PathParam("id") Long id) {
return Uni.createFrom().item(() -> expensiveBlockingOperation(id));
}@GET
@Path("/cached")
@NonBlocking // Force I/O thread
public String getCached() {
return cache.get("key");
}Call other APIs reactively:
@Path("/api")
@RegisterRestClient
public interface ExternalApi {
@GET
@Path("/data")
Uni<Data> getData();
}
@Path("/proxy")
public class ProxyResource {
@RestClient
ExternalApi externalApi;
@GET
@Path("/data")
public Uni<Data> proxyData() {
return externalApi.getData();
}
}Convert to blocking for tests:
@Test
public void testGetUser() {
Uni<User> userUni = resource.get(1L);
User user = userUni.await().indefinitely();
assertEquals("Alice", user.name);
}Note: Never use .await() in production code!
public Uni<UserDTO> getUserDTO(Long id) {
return repository.findById(id)
.onItem().transform(UserDTO::new);
}public Uni<User> updateUser(Long id, UserUpdate update) {
return repository.findById(id)
.onItem().ifNull().failWith(NotFoundException::new)
.onItem().transform(user -> {
user.name = update.name;
user.email = update.email;
return user;
})
.onItem().transformToUni(repository::persist);
}public Uni<Response> activate(Long id) {
return repository.findById(id)
.onItem().transformToUni(user -> {
if (user.active) {
return Uni.createFrom().item(
Response.status(409).entity("Already active").build()
);
}
user.active = true;
return repository.persist(user)
.onItem().transform(u -> Response.ok(u).build());
});
}