Skip to content

Latest commit

 

History

History

README.md

Product Catalog Example

A complete example demonstrating Facet's core features through a product catalog application.

Features Demonstrated

  • 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 page and pagesize
  • 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)

JavaScript Plugin: Product Statistics

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.

What It Does

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)

Endpoints

# 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/stats

The HTML dashboard is accessible from the Stats link in the navigation bar.

Hot Reload

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.

Files

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.

CRUD Operations

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.

Why Fragments?

  • 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=edit for deep linking to specific UI states
  • Component reusability: Same fragment can be loaded from list page or detail page

The Pattern

This architectural approach provides:

  1. Clean REST URLs - Resources, not actions: /products/{id} represents the resource
  2. HTTP methods for actions - POST (create), GET (read), PATCH (update), DELETE (delete)
  3. Fragments for UI components - Forms are reusable components loaded via HTMX
  4. Progressive enhancement - Works with JavaScript (HTMX), can fallback to traditional POST/redirect
  5. No backend code - RESTHeart handles all MongoDB operations directly

Quick Start

Prerequisites

  • Docker and Docker Compose installed
  • Java 25+ and Maven (only if building a local image)

Running the Example

  1. Optional: build the Facet plugin (only if you want a local image):
cd /path/to/facet
mvn package -DskipTests
  1. 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).

  1. Wait for services to start (watch logs for "RESTHeart started" message):

    docker-compose logs -f facet
  2. Access the application:

    Authentication: See the Authentication Setup section below for details on user credentials and permissions.

User Accounts

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

What's Included

Sample Data

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

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)

Configuration Files

Template Resolution Examples

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.htmlshop/list.htmlshop/index.htmllist.htmlindex.html
GET /shop/products/65abc... shop/products/view.html shop/products/index.htmlshop/view.html → ...
GET /shop/products (HTMX, target: #product-list) _fragments/product-list.html No fallback (strict mode)

Key Features Explained

1. Search and Filtering

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}}

2. HTMX Partial Updates

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:

  1. Click triggers HTMX request with HX-Request: true header
  2. Facet detects HTMX and resolves _fragments/product-list.html
  3. Only the product list div is replaced, keeping search form intact

3. Role-Based UI

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:

  1. Unauthenticated user → redirected to /login
  2. Login validates credentials via fileRealmAuthenticator (users.yml)
  3. On success, jwtTokenManager issues JWT token stored in rh_auth cookie
  4. Templates receive username and roles variables from JWT
  5. fileAclAuthorizer enforces role-based permissions (defined in restheart.yml)

See the Authentication Setup section for details.

4. Template Context Variables

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

Development Workflow

Hot Reload Templates

Templates are loaded from the filesystem with caching disabled:

/pebble-template-processor:
  use-file-loader: true
  cache-active: false

Edit a templateRefresh browser → See changes immediately!

Viewing Logs

# Facet/RESTHeart logs
docker-compose logs -f facet

# MongoDB logs
docker-compose logs -f mongodb

Testing the API

The 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/stats

Stopping the Example

docker-compose down

# Remove data volume (reset to initial state)
docker-compose down -v

Extending the Example

Adding New Templates

Create 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

Adding More Data

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}'

Authentication Setup

This example uses file-based authentication for simplicity. RESTHeart separates authentication (who you are) from authorization (what you can do):

Authentication (fileRealmAuthenticator)

User credentials and role assignments are defined in users.yml:

users:
  - userid: admin
    password: secret
    roles:
      - admin
  - userid: viewer
    password: viewer
    roles:
      - viewer

The 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: fileRealmAuthenticator

Authorization (fileAclAuthorizer)

Role-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.

Adding New Users and Roles

To add a new user:

  1. Edit users.yml:

    - userid: editor
      password: editor123
      roles:
        - editor
  2. 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
  3. Restart the container: docker-compose restart facet

Alternative Authenticators

RESTHeart supports multiple authentication methods. To use MongoDB-based users instead of files:

  1. Enable mongoRealmAuthenticator in restheart.yml
  2. Disable fileRealmAuthenticator
  3. Store users in MongoDB restheart.users collection

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.

Next Steps

  • 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

Styling with Pico CSS

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.

Resources

Troubleshooting

Services won't start

# Check if ports are in use
lsof -i :8080
lsof -i :27017

# View detailed logs
docker-compose logs

Template not found

  • Check template path matches URL structure
  • Ensure templates/ directory exists and is properly structured
  • Check RESTHeart logs for template resolution attempts