Build Full-Stack Java Web Apps with Quarkus Renarde
From Struts nostalgia to modern productivity. Discover how Renarde makes end-to-end web development in Quarkus fast, type-safe, and enjoyable.
I grew up with Apache Struts. Back then, Java web development meant action classes, JSPs, and endless XML configuration. Struts shaped how a whole generation of developers built web applications. Since then, web frameworks have been a constant companion in my professional career. They have changed names and paradigms. Spring MVC, JSF, Play Framework, Vaadin, and more. But the core promise remained: give developers a productive way to build end-to-end applications without reinventing the wheel each time.
Today, Quarkus has become the go-to runtime for cloud-native Java. But if you want to build a classic full-stack web app, you often end up stitching together REST endpoints, a frontend framework, and a templating engine. It works, but it splits your development across multiple languages, toolchains, and mental models.
That’s where Quarkus Renarde comes in. Renarde is a modern take on the integrated Java web framework. It combines routing, controllers, templating, form handling, and authentication. Think of it as a Play Framework reborn for the cloud-native era, with hot reload, Panache ORM, Dev Services, and everything else Quarkus brings to the table.
Why does this matter? Because not every enterprise app needs to be a microservice or a single-page application. Many use cases still benefit from a server-side rendered, fully Java-driven stack:
Internal tools with simple CRUD interfaces
Administrative portals with authentication
Prototypes where speed of development is more important than frontend sophistication
Renarde lets you build these apps faster, safer, and in a single language.
In this tutorial, we’ll put Renarde to work by creating a simple Task Manager. The app will:
Show a list of tasks stored in a database
Let users add new tasks via a form
Allow marking tasks as complete
Display feedback using flash messages
Along the way, you’ll learn how Renarde handles routing, integrates with Qute templates, and gives you type-safe navigation and form handling. The goal isn’t just to build another to-do app. It’s to show that full-stack Java web development is still alive, and with Renarde and Quarkus, it’s more productive than ever.
Prerequisites
Java 17+ (LTS, recommended)
Apache Maven 3.9+
Podman (or Docker) for database Dev Services
Verify your Quarkus CLI:
quarkus --version
If missing, install from quarkus.io/get-started.
Project Bootstrap
Create a new Quarkus app with Renarde and Hibernate Panache:
quarkus create app com.example:renarde-tasks:1.0.0 \
-x renarde,hibernate-orm-panache,hibernate-orm-rest-data-panache,\ qute,quarkus-jdbc-postgresql,quarkus-rest-jackson \
--no-code
cd renarde-tasks
Here's what each Quarkus extension does in that command:
renarde: A web framework for Quarkus that provides MVC controllers, templates, and web utilities for building traditional web applications with server-side rendering.
hibernate-orm-panache: Provides Hibernate ORM with Panache, which simplifies database operations by offering active record and repository patterns with automatic CRUD operations.
hibernate-orm-rest-data-panache: Automatically generates REST endpoints for Panache entities, creating CRUD APIs without writing controller code.
qute: A templating engine for Quarkus that provides type-safe templates with server-side rendering capabilities for HTML, JSON, and other formats.
quarkus-jdbc-postgresql: Adds PostgreSQL JDBC driver support, enabling database connectivity to PostgreSQL databases.
quarkus-rest-jackson: Integrates Jackson JSON processing library with RESTEasy Reactive, providing automatic JSON serialization/deserialization for REST endpoints.
You can grab the running example from my Github repository.
Domain Model
Create a simple Task
entity:
package com.example.model;
import io.quarkus.hibernate.orm.panache.PanacheEntity;
import jakarta.persistence.Entity;
@Entity
public class Task extends PanacheEntity {
public String description;
public boolean done;
public static Task add(String description) {
Task task = new Task();
task.description = description;
task.done = false;
task.persist();
return task;
}
public static List<Task> findAllOrderByDoneDate() {
return find("ORDER BY doneDate ASC NULLS FIRST").list();
}
}
We extend PanacheEntity
for simplicity (auto-id, persist helpers).
Controller with Renarde
Create a Renarde controller under src/main/java/com/example/web/TaskController.java
:
package com.example.web;
import java.util.Date;
import java.util.List;
import org.jboss.resteasy.reactive.RestForm;
import org.jboss.resteasy.reactive.RestPath;
import com.example.model.Task;
import io.quarkiverse.renarde.Controller;
import io.quarkus.logging.Log;
import io.quarkus.qute.CheckedTemplate;
import io.quarkus.qute.TemplateInstance;
import jakarta.transaction.Transactional;
import jakarta.validation.constraints.NotBlank;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
public class TaskController extends Controller {
@CheckedTemplate
static class Templates {
public static native TemplateInstance index(List<Task> tasks);
}
public TemplateInstance index() {
List<Task> tasks = Task.findAllOrderByDoneDate();
return Templates.index(tasks);
}
@POST
@Transactional
public void add(@NotBlank @RestForm String task) {
if (validationFailed()) {
flash("error", "Description required");
index();
}
Task newtask = new Task();
newtask.description = task;
newtask.persist();
flash("message", "Task added");
index();
}
@POST
public void delete(@RestPath Long id) {
// find the Task
Task task = Task.findById(id);
notFoundIfNull(task);
// delete it
task.delete();
// show message
flash("message", "Task deleted");
// redirect to index page
index();
}
@GET
@Transactional
public void toggle(@RestPath Long id) {
Log.infof("Toggling task with id: %s", id);
// find the Task
Task task = Task.findById(id);
notFoundIfNull(task);
// switch the done state
task.done = !task.done;
if (task.done)
task.doneDate = new Date();
// send loving message
flash("message", "Task updated");
// redirect to index page
index();
}
}
Notes:
@CheckedTemplate
ensures compile-time validation of Qute templates.index()
generates type-safe routes.Transactions are handled with
@Transactional
.
Qute Templates
Create src/main/resources/templates/TaskController/index.html
:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Renarde Task Manager</title>
<link rel="stylesheet" href="/_renarde/css/renarde.css">
</head>
<body>
<h1>Task Manager</h1>
{#if flash:error}
<div class="flash-message flash-error">{flash:error}</div>
{/if}
{#if flash:message}
<div class="flash-message flash-success">{flash:message}</div>
{/if}
<div class="add-form">
{#form uri:TaskController.add method='POST'}
<input type="text" name="task" placeholder="What needs to be done?" />
<button type="submit">Add Task</button>
{/form}
</div>
<ul class="task-list">
{#for task in tasks}
<li class="task-item {#if task.done}completed{/if}">
<form action="{uri:TaskController.toggle(task.id)}" method="get" style="display:inline">
<button type="submit" class="toggle-button {#if task.done}checked{/if}">
{#if task.done}✓{#else}☐{/if}
</button>
</form>
<span class="task-description">{task.description}</span>
</li>
{#else}
<li class="empty-state">No tasks yet. Add one above to get started!</li>
{/for}
</ul>
</body>
</html>
This template uses:
Renarde’s built-in URI helpers (
{#form:...}
)Flash messages
Simple form handling
Run and Verify
Start the app in dev mode:
./mvnw quarkus:dev
Open http://localhost:8080/TaskController/index.
Try:
Adding a task
Marking it done/undone
Leaving the description empty (flash message shown)
Expected behavior: tasks are persisted in a PostgreSQL Dev Service container and reload instantly with Quarkus hot reload.
Production Notes
Use a proper PostgreSQL database instead of Dev Services
Enable HTTPS in production (
quarkus.http.ssl.*
)Use Flyway or Liquibase for schema migrations
Configure session persistence for clustered deployments
What to do next
Replace PanacheEntity with PanacheRepository for stricter layering
Use Renarde’s
HxController
with HTMX for dynamic updates without page reloadsIntegrate OAuth2 (GitHub, Google) with Quarkus OIDC extension
Deploy to OpenShift or Kubernetes with
./mvnw clean package -Dquarkus.container-image.build=true
Final Thoughts
Quarkus Renarde proves that full-stack Java web development is not dead. It’s faster, safer, and integrated into the Quarkus ecosystem. You can go from idea to working app in minutes, all in one language.
Sometimes, the simplest tools are the most powerful.