binden

Binden CI Status version Known Vulnerabilities Coverage Status code style: prettier Contributor Covenant semantic-release Conventional Commits GitHub top language node version npm downloads License

A simple server framework (written in TypeScript).

Installation

npm install binden

Usage

Binden

  • .use() - Add a Middleware/Router to the stack
import { Binden } from "binden";

const app = new Binden().use(middleware1).use(router2);
app.use("/path", middleware2, router2);
app.use(new RegExp("path"), router3, middleware1);
const middleware3 = (context) => context.json({ message: "Hello World!" });
app.use("/path2", middleware3);
  • .off() - remove a Middleware/Router form the stack
import { Binden } from "binden";

const app = new Binden()
.use("/path", middleware1)
.use(middleware2)
.off("/path", middleware1);
  • .createServer() - create a server (HTTP)
import { Binden } from "binden";

const app = new Binden()
.use(new RegExp("path"), middleware)
.use("/path2", router);
const server = app.createServer();
  • .createSecureServer() - create a server (HTTPS)
import { Binden } from "binden";

const app = new Binden().use("/path", middleware).use("/path2", router);
const secureServer = app.createSecureServer({ key, cert });

Context

const { log } = context;
log.info("Hello World", { data: 100 });
  • .setHeader() - Set a response header
const name = "X-HEADER";
const value = ["value1", "value2"];
context.setHeader(name, value);
  • .status() - set the response status
context.status(401);
  • .request - get the original request object (instanceof BindenRequest)
const { request } = context;
  • .response - get the original response object (instanceof BindenResponse)
const { response } = context;
  • .id - get id of the context (generated by the randomUUID() function and logged as trace_id by the context.log)
const { id } = context;
  • .done
class MyMiddleware extends Middleware {
public run(context: Context): void {
context.done = true; // Stop passing the `context` to other middlewares
}
}

or with a function

const MyMiddleware = (context): void => {
context.done = true;
};
  • .url - parsed URL object
const {
log,
url: { search },
} = context;
log.trace("url search string", { search });
  • .send() - execute context.response.send() and set context.done to true
await context.send(data);
// or
await context.response.send(data);
context.done = true;
  • .json() - execute context.response.json() and set context.done to true
const json = { message: "Hello World!" };
await context.json(json);
// or
await context.response.json(json);
context.done = true;

A custom stringify function (e.g. fast-json-stringify) can pan passed as the second argument.

const json = { currency: "💶", value: 120 };
const fastJSON = await import("fast-json-stringify");
const stringify = fastJSON({
title: "Example Schema",
type: "object",
properties: {
currency: {
type: "string",
},
value: {
type: "integer",
},
},
required: ["currency", "value"],
additionalProperties: false,
});
const json = { currency: "💶", value: 120 };
await context.json(json, stringify);
// or using `BindenResponse`
await context.response.json(json, stringify);
context.done = true;
  • .text() - execute context.response.text() and set context.done to true
const text = "Hello World!";
await context.text(text);
// or
await context.response.text(text);
context.done = true;
  • .html() - execute context.response.html() and set context.done to true
const html = "<html></html>";
await context.html(html);
// or
await context.response.html(html);
context.done = true;
  • .form() - execute context.response.form() and set context.done to true
const form = new URLSearchParams({ a: "1" });
await context.form(form);
// or
await context.response.form(form);
context.done = true;
  • .sendFile() - execute context.response.sendFile() and set context.done to true
const path = "<path to file>";
await context.sendFile(path);
// or
await context.response.sendFile(path);
context.done = true;
  • .throw() - throw BindenError
context.throw(402, { json: { error: "Payment Required" }, expose: true });

Middleware

Any middleware should be extended from the abstract Middleware class and implement the .run() method

import { randomInt } from "crypto";
import { Middleware, Context } from "binden";

export class MyMiddleware extends Middleware {
public async run(context: Context): Promise<void> {
const randomNumber = await new Promise((resolve, reject) => {
randomInt(1, 100, (error, n) => {
if (error) {
reject(error);
} else {
resolve(n);
}
});
});

if (randomNumber <= 50) {
context.throw(400, {
message: "Generated number is less than or equal to 50",
expose: true,
});
}

return context.json({ message: "Generated number is greater than 50" });
}
}
  • .disabled - One can disable a middleware at any time
import { Middleware, Context } from "binden";

export class MyMiddleware1 extends Middleware {
public run(context: Context): Promise<void> {
return context.json({ message: "Hello World" });
}
}

const mm1 = new MyMiddleware1({ disabled: true });

export class MyMiddleware2 extends Middleware {
public async run(): Promise<void> {
// Disable `mm1` every hour
setInterval(
() => {
mm1.disabled = !mm1.disabled;
},
1000 * 60 * 60,
);
}
}
  • .ignore_errors - ignore errors from await this.run(context)
import { Middleware, Context } from "binden";

export class MyMiddleware1 extends Middleware {
public run(context: Context): Promise<void> {
if (this.ignore_errors) {
return context.json({ message: "Hello World" });
}
context.throw(400);
}
}

const mm1 = new MyMiddleware1({ ignore_errors: true });

export class MyMiddleware2 extends Middleware {
public async run(): Promise<void> {
// Throw errors from `mm1.run()` every minute
setInterval(() => {
mm1.ignore_errors = !mm1.ignore_errors;
}, 1000 * 60);
}
}

BindenError

BindenError represents an HTTP error

import { Middleware, Context, BindenError } from "binden";

export class MyMiddleware extends Middleware {
public run(context: Context): Promise<void> {
const { length } = context.request.cookies;

if (!length) {
const status = 401;
const expose = true;
const message =
"Text message to send (when `expose === true` and `json === null`)";
const json = {
error:
"Send `json` as application/json (when `expose === true`) instead of `message`",
};
throw new BindenError(status, { expose, message, json });
}

try {
await validateBody();
} catch (cause) {
const message = "Invalid body";
const expose = true;
throw new BindenError(400, { expose, message, json: { message }, cause });
}

return context.json({ message: `Received ${length} cookies` });
}
}

BindenRequest

Simple usage with http

import { createServer } from "http";
import { BindenRequest } from "binden";
server = createServer({ IncomingMessage: BindenRequest });
  • .header() - get a header by name
const rawHeader = request.header("X-Header");
  • .id - get the request id
const { id } = request;
const { protocol } = request;
if (protocol !== "https:") {
console.error("The connection is not secure");
}
  • .secure - same as request.protocol === "https:"
const { secure } = request;
if (!secure) {
console.error("The connection is not secure");
}
  • .query - A copy of URL.search parsed by the querystring.parse() method
const { query } = request;
// same as
const query = { ...parse(this.URL.search.substring(1)) };

BindenResponse

Simple usage with http

import { createServer } from "http";
import { BindenResponse } from "binden";
server = createServer({ ServerResponse: BindenResponse });
  • .cookies - The .send() method will add cookies to the response
import { randomUUID } from "crypto";
import { Cookie } from "binden";

const key = "__Secure-Random-UUID";
const value = randomUUID();
const cookie = new Cookie({ key, value });
response.cookies.add(cookie);
await response.send("Check the `Set-Cookie` header for a random UUID");
  • .status() - Set the status code of the response
await response.status(400).send();
  • .set() - Set the headers
const headers = {
"X-AMOUNT": "100.02 USD",
"X-MONTHS": ["jan", "feb"],
};
await response.status(402).set(headers).send("Payment is required");
  • .send() - send data
await response.send(
"Could be `number` | `string` | `Buffer` | `Readable` | `bigint` | `undefined`",
);
  • .json() - send an object as application/json using JSON.stringify()
const json = { k: "v", k1: 1, m: "message", f: false };
await response.json(json);

or using a custom stringify function

const json = { currency: "💶", value: 120 };
const fastJSON = await import("fast-json-stringify");
const stringify = fastJSON({
title: "Example Schema",
type: "object",
properties: {
currency: {
type: "string",
},
value: {
type: "integer",
},
},
required: ["currency", "value"],
additionalProperties: false,
});
await response.json(json);
  • .text() - send text as plain/text
const text = "Hello World!";
await response.text(text);
  • .html() - send text as text/html
const html = "<html></html>";
await response.html(html);
  • .form() - send URLSearchParams;
const form = new URLSearchParams({ a: "1", b: ["a", "c"] });
await response.form(form);
const path = "<path to file>";
await response.sendFile(path);
// Or with custom Stats
import { stat } from "node:fs/promises";
const stats = await stat("<PATH>");
await response.sendFile(path, stats);

Headers

import { AcceptEncoding } from "binden";

const encodings = AcceptEncoding.fromString(request.headers["accept-encoding"]);
// or using BindenRequest
const { accept_encoding } = request;
import { Authorization } from "binden";

const authorization = AcceptEncoding.fromString(
request.headers["Authorization"],
);
// or using BindenRequest
const { authorization } = request;
import { ContentEncoding } from "binden";

const encodings = ContentEncoding.fromString(
request.headers["content-encoding"],
);
// or using BindenRequest
const { content_encoding } = request;
import { ContentRange } from "binden";

const cr = new ContentRange({ start: 0, end: 499, size: 1000 });
response.setHeader("Content-Range", cr.toString());
import { ContentType } from "binden";

const type = ContentType.fromString(request.headers["content-type"]);
// or using BindenRequest
const { content_type } = request;
import { Cookie } from "binden";

const cookies = Cookie.fromString(request.headers["cookie"]);
// or using BindenRequest
const { cookies } = request;
// or using BindenResponse
const cookie1 = new Cookie({
key: "__Secure-K1",
value: "v1",
http_only: false,
});
const cookie2 = new Cookie({
key: "K2",
value: "v2",
same_site: "None",
max_age: 1000,
});
response.cookies.add(cookie1).add(cookie2);
import { Forwarded } from "binden";

const forwarded = Forwarded.fromString(request.headers["forwarded"]);
// or using BindenRequest
const { forwarded } = request;
import { IfModifiedSince } from "binden";

const if_modified_since = IfModifiedSince.fromString(
request.headers["if-modified-since"],
);
// or using BindenRequest
const { if_modified_since } = request;
import { Range } from "binden";

const range = Range.fromString(request.headers.range);
// or using BindenRequest
const { range } = request;

Test

npm run test:ci