JavaScript Services
RESTHeartA service
A service binds a function to a uri. The module exports it as its default export, declared with defineService:
import { defineService } from "restheart:sdk";
export default defineService({
name: "helloWorldService",
description: "just another Hello World",
uri: "/hello",
secured: false,
matchPolicy: "EXACT",
contentKind: "json",
handle(req, res) {
res.json({ msg: `Hello ${req.query.name ?? "World"}` });
},
});
The module file goes in rh:services of package.json, see Packaging.
| property | description | mandatory | default |
|---|---|---|---|
|
the name of the service; also the name of its section in |
yes |
|
|
what it does, for the log and the catalog |
yes |
|
|
the uri it is bound to, starting with |
yes |
|
|
|
no |
|
|
|
no |
|
|
how the request and response bodies are typed, see below |
no |
|
|
the function that answers the request |
yes |
contentKind
contentKind says what req.content and res.content are. It picks the Java service the plugin runs as (StringService, JsonService, BsonService, ByteArrayService), and in TypeScript it is what req and res are inferred from.
| kind | req.content |
use it for |
|---|---|---|
|
the body as a string, |
text, a raw body to verify a signature on, anything you parse yourself |
|
the body as native data: objects, arrays, strings, numbers, booleans, |
a JSON API |
|
like |
documents going to or coming from MongoDB |
|
the body as bytes |
binary content |
The mongo and csv kinds exist for interceptors only.
The request
Data is exposed as data, behaviour as methods. A plugin reads req.query.limit, not a getQueryParameter call, and every decision about what a value is was made once, in the runtime, for every plugin: a missing query parameter is undefined, a repeated header is its first value, headers are case-insensitive.
| member | value |
|---|---|
|
strings; |
|
the first value of the query parameter, a string, |
|
every value of the parameter, an array, for the parameters that repeat ( |
|
the first value of the header, case-insensitive, |
|
|
|
the body, typed by |
|
the |
|
the method predicates |
|
whether the request is authenticated |
|
whether the request is handled by the service of that name; what an interceptor’s |
|
the parameters of the path against a template, |
|
the value of a cookie, |
|
a parameter attached to the request by another plugin, and attaching one for the plugins that run after |
|
the MongoDB part of a request handled by the MongoDB service, for |
|
the Java |
The query parameters, the headers and the content are read lazily: handing a large body to a plugin costs nothing until the plugin looks into it. req.content.user.name reads one path, it does not copy the document. The values support everything a native object does: in, Object.keys(), the spread operator, destructuring, JSON.stringify().
The response
| member | value |
|---|---|
|
sets the content, the content type and, when none is set yet, status 200, in one call. The single most repeated snippet of a plugin |
|
the same, as |
|
the same, as |
|
read and set: |
|
sets a header, case-insensitive; |
|
the content, typed by |
|
read and set |
|
the error body RESTHeart answers with everywhere, |
|
the Java |
res.json() converts what it is given according to the content kind: on a json service a native object becomes a JSON document, on a bson service it becomes BSON, with { $oid } and { $date } read back into their types and a native Date written as a BSON date. A document read with the mongo capability goes through untouched.
Errors
Throw a typed error and the request is answered with its status and message:
import { defineService, BadRequestError, NotFoundError } from "restheart:sdk";
export default defineService({
name: "users", description: "one user", uri: "/users", contentKind: "json", secured: true,
handle(req, res) {
const id = req.pathParams("/users/{id}").id;
if (!id) throw new BadRequestError("the user id is required");
const user = find(id);
if (!user) throw new NotFoundError(`no user ${id}`);
res.json(user);
},
});
| error | status |
|---|---|
|
400 |
|
401 |
|
403 |
|
404 |
|
409 |
|
any status |
A throw cannot be forgotten the way a res.setInError(400, msg); return; can. res.setInError() still works, for a plugin that prefers it.
Any other exception, a TypeError, a failed http call, a bug, becomes a 500 Internal Server Error. The exception is logged with the plugin name and the JavaScript stack trace, and nothing of it reaches the client.
A complete example
A webhook receiver: Stripe, GitHub and Twilio sign their payloads with HMAC, which needs crypto, and the signature covers the raw body, which needs contentKind: "string".
import { defineService, crypto, config, logger, UnauthorizedError } from "restheart:sdk";
export default defineService({
name: "webhookService",
description: "verifies the HMAC signature of a webhook payload",
uri: "/webhook",
matchPolicy: "EXACT",
contentKind: "string",
handle(req, res) {
if (!req.isPost()) {
res.statusCode = 405;
return;
}
const signature = req.headers["x-signature"] ?? "";
const expected = "sha256=" + crypto.hmacSha256(config.secret as string, req.content ?? "");
if (!crypto.timingSafeEqual(signature, expected)) {
throw new UnauthorizedError("bad signature");
}
const event = JSON.parse(req.content ?? "{}");
logger.info("webhook event {}", event.type);
res.json({ received: true });
},
});
With, in restheart.yml:
webhookService:
secret: the-signing-secret-of-the-provider