Getting Started with Facet

Add server-rendered HTML to your REST API. No new mental model required.

You have a working REST API. Now you need web pages.

The standard options all require adding something significant to your stack: a React frontend with its own build pipeline, or a template engine like Thymeleaf that requires a controller for every new page.

Facet takes a different approach. You add a template file. The endpoint starts serving HTML. Your API keeps working exactly as before.

Quick Start

The fastest way to see Facet in action is to run the complete product catalog example.

1. Clone and Run

# Clone the repository
git clone https://github.com/SoftInstigate/facet.git
cd facet

# Start the product catalog example (published image)
cd examples/product-catalog
docker compose up

# Optional: build a local image (for plugin changes)
# mvn package -DskipTests
# docker compose up --build

Wait for services to start (few seconds), then open http://localhost:8080/shop/products

You'll see a styled product catalog with search, filtering, and pagination. All server-rendered from MongoDB data.

Default login credentials:

  • Admin: admin / secret (full access)
  • Viewer: viewer / viewer (read-only)

Docker Hub image: https://hub.docker.com/r/softinstigate/facet

2. Explore the Dual Interface

The same endpoint serves both HTML and JSON:

# Browser request → HTML
curl -u admin:secret -H "Accept: text/html" http://localhost:8080/shop/products

# API request → JSON
curl -u admin:secret -H "Accept: application/json" http://localhost:8080/shop/products

Key concept: Templates are opt-in. No template = JSON API unchanged.

3. Hot Reload Templates

Templates reload automatically when file-loader is enabled and cache is disabled:

  1. Edit examples/product-catalog/templates/shop/products/list.html
  2. Refresh your browser
  3. See changes instantly, no restart needed!

Understanding the Example

Directory Structure

examples/product-catalog/
├── docker-compose.yml     # MongoDB + Facet services
├── restheart.yml          # Facet configuration
├── init-data.js           # Sample product data
├── templates/
│   ├── layout.html        # Base layout
│   ├── shop/
│   │   └── products/
│   │       ├── list.html  # Product collection view
│   │       └── view.html  # Product detail view
│   └── _fragments/
│       └── product-list.html  # HTMX fragment
└── static/
    └── favicon.ico        # Static assets

Template Resolution

When you visit /shop/products (collection), Facet looks for templates in this order:

1. templates/shop/products/list.html   ✓ (found!)
2. templates/shop/products/index.html  (optional fallback)
3. templates/shop/list.html            (parent fallback)
4. templates/shop/index.html           (parent fallback)
5. templates/list.html                 (global collection template)
6. templates/index.html                (global fallback)
7. No template → return JSON

Template naming convention:

  • list.html - Collection views (recommended - clean, no conditional logic)
  • view.html - Document views (recommended - clean, no conditional logic)
  • index.html - Optional fallback (when list/view share template logic)

Template Example

Here's the actual product list template (simplified):

{% extends "layout" %}

{% block main %}
<h1>Products</h1>

<!-- Search form -->
<form method="GET" hx-get="{{ path }}" hx-target="#product-list">
  <input type="text" name="search" placeholder="Search products...">
  <button type="submit">Search</button>
</form>

<!-- Product list (replaced by HTMX on search) -->
<div id="product-list">
  {% include "_fragments/product-list" %}
</div>
{% endblock %}

Fragment Template

HTMX requests target fragments for partial updates:

{# templates/_fragments/product-list.html #}
{% if items is not empty %}
  {% for item in items %}
  <article>
    <h3>{{ item.data.name }}</h3>
    <p>{{ item.data.description }}</p>
    <span>${{ item.data.price }}</span>
  </article>
  {% endfor %}

  <!-- Pagination -->
  {% if totalPages > 1 %}
  <nav>
    {% for p in range(1, totalPages + 1) %}
    <a href="?page={{ p }}" 
       hx-get="{{ path }}?page={{ p }}"
       hx-target="#product-list">{{ p }}</a>
    {% endfor %}
  </nav>
  {% endif %}
{% else %}
  <p>No products found.</p>
{% endif %}

Key Concepts

1. Path-Based Templates

Template location mirrors your API structure with explicit action templates:

URL: /shop/products (collection)
  → Template: templates/shop/products/list.html

URL: /shop/products/{id} (document)
  → Template: templates/shop/products/view.html

Why explicit templates? Cleaner code without conditional logic checking request type. File names clearly indicate purpose.

2. Template Context Variables

Facet provides rich context to templates:

Variable Description Example
items Array of MongoDB documents {% for item in items %}
page Current page number {{ page }} of {{ totalPages }}
pagesize Items per page Default: 100
path Full request path /shop/products
filter MongoDB query filter {"category":"Electronics"}
username Authenticated user null if not logged in
roles User's roles ['admin', 'editor']

See all variables →

3. HTMX Integration

Facet automatically detects HTMX requests and returns fragments:

<!-- Full page on direct visit -->
<a href="/shop/products">Products</a>

<!-- Partial update with HTMX -->
<a href="/shop/products"
   hx-get="/shop/products"
   hx-target="#main-content">Products</a>

How it works:

  1. HTMX sends HX-Request: true header
  2. Facet detects HTMX request
  3. Returns fragment template instead of full page
  4. No JavaScript needed!

4. MongoDB Query Parameters

RESTHeart provides powerful query parameters that work automatically:

# Filter by category
http://localhost:8080/shop/products?filter={"category":"Electronics"}

# Sort by price
http://localhost:8080/shop/products?sort={"price":1}

# Pagination
http://localhost:8080/shop/products?page=2&pagesize=10

# Combine them
http://localhost:8080/shop/products?filter={"price":{"$lt":100}}&sort={"price":1}&page=1

All query parameters are available as template variables.

Creating Your Own Application

1. Start from the Example

# Copy the product catalog example
cd examples
cp -r product-catalog my-app
cd my-app

2. Customize Your Data

Edit init-data.js:

db = db.getSiblingDB('mydb');
db.createCollection('myitems');

db.myitems.insertMany([
  { name: "Item 1", value: 100 },
  { name: "Item 2", value: 200 }
]);

3. Update Templates

Rename template directories to match your structure:

mv templates/shop templates/mydb
mv templates/mydb/products templates/mydb/myitems

Edit templates/mydb/myitems/list.html to display your collection view, and optionally create view.html for document detail views.

4. Update Configuration

In restheart.yml, change the name:

/core:
  name: my-app

5. Start Your App

docker compose up

Use docker compose up --build if you are building a local image.

Visit http://localhost:8080/mydb/myitems

Next Steps