No backend code required.
If you are building a web interface on top of a REST API and want to avoid maintaining a separate JavaScript project, Facet is worth a look. You create template files that mirror your API structure. That is the entire workflow.
-
Zero Backend Code: Ship features without writing controllers or services. Templates plus data equals HTML, end to end.
-
Non-breaking: SSR is opt-in per resource. Endpoints without a template keep returning JSON unchanged. Your existing API is never affected by adding Facet.
-
Hybrid by Default: The same endpoint serves JSON to API clients and HTML to browsers via content negotiation. One codebase, not two.
-
Convention-Based: Drop a template where the path matches the resource and you are done. No routing files, no configuration.
-
Progressive Enhancement: Start with server-side rendering, add HTMX for interactivity where needed. SEO-friendly by default.
Building a web UI on top of a REST API usually means choosing between a JavaScript frontend project or a Java template engine that requires controllers. Facet is a third option.
Facet is a SSR Java Web Framework that transforms REST APIs into web interfaces using simple HTML templates. You already have the API, just add templates where you want HTML.
The core idea: Your API structure is your site structure.
Your API:
├── /shop/products → Returns JSON
└── /shop/products/123 → Returns JSON
Add templates:
└── templates/
└── shop/
└── products/
├── list.html → Renders /shop/products as HTML
└── view.html → Renders /shop/products/123 as HTML
Result:
├── /shop/products → HTML for browsers, JSON for APIs
└── /shop/products/123 → HTML for browsers, JSON for APIs
No routing files, no controllers and no duplicate logic. Just drop templates where your data lives.
Try the working example:
git clone https://github.com/SoftInstigate/facet.git
cd facet
# Option A: use the published image
cd examples/product-catalog
docker compose up
# Option B: build locally (for plugin changes)
# mvn package -DskipTests
# docker compose up --buildNote: example docker-compose files build a local image by default. To use the published image, replace the build: section with image: softinstigate/facet:latest.
Open: http://localhost:8080/shop/products
You'll see a complete product catalog with search, pagination, and authentication—all built with templates.
→ Follow the Tutorial to understand how it works by exploring the code.
{
"name": "Laptop Pro",
"price": 1299,
"category": "Electronics"
}curl http://localhost:8080/shop/products
# Returns JSON array of products{% extends "layout" %}
{% block main %}
<h1>Products</h1>
{% for product in items %}
<article>
<h3>{{ product.data.name }}</h3>
<p>Category: {{ product.data.category }}</p>
<span>${{ product.data.price }}</span>
</article>
{% endfor %}
{% endblock %}Open http://localhost:8080/shop/products in your browser—you get HTML. Call it from your app with Accept: application/json—you get JSON.
Templates automatically match API paths:
GET /shop/products → templates/shop/products/list.html
GET /shop/products/123 → templates/shop/products/view.html
GET /shop/categories → templates/shop/categories/list.html
No routing configuration needed.
Pagination, filters, sorting—all available automatically:
<!-- Pagination works out of the box -->
<nav>
Page {{ page }} of {{ totalPages }}
{% if page < totalPages %}
<a href="?page={{ page + 1 }}">Next</a>
{% endif %}
</nav>
<!-- MongoDB queries accessible -->
{% if filter %}
<p>Showing filtered results</p>
{% endif %}
<!-- Authentication built in -->
{% if roles contains 'admin' %}
<button>Delete</button>
{% endif %}Partial page updates work automatically—no backend code needed:
<!-- Click updates just the product list -->
<a href="?sort=price"
hx-get="?sort=price"
hx-target="#product-list">
Sort by Price
</a>
<div id="product-list">
<!-- Products here -->
</div>Facet detects HTMX requests and renders only what changed.
Edit templates, refresh browser, see changes. No restart required.
Good for:
- Admin dashboards over MongoDB data
- Content-driven websites
- Internal tools and CRUD interfaces
- Adding web UI to existing REST APIs
- Projects where you want HTML without complex frameworks
Not for:
- Heavy client-side state management (use React/Vue)
- Non-MongoDB databases (Facet requires a MongoDB-compatible database)
- Projects without REST API layer
Facet: Drop templates in folder matching API path → Done
Traditional: Write routes + controllers + views + models
Facet: Server-renders from REST API, simpler stack
Next.js: Full-stack React, complex build process, more moving parts
Facet: Built-in HTMX support, convention-based routing
HTMX + Framework: More manual setup, explicit route definitions
Built on proven technologies:
- RESTHeart - Production-grade MongoDB REST API server
- Pebble - Fast template engine (similar to Jinja2/Twig)
- GraalVM - High-performance runtime with optional native compilation
Runtime options:
- Standard JVM: ~1s startup, full plugin support
- Native image: <100ms startup, minimal memory (~50MB)
Deployment: Single JAR or native binary, runs anywhere—Docker, Kubernetes, bare metal.
| Database | Support Level | Notes |
|---|---|---|
| ✅ MongoDB | Full | All versions 3.6+ |
| ✅ MongoDB Atlas | Full | Cloud-native support |
| ✅ Percona Server | Full | Drop-in MongoDB replacement |
| ⚙️ FerretDB | Good | PostgreSQL-backed MongoDB alternative |
| ⚙️ AWS DocumentDB | Good | Most features work, some MongoDB 4.0+ features missing |
| ⚙️ Azure Cosmos DB | Good | With MongoDB API compatibility layer |
Compatibility depends on MongoDB wire protocol implementation.
Learn by example:
- Product Catalog Tutorial - Walk through working code
- Developer's Guide - Complete reference
- Template Variables - What's available in templates
Try it yourself (quickstart):
# Start the quickstart stack (MongoDB + Facet)
docker compose up
# If you want to build a local image instead:
# mvn -pl core -am -DskipTests package
# docker compose up --build
# Visit in browser (login required)
open http://localhost:8080/Note: the root docker-compose.yml builds a local image by default. To use the published image, replace the build: section with image: softinstigate/facet:latest.
Login with admin / secret, then visit /mydb/products to see the seeded data rendered by the default template.
Add a product via curl and refresh the HTML list:
curl -X POST http://localhost:8080/mydb/products \
-u admin:secret \
-H "Content-Type: application/json" \
-d '{"name":"Desk Lamp","price":49,"category":"Home"}'
open http://localhost:8080/mydb/productsRun the quickstart image directly (standalone):
docker run --rm -p 8080:8080 \
-v "$PWD/etc/restheart.yml:/opt/restheart/etc/restheart.yml:ro" \
-v "$PWD/etc/users.yml:/opt/restheart/etc/users.yml:ro" \
-v "$PWD/templates:/opt/restheart/templates:ro" \
-v "$PWD/static:/opt/restheart/static:ro" \
softinstigate/facet:latest -o /opt/restheart/etc/restheart.ymlFacet publishes release tags only to JitPack. Use the raw tag name (no v prefix) as the version.
Maven:
<repositories>
<repository>
<id>jitpack</id>
<url>https://jitpack.io</url>
</repository>
</repositories>
<dependency>
<groupId>com.github.SoftInstigate</groupId>
<artifactId>facet-core</artifactId>
<version>RELEASE_VERSION</version>
</dependency>Gradle (Kotlin DSL):
repositories {
maven("https://jitpack.io")
}
dependencies {
implementation("com.github.SoftInstigate:facet-core:RELEASE_VERSION")
}Release binaries: Download facet-core.jar and dependencies from GitHub Releases.
Docker Hub: A prebuilt image is available at softinstigate/facet (tags match release versions).
Contributions welcome! See open issues or start a discussion.
Apache License 2.0 - Free for commercial use.
Built with ❤️ by SoftInstigate
