Live
Black Hat USAAI BusinessBlack Hat AsiaAI BusinessCrowdStrike, Cisco and Palo Alto Networks all shipped agentic SOC tools at RSAC 2026 — and all three missed the same gapVentureBeat AICrypto Startup Uses Polymarket to Bet on Its Own Fundraise, Blindsiding BackersDecrypt AICommunity Input Sought on AI Education Program - Signals AZGNews AI educationGetting Started with Claude Code: A Guide to Slash Commands and TipsDEV CommunityChatGPT Hits Apple CarPlay and the In-Dash AI Race Just Got Real - Startup FortuneGNews AI AppleIf You Hold Solana on Magic Eden's Wallet, It's Time to Move It or Lose ItDecrypt AIYou can now use ChatGPT with Apple’s CarPlayThe VergeYou can now use ChatGPT with Apple’s CarPlay - The VergeGNews AI ApplePacifiCan invests $13.8 million to advance defence innovation in AI and aerospace in British Columbia - canada.caGNews AI CanadaA Beginner’s Guide to Open Source Contributions (From My Journey and Mistakes)DEV CommunityI found the 3 best last minute tech deals under $100 during Amazon's Spring SaleZDNet AI‘Go get your own oil’: Trump’s message to allies who haven’t backed war in IranFortune TechBlack Hat USAAI BusinessBlack Hat AsiaAI BusinessCrowdStrike, Cisco and Palo Alto Networks all shipped agentic SOC tools at RSAC 2026 — and all three missed the same gapVentureBeat AICrypto Startup Uses Polymarket to Bet on Its Own Fundraise, Blindsiding BackersDecrypt AICommunity Input Sought on AI Education Program - Signals AZGNews AI educationGetting Started with Claude Code: A Guide to Slash Commands and TipsDEV CommunityChatGPT Hits Apple CarPlay and the In-Dash AI Race Just Got Real - Startup FortuneGNews AI AppleIf You Hold Solana on Magic Eden's Wallet, It's Time to Move It or Lose ItDecrypt AIYou can now use ChatGPT with Apple’s CarPlayThe VergeYou can now use ChatGPT with Apple’s CarPlay - The VergeGNews AI ApplePacifiCan invests $13.8 million to advance defence innovation in AI and aerospace in British Columbia - canada.caGNews AI CanadaA Beginner’s Guide to Open Source Contributions (From My Journey and Mistakes)DEV CommunityI found the 3 best last minute tech deals under $100 during Amazon's Spring SaleZDNet AI‘Go get your own oil’: Trump’s message to allies who haven’t backed war in IranFortune Tech

Building a Full-Stack Java App with Quarkus — No React, No Angular, No Problem

DEV Communityby Vinicius SengerMarch 31, 20269 min read0 views
Source Quiz

<p>You don't need React. You don't need Angular. You don't need Vue, Svelte, or the JavaScript framework that launched last Tuesday.</p> <p>I built <strong>re:Money</strong> — a full-stack financial tracking application with a dashboard, pivot tables, CSV import, inline editing, modals, and filtering — using nothing but <strong>Quarkus</strong>, its built-in <strong>Qute</strong> template engine, plain <strong>CSS</strong>, and a sprinkle of <strong>vanilla JavaScript</strong>. The backend talks to <strong>DynamoDB</strong> and the whole thing runs as a single JAR.</p> <p>Let me show you how.</p> <h2> Why Skip the Front-End Framework? </h2> <p>For many internal tools, personal projects, and CRUD apps, a full SPA (single page application) framework adds:</p> <ul> <li>A separate build pipeli

You don't need React. You don't need Angular. You don't need Vue, Svelte, or the JavaScript framework that launched last Tuesday.

I built re:Money — a full-stack financial tracking application with a dashboard, pivot tables, CSV import, inline editing, modals, and filtering — using nothing but Quarkus, its built-in Qute template engine, plain CSS, and a sprinkle of vanilla JavaScript. The backend talks to DynamoDB and the whole thing runs as a single JAR.

Let me show you how.

Why Skip the Front-End Framework?

For many internal tools, personal projects, and CRUD apps, a full SPA (single page application) framework adds:

  • A separate build pipeline (Node, npm, Webpack/Vite)

  • A JSON API contract you have to maintain

  • Client-side state management

  • Hundreds of megabytes of node_modules

With Quarkus + Qute, you get server-side rendering with type-safe templates, hot reload in dev mode, and a single mvn build. Your HTML is your UI. Your Java objects flow directly into your pages. Done.

The Stack

Layer Technology

Runtime Java 21

Framework Quarkus 3.x

Templating Qute (built into Quarkus)

Styling Plain CSS

Interactivity Vanilla JavaScript

Database Amazon DynamoDB

Build Maven

One language. One build tool. One deployable artifact.

Project Structure

src/main/java/org/reos/money/ ├── model/ # Domain POJOs ├── resource/ # REST + UI endpoints (JAX-RS) └── service/ # Business logic + DynamoDB access

src/main/resources/ ├── templates/ # Qute HTML templates ├── META-INF/resources/css/ # Static CSS └── application.properties`

Enter fullscreen mode

Exit fullscreen mode

The key insight: UI endpoints and API endpoints live side by side in the same Quarkus app. No separate front-end project.

Step 1: Add the Dependencies

You only need one extra dependency beyond the standard Quarkus REST setup:

`

io.quarkus quarkus-rest-qute `

Enter fullscreen mode

Exit fullscreen mode

That's it. Qute ships with Quarkus — quarkus-rest-qute just wires it into your JAX-RS resources so you can return TemplateInstance from your endpoints.

Step 2: Define Your Model

A plain Java class. No JPA annotations, no ORM magic — just fields and a factory method to map from DynamoDB's AttributeValue maps:

@RegisterForReflection public class Entry {  String id;  public Long timestamp;  public String accountID;  public String description;  public String category;  public BigDecimal amount;  public BigDecimal balance;  public String date;

public Entry() { this.id = UUID.randomUUID().toString(); }

public static Entry from(Map item) { Entry entry = new Entry(); entry.id = item.get("id").s(); entry.setAccountID(item.get("accountID").s()); entry.setDescription(item.get("description").s()); entry.setAmount(new BigDecimal(item.get("amount").n())); entry.setBalance(new BigDecimal(item.get("balance").n())); entry.setDate(item.get("date").s()); entry.setTimestamp(Long.parseLong(item.get("timestamp").n())); entry.setCategory(item.get("category").s()); return entry; }

// getters and setters... }`

Enter fullscreen mode

Exit fullscreen mode

@RegisterForReflection is there for Quarkus native compilation support. The from() static factory keeps DynamoDB mapping logic close to the model.

Step 3: Build the Service Layer

The service layer handles all DynamoDB operations. A base class encapsulates the repetitive request-building:

public class AbstractService {

public static final String ENTRY_ID_COL = "id"; public static final String ENTRY_ACCOUNTID_COL = "accountID"; // ... other column constants

@ConfigProperty(name = "dynamodb.table.account.data") String tableName;

protected ScanRequest scanRequest() { return ScanRequest.builder() .tableName(tableName) .attributesToGet(ENTRY_ID_COL, ENTRY_ACCOUNTID_COL, /* ... */) .build(); }

protected PutItemRequest putRequest(Entry entry) { Map item = new HashMap<>(); item.put(ENTRY_ID_COL, AttributeValue.builder().s(entry.getId()).build()); item.put(ENTRY_AMOUNT_COL, AttributeValue.builder().n(entry.getAmount().toString()).build()); // ... map all fields return PutItemRequest.builder() .tableName(tableName).item(item).build(); }

protected DeleteItemRequest deleteRequest(String id) { return DeleteItemRequest.builder() .tableName(tableName) .key(Map.of(ENTRY_ID_COL, AttributeValue.builder().s(id).build())) .build(); } }`

Enter fullscreen mode

Exit fullscreen mode

Then the concrete service adds business logic:

@ApplicationScoped public class EntryService extends AbstractService {

@Inject DynamoDbClient dynamoDB;

public List findAll() { return dynamoDB.scan(scanRequest()).items().stream() .map(Entry::from) .sorted(Comparator.comparing(Entry::getTimestamp)) .collect(Collectors.toList()); }

public Entry addEntry(Entry entry) { dynamoDB.putItem(putRequest(entry)); return entry; }

public List findByFilters(String accountID, String category, Long startDate, Long endDate, String sortOrder, String excludeCategories, String description) { List entries = findAll();

if (accountID != null) entries = entries.stream() .filter(e -> accountID.equals(e.getAccountID())) .collect(Collectors.toList());

if (category != null) entries = entries.stream() .filter(e -> category.equals(e.getCategory())) .collect(Collectors.toList());

// ... date range, exclude, description filters

if ("desc".equalsIgnoreCase(sortOrder)) entries.sort(Comparator.comparing(Entry::getTimestamp).reversed()); else entries.sort(Comparator.comparing(Entry::getTimestamp));

return entries; }

// replaceCategory, deleteAccount, recalculateBalances, etc. }`

Enter fullscreen mode

Exit fullscreen mode

No Spring Data, no Hibernate — just the AWS SDK DynamoDB client injected by Quarkus.

Step 4: The UI Resource — Where the Magic Happens

This is the core pattern. Instead of returning JSON, your endpoint returns a TemplateInstance:

@Path("/ui") public class ExpenseUIResource {

@Inject Template dashboard; // Matches templates/dashboard.html

@Inject EntryService entryService;

@GET @Produces(MediaType.TEXT_HTML) public TemplateInstance getDashboard( @QueryParam("account") String account, @QueryParam("category") String category, @QueryParam("startDate") String startDate, @QueryParam("endDate") String endDate, @QueryParam("sortOrder") String sortOrder) throws Exception {

// Parse dates, apply filters... List entries = entryService.findByFilters( account, category, start, end, order, null, null);

return dashboard .data("entries", entries) .data("accounts", entryService.listAccounts()) .data("categories", entryService.listCategories()) .data("selectedAccount", account) .data("selectedCategory", category) .data("sortOrder", order); } }`

Enter fullscreen mode

Exit fullscreen mode

When you @Inject Template dashboard, Quarkus automatically looks for src/main/resources/templates/dashboard.html. You pass data with .data("key", value) and Qute renders it server-side. The browser gets plain HTML.

Handling Form Submissions

Forms use standard HTML posts — no fetch(), no JSON serialization:

@POST @Path("/entry") @Consumes(MediaType.APPLICATION_FORM_URLENCODED) public Response addEntry(  @FormParam("accountID") String accountID,  @FormParam("description") String description,  @FormParam("category") String category,  @FormParam("amount") BigDecimal amount,  @FormParam("date") String date) throws Exception {

Entry entry = new Entry(); entry.setAccountID(accountID); entry.setDescription(description); entry.setCategory(category); entry.setAmount(amount); entry.setDate(date);

entryService.addEntry(entry);

// Post-Redirect-Get pattern return Response.seeOther(URI.create("/ui")).build(); }`

Enter fullscreen mode

Exit fullscreen mode

The Post-Redirect-Get pattern prevents duplicate submissions on refresh. The browser submits a form, the server processes it, then redirects back to the dashboard. Classic web development — and it works perfectly.

Step 5: Qute Templates — Your View Layer

Qute templates look like HTML with simple expressions. Here's a simplified version of the dashboard:

`

re:Money Dashboard

All Accounts {#for account in accounts}

{account}

{/for}

All Categories {#for category in categories}

{category}

{/for}

{#for entry in entries}

{/for}

Date Description Account Category Amount

{entry.date} {entry.description} {entry.accountID} {entry.category}

${entry.amount}

`

Enter fullscreen mode

Exit fullscreen mode

Key Qute features used here:

  • {#for ... in ...} — loops over collections

  • {#if ...} — conditional rendering

  • {expression ?: 'default'} — elvis operator for defaults

  • {entry.amount} — direct property access on Java objects

The onchange="this.form.submit()" on the selects gives you instant filtering without any JavaScript framework. The browser submits the form as a GET request, the server re-renders the page with filtered data. Simple.

Step 6: Adding Interactivity with Vanilla JS

You don't need React for modals, inline editing, or dynamic filters. Here's how re:Money handles it:

Modal for Adding/Editing Entries

`

Add New Entry

Add Entry

function openModal() { document.getElementById('entryModal').classList.add('active'); }

function editEntry(id, accountID, description, category, amount, date) { document.getElementById('modalTitle').textContent = 'Edit Entry'; document.getElementById('entryForm').action = '/ui/entry/' + id + '/update'; document.getElementById('descriptionUI').value = description; document.getElementById('amountUI').value = amount; document.getElementById('dateUI').value = date; document.getElementById('entryModal').classList.add('active'); } `

Enter fullscreen mode

Exit fullscreen mode

The modal is pure CSS (.modal.active { display: flex; }). The form posts to the server. No state management library needed.

Inline Category Editing

`

{entry.category}

{#for cat in categories}

{cat}

{/for}

✏️ `

Enter fullscreen mode

Exit fullscreen mode

Click the pencil, the span hides, the select shows. Pick a new category, a small fetch() call updates it server-side. This is the only place we use fetch() — for a better UX on a single field update. Everything else is plain form submissions.

Step 7: DynamoDB Setup

Create the table:

aws dynamodb create-table \  --table-name entry \  --attribute-definitions AttributeName=id,AttributeType=S \  --key-schema AttributeName=id,KeyType=HASH \  --billing-mode PAY_PER_REQUEST \  --region us-east-1

Enter fullscreen mode

Exit fullscreen mode

For local development, add --endpoint-url http://localhost:8000 and configure Quarkus:

%dev.quarkus.dynamodb.endpoint-override=http://localhost:8000 dynamodb.table.account.data=entry

Enter fullscreen mode

Exit fullscreen mode

The %dev. prefix means this config only applies in dev mode. In production, Quarkus uses your default AWS credentials.

Step 8: Run It

./mvnw compile quarkus:dev

Enter fullscreen mode

Exit fullscreen mode

Open http://localhost:8080/ui and you have a full working app. Quarkus dev mode gives you:

  • Live reload — edit a Java file or a template, save, refresh the browser

  • Dev UI at /q/dev/ — inspect beans, config, endpoints

  • Zero restart — changes apply instantly

The Result

With this approach, re:Money has:

  • 📊 Dashboard with multi-criteria filtering and sorting

  • ➕ Add/Edit/Delete entries via modal forms

  • 🗂️ Category management — rename, delete, inline edit

  • 🏦 Account management — rename, delete, recalculate balances

  • 📈 Pivot tables — cross-account category analysis

  • 📁 CSV import — bulk data loading

  • 💬 Chat interface — natural language queries (via Bedrock)

All in a single Quarkus application. No npm install. No package.json. No webpack config. No CORS issues. No separate deployment for the front-end.

When Should You Use This Approach?

✅ Good fit:

  • Internal tools and admin panels

  • Personal projects and prototypes

  • CRUD-heavy applications

  • Small team projects where everyone knows Java

  • Apps where SEO matters (server-rendered HTML)

❌ Consider a SPA when:

  • You need rich real-time interactivity (collaborative editing, drag-and-drop)

  • Your front-end team is specialized in JavaScript/TypeScript

  • You're building a complex single-page experience with heavy client-side state

Key Takeaways

  • Qute is underrated. It's type-safe, fast, and integrates seamlessly with Quarkus CDI. Just @Inject Template myPage and you're rendering HTML.

  • HTML forms still work. The Post-Redirect-Get pattern handles 90% of CRUD interactions without any JavaScript.

  • Vanilla JS is enough. Modals, toggles, inline edits — you don't need 200KB of framework for this.

  • One deployable artifact. Your UI, API, and business logic ship as a single JAR. Deploy it anywhere Java runs.

  • DynamoDB + Quarkus is a clean combo. The Quarkiverse extension handles client configuration. You write putItem / scan / deleteItem and move on.

The web platform is more capable than we give it credit for. Sometimes the best front-end framework is no framework at all.

Get re:Money

Want to try it yourself or use it as a starting point for your own project?

  • 📖 re:Money — Personal Financial Management Re-imagined with AI — full project overview on AWS Community

  • 💻 Source code on GitHub

Clone it, run ./mvnw compile quarkus:dev, and you're up in seconds.

Was this article helpful?

Sign in to highlight and annotate this article

AI
Ask AI about this article
Powered by AI News Hub · full article context loaded
Ready

Conversation starters

Ask anything about this article…

Daily AI Digest

Get the top 5 AI stories delivered to your inbox every morning.

Knowledge Map

Knowledge Map
TopicsEntitiesSource
Building a …modellaunchversionupdateproductapplicationDEV Communi…

Connected Articles — Knowledge Graph

This article is connected to other articles through shared AI topics and tags.

Knowledge Graph100 articles · 93 connections
Scroll to zoom · drag to pan · click to open

Discussion

Sign in to join the discussion

No comments yet — be the first to share your thoughts!

More in Products