DEV Community

Markus
Markus

Posted on • Originally published at the-main-thread.com on

Build a Full-Stack Todo App with Quarkus, Panache, and Qute

Hero image

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 endpoints

  • quarkus-qute for server-side templating

  • quarkus-hibernate-orm-panache for simplified ORM over JPA

  • quarkus-jdbc-postgresql for database access via PostgreSQL

  • Quarkus 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
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Then open your browser and navigate to:

http://localhost:8080/page/todos
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)