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
- RESTHeart JavaScript plugins — full runtime API, TypeScript support, advanced examples
- JavaScript plugins reference — complete guide in the Facet repository
- Developer's Guide — how plugins integrate with Facet's template system
- Template System — template resolution algorithm