JavaScript Plugins — Custom Backend Logic Without Java

Write server-side services and interceptors in JavaScript, with hot-reload

Facet runs on RESTHeart, which supports server-side plugins written in JavaScript using GraalVM. You can add custom endpoints, data aggregations, and request/response hooks without writing Java and without a compilation step. Edit a .mjs file, send a request, see the result.

For the full runtime API reference, see the RESTHeart JavaScript plugins documentation.

Why JavaScript Plugins?

Aspect JavaScript plugin Java plugin
Language JavaScript / TypeScript (ESM) Java
Compilation None mvn package + restart
Hot-reload Yes (next request) No
MongoDB access mclient global (GraalVM interop) MongoClient injection
Deployment Mount a directory Copy JAR

Two extension points are available: Services (new HTTP endpoints) and Interceptors (hooks into existing request/response lifecycle).

Writing a Service

A service is an ES module that exports an options object and a handle function.

// hello.mjs
export const options = {
    name: "helloService",
    description: "A minimal example",
    uri: "/api/hello",
    secured: true,
    matchPolicy: "EXACT"
};

export function handle(request, response) {
    const result = { message: "Hello from JavaScript!" };
    response.setContent(JSON.stringify(result));
    response.setContentTypeAsJson();
}

Querying MongoDB

RESTHeart injects a mclient global (a MongoClient instance). Use Java.type() to import BSON classes.

const BsonDocument = Java.type("org.bson.BsonDocument");

export function handle(request, response) {
    const db   = mclient.getDatabase("mydb");
    const coll = db.getCollection("mycollection", BsonDocument.class);

    let results = [];
    const it = coll.find().limit(10).iterator();
    while (it.hasNext()) {
        results.push(JSON.parse(it.next().toJson()));
    }

    response.setContent(JSON.stringify(results));
    response.setContentTypeAsJson();
}

Common BSON accessor methods:

Type Method
String doc.getString("key").getValue()
Number doc.getNumber("key").doubleValue()
Integer doc.getNumber("key").intValue()
Boolean doc.getBoolean("key").getValue()
Key exists doc.containsKey("key")

Writing an Interceptor

An interceptor hooks into the request/response lifecycle for existing endpoints. It exports a resolve predicate that controls which requests it applies to.

// add-header.mjs
export const options = {
    name: "addHeader",
    description: "Adds a custom response header",
    interceptPoint: "RESPONSE"
};

export function resolve(request) {
    return request.getPath().startsWith("/api/");
}

export function handle(request, response) {
    response.getHeaders().add("X-Powered-By", "Facet");
}

Available intercept points: REQUEST_BEFORE_AUTH, REQUEST_AFTER_AUTH, RESPONSE, RESPONSE_ASYNC.

Plugin Manifest

Each plugin directory needs a package.json that declares its services and interceptors:

{
  "name": "my-plugin",
  "version": "1.0.0",
  "rh:services": ["hello.mjs"],
  "rh:interceptors": ["add-header.mjs"]
}

RESTHeart scans plugin directories at startup and loads any .mjs files listed in rh:services or rh:interceptors.

Folder Layout and Deployment

plugins/
└── my-plugin/
    ├── package.json
    ├── hello.mjs
    └── add-header.mjs

Mount the plugin directory in Docker Compose:

services:
  facet:
    image: softinstigate/facet:latest
    volumes:
      - ./plugins/my-plugin:/opt/restheart/plugins/my-plugin:ro

Edit any .mjs file while the stack is running. The next HTTP request picks up the change automatically. No restart, no build.

Pairing with Facet Templates

A JavaScript service returns JSON. When a browser sends Accept: text/html, Facet intercepts the response and renders an HTML template. The template path follows the same convention as MongoDB endpoints: a service at /shop/stats resolves to templates/shop/stats/index.html.

Every top-level JSON key becomes a template variable. If your service returns:

{ "total": 42, "avgPrice": 309.49 }

The template receives {{ total }} and {{ avgPrice }} directly:

{% extends "layout.html" %}
{% block content %}
<h1>Statistics</h1>
<p>Total products: {{ document.total }}</p>
<p>Average price: ${{ document.avgPrice }}</p>
{% endblock %}

The same endpoint serves both formats:

# HTML (browser)
curl -H "Accept: text/html" http://localhost:8080/shop/stats

# JSON (API client)
curl http://localhost:8080/shop/stats

Working Example

The product-catalog example includes a complete JavaScript plugin at plugins/product-stats/:

File Purpose
product-stats.mjs Aggregates product stats from MongoDB
package.json Plugin manifest
templates/shop/stats/index.html HTML dashboard template

Run it:

cd examples/product-catalog
docker compose up

Then open http://localhost:8080/shop/stats (login: admin / secret).

Further Reading