A complete example demonstrating Facet's core features through a product catalog application.
- Path-based template resolution - Templates organized by URL structure
- MongoDB data binding - Direct display of MongoDB documents
- Search and filtering - MongoDB query parameters (
filter,sort,keys) - Pagination - Navigate large datasets with
pageandpagesize - HTMX partial updates - Search and paginate without full page reloads
- Authentication - Role-based access control (admin vs viewer)
- CRUD operations - Full create, read, update, delete using HTMX fragments (see below)
- Template inheritance - Shared layout and reusable fragments
- JavaScript plugin - RESTHeart service written in JavaScript (no Java, hot-reload)
The example includes a RESTHeart JavaScript plugin that demonstrates writing server-side logic without Java. The plugin lives in plugins/product-stats/ and is picked up automatically by RESTHeart at startup—no compilation needed.
plugins/product-stats/product-stats.mjs is a RESTHeart service registered at /shop/stats. It uses the MongoDB Java driver (available as the mclient global via GraalVM interop) to iterate shop.products and compute:
- Total / in-stock / out-of-stock product counts
- Average, minimum, and maximum price
- Total inventory value (price × stock across all products)
- Per-category breakdown sorted by product count
- Low-stock alerts (products with ≤ 5 units remaining)
# JSON response — plain API call
curl -u admin:secret http://localhost:8080/shop/stats
# HTML response — Facet renders templates/shop/stats/index.html
curl -u admin:secret -H "Accept: text/html" http://localhost:8080/shop/statsThe HTML dashboard is accessible from the Stats link in the navigation bar.
Edit plugins/product-stats/product-stats.mjs while the stack is running. The next request automatically uses the updated code — no container restart required, exactly like Pebble templates.
| File | Description |
|---|---|
plugins/product-stats/product-stats.mjs |
The JavaScript service |
plugins/product-stats/package.json |
Declares the plugin to RESTHeart |
templates/shop/stats/index.html |
Facet template for the HTML dashboard |
For a full guide on writing and deploying JavaScript plugins, see the Developer's Guide — JavaScript Plugins.
The product catalog demonstrates full CRUD using HTMX fragments:
- Create: Click "+ Add Product" → HTMX loads _fragments/product-new.html fragment → Form submits via POST
- Read: Click product card → Navigates to
/products/{id}with view.html template - Update: Click "Edit Product" → HTMX loads _fragments/product-form.html fragment → Form submits via PATCH
- Delete: Click "Delete Product" → JavaScript
fetch()with DELETE method
All operations use RESTHeart's native MongoDB REST API with standard HTTP methods (POST/GET/PATCH/DELETE) - no custom backend code needed.
- Keeps URLs clean and REST-compliant:
/products/{id}not/products/{id}/edit - No routing conflicts: Document IDs never conflict with action names (e.g., a document with
_id: "edit"works correctly as/products/edit) - Forms load instantly: No page refresh, smooth user experience via HTMX
- URLs remain shareable: Use query parameters like
?mode=editfor deep linking to specific UI states - Component reusability: Same fragment can be loaded from list page or detail page
This architectural approach provides:
- Clean REST URLs - Resources, not actions:
/products/{id}represents the resource - HTTP methods for actions - POST (create), GET (read), PATCH (update), DELETE (delete)
- Fragments for UI components - Forms are reusable components loaded via HTMX
- Progressive enhancement - Works with JavaScript (HTMX), can fallback to traditional POST/redirect
- No backend code - RESTHeart handles all MongoDB operations directly
- Docker and Docker Compose installed
- Java 25+ and Maven (only if building a local image)
- Optional: build the Facet plugin (only if you want a local image):
cd /path/to/facet
mvn package -DskipTests- Start the example (from the product-catalog directory):
cd examples/product-catalog
docker compose up
By default, the docker-compose file builds a local image. To use the published image instead, replace the `build:` section with:
```yaml
image: softinstigate/facet:latest
Then run docker compose up (no --build).
-
Wait for services to start (watch logs for "RESTHeart started" message):
docker-compose logs -f facet
-
Access the application:
- Product Catalog: http://localhost:8080/shop/products
- Facet API: http://localhost:8080/shop/products (with
Accept: application/json) - Ping endpoint: http://localhost:8080/ping
Authentication: See the Authentication Setup section below for details on user credentials and permissions.
The example includes two pre-configured users:
| Username | Password | Role | Permissions |
|---|---|---|---|
admin |
secret |
admin | Full access (CRUD) |
viewer |
viewer |
viewer | Read-only access |
Login URL: http://localhost:8080/login
The example automatically loads 10 sample products into the shop.products collection:
- Electronics (laptop, mouse, keyboard, monitor, etc.)
- Furniture (chair, desk, lamp)
- Appliances (coffee maker)
All products include:
- Name, description, price
- Category and stock level
- Tags for filtering
- Timestamps
templates/
├── layout.html # Base layout with navigation
├── shop/
│ ├── products/
│ │ ├── list.html # Product list page (collection view)
│ │ └── view.html # Product detail page (document view)
│ └── stats/
│ └── index.html # Stats dashboard (rendered from JS plugin output)
└── _fragments/
└── product-list.html # Reusable product list (for HTMX)
- restheart.yml - RESTHeart configuration with Facet settings and ACL permissions
- users.yml - File-based user credentials and role assignments
- init-data.js - MongoDB initialization script with sample products
- static/ - Static assets (favicon, images, CSS, JS)
- plugins/product-stats/ - JavaScript plugin (product statistics service)
- Dockerfile - Docker image with Facet plugin pre-installed
- docker-compose.yml - Multi-container setup (RESTHeart + MongoDB)
When you request different URLs, Facet resolves templates hierarchically:
| Request URL | Template Resolved | Fallback Order |
|---|---|---|
GET /shop/products |
shop/products/list.html |
→ shop/products/index.html → shop/list.html → shop/index.html → list.html → index.html |
GET /shop/products/65abc... |
shop/products/view.html |
→ shop/products/index.html → shop/view.html → ... |
GET /shop/products (HTMX, target: #product-list) |
_fragments/product-list.html |
No fallback (strict mode) |
The search form demonstrates MongoDB query building:
<!-- Form builds filter parameter -->
<form method="GET" hx-get="/shop/products" hx-target="#product-list">
<input id="searchText" placeholder="Search...">
<select id="categorySelect">...</select>
<input type="hidden" name="filter" id="filterInput">
</form>
<script>
// JavaScript builds MongoDB query
let filter = {};
if (searchText) filter.$text = { $search: searchText };
if (category) filter.category = category;
document.getElementById('filterInput').value = JSON.stringify(filter);
</script>Result: ?filter={"category":"Electronics","price":{"$lte":100}}
Pagination links include HTMX attributes for partial updates:
<a href="?page=2&pagesize=10"
hx-get="/shop/products?page=2&pagesize=10"
hx-target="#product-list">
Next
</a>What happens:
- Click triggers HTMX request with
HX-Request: trueheader - Facet detects HTMX and resolves
_fragments/product-list.html - Only the product list div is replaced, keeping search form intact
Templates use roles context variable to show/hide features:
{% if roles and 'admin' in roles %}
<a href="/shop/products/new" class="button is-primary">Add Product</a>
{% endif %}Authentication & Authorization Flow:
- Unauthenticated user → redirected to
/login - Login validates credentials via
fileRealmAuthenticator(users.yml) - On success,
jwtTokenManagerissues JWT token stored inrh_authcookie - Templates receive
usernameandrolesvariables from JWT fileAclAuthorizerenforces role-based permissions (defined in restheart.yml)
See the Authentication Setup section for details.
All templates have access to these variables:
| Variable | Example Value | Description |
|---|---|---|
path |
/shop/products |
Full request path |
database |
shop |
MongoDB database name |
collection |
products |
MongoDB collection name |
items |
[{data: {...}, _id: {...}}, ...] |
Array of documents |
page |
2 |
Current page number |
pagesize |
10 |
Items per page |
totalItems |
50 |
Total document count |
totalPages |
5 |
Total pages |
filter |
{"category":"Electronics"} |
MongoDB filter (JSON string) |
sort |
{"price":1} |
MongoDB sort (JSON string) |
username |
admin |
Current user (null if not authenticated) |
roles |
['admin'] |
User roles array |
Templates are loaded from the filesystem with caching disabled:
/pebble-template-processor:
use-file-loader: true
cache-active: falseEdit a template → Refresh browser → See changes immediately!
# Facet/RESTHeart logs
docker-compose logs -f facet
# MongoDB logs
docker-compose logs -f mongodbThe same endpoints serve JSON for API clients:
# List products (JSON)
curl -u admin:secret http://localhost:8080/shop/products
# Get single product
curl -u admin:secret http://localhost:8080/shop/products/65abc123...
# Search with filter
curl -u admin:secret "http://localhost:8080/shop/products?filter=%7B%22category%22%3A%22Electronics%22%7D"
# Create product (requires authentication)
curl -u admin:secret -X POST http://localhost:8080/shop/products \
-H "Content-Type: application/json" \
-d '{"name":"New Product","price":99.99,"category":"Test","stock":10}'
# Inventory stats (JavaScript plugin — returns computed aggregates)
curl -u admin:secret http://localhost:8080/shop/statsdocker-compose down
# Remove data volume (reset to initial state)
docker-compose down -vCreate new templates matching your URL structure:
templates/
└── shop/
├── products/
│ ├── index.html # /shop/products
│ ├── view.html # /shop/products/{id}
│ └── categories/
│ └── index.html # /shop/products/categories
└── orders/
└── index.html # /shop/orders
Edit init-data.js and restart services:
db.products.insertMany([
{ name: "Your Product", price: 49.99, category: "New", stock: 100 }
]);Or use the API:
curl -u admin:secret -X POST http://localhost:8080/shop/products \
-H "Content-Type: application/json" \
-d '{"name":"Runtime Product","price":19.99,"category":"Dynamic","stock":5}'This example uses file-based authentication for simplicity. RESTHeart separates authentication (who you are) from authorization (what you can do):
User credentials and role assignments are defined in users.yml:
users:
- userid: admin
password: secret
roles:
- admin
- userid: viewer
password: viewer
roles:
- viewerThe fileRealmAuthenticator validates credentials and assigns roles during login. Configured in restheart.yml:
/fileRealmAuthenticator:
enabled: true
conf-file: /opt/restheart/etc/users.yml
/basicAuthMechanism:
enabled: true
authenticator: fileRealmAuthenticatorRole-based permissions (ACL) are defined in restheart.yml:
/fileAclAuthorizer:
enabled: true
permissions:
# Admin can do anything
- role: admin
predicate: path-prefix[path=/]
priority: 0
mongo:
readFilter: null
writeFilter: null
# Viewer can only read
- role: viewer
predicate: path-prefix[path=/]
priority: 1
mongo:
readFilter: null
writeFilter: '{"_id": {"$exists": false}}'The fileAclAuthorizer enforces what authenticated users can do based on their roles.
To add a new user:
-
Edit users.yml:
- userid: editor password: editor123 roles: - editor
-
Add permissions for the role in restheart.yml:
- role: editor predicate: path-prefix[path=/shop/products] priority: 2 mongo: readFilter: null writeFilter: null # Can write to products
-
Restart the container:
docker-compose restart facet
RESTHeart supports multiple authentication methods. To use MongoDB-based users instead of files:
- Enable
mongoRealmAuthenticatorin restheart.yml - Disable
fileRealmAuthenticator - Store users in MongoDB
restheart.userscollection
For production, consider:
- MongoDB-based authentication for dynamic user management
- LDAP/Active Directory for enterprise SSO
- OAuth2/OIDC for social login
See RESTHeart Security Documentation for complete configuration options.
- Add more features: Shopping cart, checkout flow, reviews
- Explore HTMX: Add more interactive components without JavaScript
- Customize styling: Pico CSS provides semantic defaults - add custom CSS in
<style>or create a custom theme - Add validation: Form validation and error handling
- Implement search: Full-text search with MongoDB text indexes
- Add images: Product images with file upload
This example uses Pico CSS - a minimal, semantic CSS framework similar to the approach used by FastHTML. Pico automatically styles semantic HTML elements without requiring class names:
- Write
<nav><ul><li>→ Get a beautiful navigation bar - Write
<article>→ Get styled cards - Write
<button>or<a role="button">→ Get styled buttons - Use
<mark>,<ins>,<del>,<kbd>→ Get semantic highlights
Benefits: Clean HTML, minimal custom CSS, better accessibility, smaller footprint (~10KB vs 200KB for utility frameworks).
To customize: Add your own CSS in the <style> block or override Pico's CSS variables.
- Facet Developer's Guide
- Template Context Reference
- Product Catalog Tutorial
- RESTHeart Documentation
- Pebble Templates
- HTMX Documentation
- Pico CSS Documentation
# Check if ports are in use
lsof -i :8080
lsof -i :27017
# View detailed logs
docker-compose logs- Check template path matches URL structure
- Ensure templates/ directory exists and is properly structured
- Check RESTHeart logs for template resolution attempts