Build a Full-Stack Todo App with Quarkus, Panache, and Qute
From Zero to Web App with a Reactive Backend, Templated Frontend, and Zero Config PostgreSQL
Every developer has written a Todo app at some point. But with Quarkus, building one becomes more than just an exercise in CRUD, it’s a showcase of modern Java development done right. In this hands-on tutorial, you’ll create a complete full-stack web application with a RESTful backend, a dynamic HTML frontend, and a PostgreSQL database: All powered by Quarkus.
This guide assumes basic Java and Maven knowledge but is beginner-friendly overall. You’ll walk away with a working app and a better understanding of how to build reactive, productive web applications using Quarkus and its ecosystem.
Why Quarkus?
Quarkus is a Kubernetes-native Java framework designed for speed, simplicity, and modern development. With features like live coding, zero-config Dev Services, and support for imperative or reactive styles, Quarkus brings joy back to backend development.
In this app, we’ll use:
quarkus-rest-jackson
for building REST endpointsquarkus-qute
for server-side templatingquarkus-hibernate-orm-panache
for simplified ORM over JPAquarkus-jdbc-postgresql
for database access via PostgreSQLQuarkus Dev Services to automatically spin up a PostgreSQL container during development
Project Setup
Let’s start by generating a Quarkus project with the required extensions. If you haven’t already, install the Quarkus CLI first.
quarkus create app com.example:quarkus-todo-app \
--extension=quarkus-rest-jackson,quarkus-qute,quarkus-hibernate-orm-panache,quarkus-jdbc-postgresql
cd quarkus-todo-app
This will scaffold your Maven project and include support for REST APIs, JPA with Panache, and PostgreSQL. You’re ready to start coding. If you don’t want to follow all steps, you can also check out the complete app from my Github repository.
Build the Backend
Define the Todo
Entity
Let’s model our main entity: a Todo
item with a title and completion flag. Rename and change the MyEntity.java that Quarkus scaffolded for you to: src/main/java/com/example/Todo.java
package com.example;
import io.quarkus.hibernate.orm.panache.PanacheEntity;
import jakarta.persistence.Entity;
@Entity
public class Todo extends PanacheEntity {
public String title;
public boolean completed;
}
The class extends PanacheEntity
, which gives us out-of-the-box access to methods like listAll()
, persist()
, and findById()
. The database schema will be generated from this entity.
Create the REST API
Now build a REST resource to expose the CRUD operations for our Todo
items. Rename and change the GreetingResource.java that the Quarkus CLI scaffolded for you to: src/main/java/com/example/TodoResource.java
package com.example;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.transaction.Transactional;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.util.List;
package com.example;
import java.util.List;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.transaction.Transactional;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.NotFoundException;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
@Path("/todos")
@ApplicationScoped
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class TodoResource {
@GET
public List<Todo> getAll() {
return Todo.listAll();
}
@POST
@Transactional
public Response create(Todo todo) {
todo.persist();
return Response.status(Response.Status.CREATED).entity(todo).build();
}
@PUT
@Path("/{id}")
@Transactional
public Todo update(@PathParam("id") Long id, Todo todo) {
Todo entity = Todo.findById(id);
if (entity == null) {
throw new NotFoundException();
}
entity.title = todo.title;
entity.completed = todo.completed;
return entity;
}
@DELETE
@Path("/{id}")
@Transactional
public Response delete(@PathParam("id") Long id) {
Todo.deleteById(id);
return Response.noContent().build();
}
}
You now have a functional REST API with endpoints to get, create, update, and delete todos.
Let Dev Services Handle the Database
You don’t need to install or configure PostgreSQL manually. Quarkus Dev Services will start a temporary containerized PostgreSQL instance when you run the app in dev mode.
Tell Quarkus to generate the database schema and log SQL for debugging:
src/main/resources/application.properties
quarkus.hibernate-orm.database.generation=drop-and-create
quarkus.hibernate-orm.log.sql=true
quarkus.datasource.db-kind=postgresql
That’s all. No need for database URLs, credentials, or Docker Compose.
Create the Frontend with Qute
Build the Template
Now let’s build a basic HTML page using Qute that lists our todos and includes a form to add new ones.
src/main/resources/templates/todos.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Quarkus Todo App</title>
</head>
<body>
<h1>My Todo List</h1>
<ul>
{#for todo in todos}
<li>
{todo.title} - {todo.completed ? 'Done' : 'Pending'}
</li>
{/for}
</ul>
<h2>Add New Todo</h2>
<form method="POST" action="/page/todos">
<input type="text" name="title" required>
<button type="submit">Add</button>
</form>
</body>
</html>
Qute syntax is concise and intuitive for anyone with basic templating experience. Learn more in the Qute guide.
Serve the Template from a Resource
To render the HTML, we’ll add a new resource class that fetches todos from the database and passes them to the template.
src/main/java/com/example/PageResource.java
package com.example;
import java.util.List;
import io.quarkus.qute.Template;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.FormParam;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
@Path("/page/todos")
@ApplicationScoped
public class PageResource {
@Inject
Template todos;
@GET
@Produces(MediaType.TEXT_HTML)
public String get() {
List<Todo> todoList = Todo.listAll();
return todos.data("todos", todoList).render();
}
@POST
@Transactional
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
@Produces(MediaType.TEXT_HTML)
public String add(@FormParam("title") String title) {
Todo newTodo = new Todo();
newTodo.title = title;
newTodo.completed = false;
newTodo.persist();
return get(); // Re-render updated page
}
}
You’ve now connected the database to a dynamic HTML template without needing a single line of JavaScript.
Run the App
Start the dev mode server:
quarkus dev
Then open your browser and navigate to:
http://localhost:8080/page/todos
Add a few todos. Refresh the page. Everything is live. You can even make changes to your Java classes or templates while the server is running and Quarkus will reload changes on the fly.
What Next?
Congratulations! You’ve built a full-stack Java app with Quarkus! This is a great foundation for exploring more advanced features.
Here are a few ideas for extending this project:
Add validation with
jakarta.validation.constraints.*
Add due dates and sorting
Use JavaScript to make the list reactive without page reloads
Add REST tests using RestAssured
Package the app as a native executable using
quarkus build --native
Deploy it to Kubernetes using Quarkus Kubernetes extension
Want to dig deeper into Qute, Panache, or REST endpoints in Quarkus? The official Quarkus Guides are packed with examples and best practices.
Final Thoughts
This tutorial showed you how to build a complete web application with minimal configuration and productive tooling. If you’ve been looking for a modern alternative to traditional Spring Boot + Thymeleaf setups, give Quarkus and Qute a serious try.
You’ll write less code, move faster, and enjoy the experience.
Reactive?