@transformgovsg/zing
Version:
A lightweight web framework.
1,124 lines (1,114 loc) • 32.2 kB
JavaScript
// src/zing.ts
import { createServer as createHTTPServer } from "node:http";
// src/errors.ts
var BaseError = class extends Error {
constructor(message) {
super(message);
this.name = this.constructor.name;
}
};
var ContentTooLargeError = class extends BaseError {
type = "CONTENT_TOO_LARGE_ERROR";
constructor() {
super("The request content is too large.");
}
};
var UnsupportedContentTypeError = class extends BaseError {
type = "UNSUPPORTED_CONTENT_TYPE_ERROR";
constructor() {
super("The request content type is not supported.");
}
};
var MalformedJSONError = class extends BaseError {
type = "MALFORMED_JSON_ERROR";
constructor() {
super("The request payload is not a valid JSON object.");
}
};
var InternalServerError = class extends BaseError {
type = "INTERNAL_SERVER_ERROR";
constructor(cause) {
super("An unexpected error occurred. Check the cause property for more details.");
this.cause = cause;
}
};
// src/http-status-code.ts
var HTTPStatusCode = {
Continue: 100,
SwitchingProtocols: 101,
Processing: 102,
EarlyHints: 103,
OK: 200,
Created: 201,
Accepted: 202,
NonAuthoritativeInformation: 203,
NoContent: 204,
ResetContent: 205,
PartialContent: 206,
MultiStatus: 207,
AlreadyReported: 208,
IMUsed: 226,
MultipleChoices: 300,
MovedPermanently: 301,
Found: 302,
SeeOther: 303,
NotModified: 304,
TemporaryRedirect: 307,
PermanentRedirect: 308,
BadRequest: 400,
Unauthorized: 401,
PaymentRequired: 402,
Forbidden: 403,
NotFound: 404,
MethodNotAllowed: 405,
NotAcceptable: 406,
ProxyAuthenticationRequired: 407,
RequestTimeout: 408,
Conflict: 409,
Gone: 410,
LengthRequired: 411,
PreconditionFailed: 412,
ContentTooLarge: 413,
URITooLong: 414,
UnsupportedMediaType: 415,
RangeNotSatisfiable: 416,
ExpectationFailed: 417,
ImATeapot: 418,
MisdirectedRequest: 421,
UnprocessableContent: 422,
Locked: 423,
FailedDependency: 424,
TooEarly: 425,
UpgradeRequired: 426,
PreconditionRequired: 428,
TooManyRequests: 429,
RequestHeaderFieldsTooLarge: 431,
UnavailableForLegalReasons: 451,
InternalServerError: 500,
NotImplemented: 501,
BadGateway: 502,
ServiceUnavailable: 503,
GatewayTimeout: 504,
HTTPVersionNotSupported: 505,
VariantAlsoNegotiates: 506,
InsufficientStorage: 507,
LoopDetected: 508,
NotExtended: 510,
NetworkAuthenticationRequired: 511
};
// src/options.ts
var DEFAULT_OPTIONS = {
maxBodySize: 1048576
};
// src/cookie.ts
var VALID_COOKIE_NAME_REGEX = /^[\u0021-\u003A\u003C\u003E-\u007E]+$/;
var VALID_COOKIE_VALUE_REGEX = /^[\u0021-\u003A\u003C-\u007E]*$/;
function parse(cookie, name) {
if (cookie.indexOf(name) === -1) {
return null;
}
for (let kv of cookie.trim().split(";")) {
kv = kv.trim();
const equalPos = kv.indexOf("=");
if (equalPos === -1) {
continue;
}
const key = kv.slice(0, equalPos).trim();
if (key !== name || !VALID_COOKIE_NAME_REGEX.test(key)) {
continue;
}
let value = kv.slice(equalPos + 1).trim();
if (value.startsWith('"') && value.endsWith('"')) {
value = value.slice(1, -1);
}
if (!VALID_COOKIE_VALUE_REGEX.test(value)) {
continue;
}
return decodeURIComponent(value);
}
return null;
}
function serialise(name, value, options) {
let cookie = `${name}=${encodeURIComponent(value)}`;
if (options?.path) {
cookie += `; Path=${options.path}`;
}
if (options?.domain) {
cookie += `; Domain=${options.domain}`;
}
if (options?.expires) {
cookie += `; Expires=${options.expires.toUTCString()}`;
}
if (options?.maxAge) {
if (options.maxAge > 0) {
cookie += `; Max-Age=${options.maxAge | 0}`;
}
if (options.maxAge < 0) {
cookie += "; Max-Age=0";
}
}
if (options?.secure) {
cookie += "; Secure";
}
if (options?.httpOnly) {
cookie += "; HttpOnly";
}
switch (options?.sameSite) {
case "strict":
cookie += "; SameSite=Strict";
break;
case "lax":
cookie += "; SameSite=Lax";
break;
case "none":
cookie += "; SameSite=None";
break;
}
return cookie;
}
// src/result.ts
var Ok = class {
constructor(value) {
this.value = value;
}
isOk() {
return true;
}
isErr() {
return !this.isOk();
}
unwrap() {
return this.value;
}
unwrapOr(_defaultValue) {
return this.value;
}
};
var Err = class {
constructor(error) {
this.error = error;
}
isOk() {
return false;
}
isErr() {
return !this.isOk();
}
unwrap() {
throw this.error;
}
unwrapOr(defaultValue) {
return defaultValue;
}
};
function OK(value) {
return new Ok(value);
}
function ERR(error) {
return new Err(error);
}
// src/request.ts
var ALLOWED_HTTP_METHODS_WITH_BODY = ["PATCH", "POST", "PUT"];
var Request = class {
node;
#kv = /* @__PURE__ */ new Map();
#body = null;
#options;
#url;
constructor(req, options) {
this.node = req;
this.#options = options;
const protocol = req.socket && "encrypted" in req.socket && req.socket.encrypted ? "https" : "http";
this.#url = new URL(req.url, `${protocol}://${req.headers.host}`);
}
/**
* Returns the protocol of the request.
*/
get protocol() {
return this.#url.protocol === "http:" ? "http" : "https";
}
/**
* Returns the pathname of the request.
*/
get pathname() {
return this.#url.pathname;
}
/**
* Returns the HTTP method of the request.
*/
get method() {
return this.node.method;
}
/**
* Returns the value of the given key from the request-scoped key-value
* store. If the key is not found and no default value is provided, `null`
* is returned.
*
* @param key - The key to get the value of.
* @param defaultValue - An optional default value to return if the key is not found.
*/
get(key, defaultValue) {
const value = this.#kv.get(key);
if (!value) {
return defaultValue ?? null;
}
return value;
}
/**
* Stores a value in the request-scoped key-value store. The store persists
* only for the duration of the current request. If the value is `undefined`
* or `null`, the key is removed from the store.
*
* @param key - The key to store the value under.
* @param value - The value to store.
*/
set(key, value) {
if (value === void 0 || value === null) {
this.#kv.delete(key);
return;
}
this.#kv.set(key, value);
}
/**
* Returns the value of the given cookie name from the request. If the
* cookie is not found and no default value is provided, `null` is returned.
*
* @param name - The name of the cookie to get the value of.
* @param defaultValue - An optional default value to return if the cookie is not found.
*/
cookie(name, defaultValue) {
const cookie = this.header("cookie");
if (!cookie) {
return defaultValue ?? null;
}
return parse(cookie, name) ?? defaultValue ?? null;
}
/**
* Returns the value of the given parameter name from the request. If
* the parameter is not found and no default value is provided, `null` is
* returned.
*
* @param name - The name of the parameter to get the value of.
* @param defaultValue - An optional default value to return if the parameter is not found.
*/
param(name, defaultValue) {
const params = this.get("_params");
if (!params) {
return defaultValue ?? null;
}
const value = params.get(name);
if (!value) {
return defaultValue ?? null;
}
return value;
}
/**
* Returns the value of the first occurrence of the given query name from
* the request. If the query is not found and no default value is provided,
* `null` is returned.
*
* @param name - The name of the query to get the value of.
* @param defaultValue - An optional default value to return if the query is not found.
*/
query(name, defaultValue) {
const value = this.#url.searchParams.get(name);
if (!value) {
return defaultValue ?? null;
}
return value;
}
/**
* Returns the values of all occurrences of the given query name from the
* request. If the query is not found and no default value is provided, `null`
* is returned.
*
* @param name - The name of the query to get the values of.
* @param defaultValue - An optional default value to return if the query is not found.
*/
queries(name, defaultValue) {
const value = this.#url.searchParams.getAll(name);
if (value.length === 0) {
return defaultValue ?? null;
}
return value;
}
/**
* Returns the value of the given header name from the request. If the
* header is not found and no default value is provided, `null` is returned.
*
* @param name - The name of the header to get the value of.
* @param defaultValue - An optional default value to return if the header is not found.
*/
header(name, defaultValue) {
name = name.toLowerCase();
const value = this.node.headers[name];
if (!value) {
return defaultValue ?? null;
}
if (Array.isArray(value)) {
return value.join(", ");
}
return value;
}
/**
* Returns a {@link Result} with the body of the request as a {@link Uint8Array}
* or `null` if the HTTP method is not `PATCH`, `POST`, or `PUT`.
*
* Errors that may be returned:
* - {@link ContentTooLargeError} - If the body is too large.
* - {@link InternalServerError} - If an error occurs while reading the body.
*/
async body() {
const contentLength = this.header("content-length");
if (!ALLOWED_HTTP_METHODS_WITH_BODY.includes(this.method)) {
return OK(null);
}
if (!contentLength || contentLength === "0") {
return OK(null);
}
if (Number(contentLength) > this.#options.maxBodySize) {
return ERR(new ContentTooLargeError());
}
if (this.#body) {
return OK(this.#body);
}
return new Promise(
(resolve) => {
let totalLength = 0;
const chunks = [];
const onData = (chunk) => {
totalLength += chunk.length;
if (totalLength > this.#options.maxBodySize) {
this.node.removeListener("data", onData);
this.node.removeListener("end", onEnd);
this.node.removeListener("error", onError);
resolve(ERR(new ContentTooLargeError()));
}
chunks.push(chunk);
};
const onEnd = () => {
this.node.removeListener("data", onData);
this.node.removeListener("end", onEnd);
this.node.removeListener("error", onError);
this.#body = new Uint8Array(chunks.reduce((sum, chunk) => sum + chunk.length, 0));
let offset = 0;
for (const chunk of chunks) {
this.#body.set(chunk, offset);
offset += chunk.length;
}
resolve(OK(this.#body));
};
const onError = (err) => {
this.node.removeListener("data", onData);
this.node.removeListener("end", onEnd);
this.node.removeListener("error", onError);
resolve(ERR(new InternalServerError(err)));
};
this.node.on("data", onData);
this.node.on("end", onEnd);
this.node.on("error", onError);
}
);
}
/**
* Returns a {@link Result} with the body of the request as a string or
* `null` if the HTTP method is not `PATCH`, `POST`, or `PUT`.
*
* Errors that may be returned:
* - {@link ContentTooLargeError} - If the body is too large.
* - {@link InternalServerError} - If an error occurs while reading the body.
* - {@link UnsupportedContentTypeError} - If the content type is not `text/plain`.
*/
async text() {
const contentType = this.header("content-type")?.split(";")[0];
if (!contentType || contentType !== "text/plain") {
return ERR(new UnsupportedContentTypeError());
}
const result = await this.body();
if (result.isErr()) {
return ERR(result.error);
}
if (!result.value) {
return OK(null);
}
return OK(new TextDecoder().decode(result.value));
}
/**
* Returns a {@link Result} with the body of the request as a JSON object or
* `null` if the HTTP method is not `PATCH`, `POST`, or `PUT`.
*
* Errors that may be returned:
* - {@link ContentTooLargeError} - If the body is too large.
* - {@link InternalServerError} - If an error occurs while reading the body.
* - {@link UnsupportedContentTypeError} - If the content type is not `application/json`.
* - {@link MalformedJSONError} - If the body is not valid JSON.
*/
async json() {
const contentType = this.header("content-type")?.split(";")[0];
if (!contentType || contentType !== "application/json") {
return ERR(new UnsupportedContentTypeError());
}
const result = await this.body();
if (result.isErr()) {
return ERR(result.error);
}
if (!result.value) {
return OK(null);
}
try {
return OK(JSON.parse(new TextDecoder().decode(result.value)));
} catch (err) {
if (err instanceof SyntaxError) {
return ERR(new MalformedJSONError());
}
return ERR(new InternalServerError(err));
}
}
};
// src/response.ts
var Response = class {
node;
constructor(res) {
this.node = res;
}
/**
* Returns `true` if the response has been sent, otherwise `false`.
*/
get finished() {
return this.node.writableFinished;
}
/**
* Sends the HTTP response with the specified status code and ends the
* response. If the response has already been sent, this method is a no-op
* and does nothing.
*
* @param code - The HTTP status code to send (e.g., 200, 404, 500).
*/
status(code) {
if (this.finished) {
return;
}
this.node.writeHead(code).end();
}
/**
* Sends a 200 status code to the response and ends the response. If the
* response has already been sent, this method is a no-op and does nothing.
*
* This method is a shorthand for `status(HTTPStatusCode.OK)`.
*/
ok() {
this.status(HTTPStatusCode.OK);
}
/**
* Sets a cookie on the response. If the response has already been sent,
* this method does nothing.
*
* @param name - The name of the cookie.
* @param value - The value of the cookie.
* @param options - The options for the cookie.
*/
cookie(name, value, options) {
if (this.finished) {
return;
}
this.node.setHeader("Set-Cookie", serialise(name, value, options));
}
/**
* Sets the given header key and value on the response. If the response has
* already been sent, this method does nothing.
*
* @param key - The key of the header to set.
* @param value - The value of the header to set.
*/
header(key, value) {
if (this.finished) {
return;
}
this.node.setHeader(key, value);
}
/**
* Sends a JSON response with the specified status code and ends the response.
* If the response has already been sent, this method is a no-op and does
* nothing.
*
* @param code - The HTTP status code to send (e.g., 200, 404, 500).
* @param data - The JSON data to send in the response body.
*/
json(code, data) {
if (this.finished) {
return;
}
const raw = JSON.stringify(data);
const headers = {
"content-type": "application/json; charset=utf-8",
"content-length": Buffer.byteLength(raw).toString()
};
if (this.node.req.method === "HEAD") {
this.node.writeHead(code, headers).end();
return;
}
this.node.writeHead(code, headers).end(raw);
}
/**
* Sends a text response with the specified status code and ends the response.
* If the response has already been sent, this method is a no-op and does
* nothing.
*
* @param code - The HTTP status code to send (e.g., 200, 404, 500).
* @param data - The text data to send in the response body.
*/
text(code, data) {
if (this.finished) {
return;
}
const headers = {
"content-type": "text/plain; charset=utf-8",
"content-length": Buffer.byteLength(data).toString()
};
if (this.node.req.method === "HEAD") {
this.node.writeHead(code, headers).end();
return;
}
this.node.writeHead(code, headers).end(data);
}
};
// src/router.ts
var Node = class _Node {
type;
fragment;
indicies;
children;
name;
data;
constructor({
type = "static",
fragment = "",
indicies = "",
children = [],
name = null,
data = null
} = {}) {
this.type = type;
this.fragment = fragment;
this.indicies = indicies;
this.children = children;
this.name = name;
this.data = data;
}
/**
* Creates a new child node and adds it to this node's children.
* The type of child node created depends on the character provided:
* - `:` creates a dynamic node
* - `*` creates a catch-all node
* - Any other character creates a static node
*
* @param char - The character to create a child node for.
*/
createChild(char) {
const child = new _Node();
switch (char) {
case ":":
child.type = "dynamic";
break;
case "*":
child.type = "catch-all";
break;
}
this.indicies += char;
this.children.push(child);
return child;
}
/**
* Returns the child node that matches the given character.
* Looks through this node's indices to find a matching child.
*
* @param char - The character to search for.
*/
findChild(char) {
for (let i = 0; i < this.indicies.length; i++) {
if (this.indicies[i] === char) {
return this.children[i];
}
}
return null;
}
/**
* Returns a human-readable string representation of the node and its children.
*/
stringify() {
let result = "\n";
const stack = [[this, "", true]];
while (stack.length > 0) {
const item = stack.pop();
if (!item) {
continue;
}
const [node, prefix, isLast] = item;
result += `${prefix}${isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 "}`;
result += `${node.fragment}, type=${node.type}, `;
if (node.indicies.length > 0) {
result += `indicies=[${node.indicies.split("").join(", ")}], `;
}
if (node.name) {
result += `name=${node.name}, `;
}
result += `data=${node.data ? "Y" : "N"}`;
result += "\n";
for (let i = node.children.length - 1; i >= 0; i--) {
const child = node.children[i];
const isLastChild = i === node.children.length - 1;
stack.push([child, prefix + (isLast ? " " : "\u2502 "), isLastChild]);
}
}
return result;
}
};
var Router = class {
#nodes = /* @__PURE__ */ new Map();
/**
* Returns a human-readable string representation of the router.
*/
stringify() {
let result = "\n";
for (const [method, node] of this.#nodes.entries()) {
result += `${method}`;
result += node.stringify();
}
return result;
}
/**
* Adds a route to the router.
*
* @param method - The HTTP method to add the route for.
* @param pattern - The pattern to add the route for.
* @param data - The data to associate with the route.
*/
addRoute(method, pattern, data) {
let node = this.#nodes.get(method);
if (!node) {
node = new Node();
this.#nodes.set(method, node);
}
if (!pattern.startsWith("/")) {
pattern = "/" + pattern;
}
while (pattern.length > 0) {
if (node.fragment.length > 0) {
const i = indexCommonPrefix(node.fragment, pattern);
if (i < node.fragment.length) {
const child = new Node({
type: node.type,
fragment: node.fragment.slice(i),
indicies: node.indicies,
children: node.children,
name: node.name,
data: node.data
});
node.fragment = pattern.slice(0, i);
node.indicies = child.fragment[0];
node.children = [child];
node.name = null;
node.data = null;
}
pattern = pattern.slice(i);
switch (node.type) {
case "dynamic":
case "catch-all":
pattern = pattern.slice(indexAny(pattern, "/"));
break;
}
if (pattern.length > 0) {
node = node.findChild(pattern[0]) ?? node.createChild(pattern[0]);
}
continue;
}
switch (node.type) {
case "static": {
const i = indexAny(pattern, ":*");
node.fragment = pattern.slice(0, i);
pattern = pattern.slice(i);
break;
}
case "dynamic": {
const i = indexAny(pattern, "/");
const name = pattern.slice(1, i);
if (name.length === 0) {
return ERR(new Error("Dynamic parameter must have a name."));
}
if (name.includes(":") || name.includes("*")) {
return ERR(
new Error("Only one dynamic or catch-all parameter is allowed per path segment.")
);
}
node.fragment = ":";
node.name = name;
pattern = pattern.slice(i);
break;
}
case "catch-all": {
const i = indexAny(pattern, "/");
const name = pattern.slice(1, i);
if (name.length === 0) {
return ERR(new Error("Catch-all parameter must have a name."));
}
if (name.length + 1 !== pattern.length) {
return ERR(new Error("Catch-all parameter must be the last path segment."));
}
if (name.includes(":") || name.includes("*")) {
return ERR(
new Error("Only one dynamic or catch-all parameter is allowed per path segment.")
);
}
node.fragment = "*";
node.name = name;
pattern = pattern.slice(pattern.length);
break;
}
}
if (pattern.length > 0) {
node = node.createChild(pattern[0]);
}
}
node.data = data;
return OK();
}
/**
* Returns the data and parameters for the route that matches the given
* pathname. If no route is found, `null` is returned.
*
* @param method - The HTTP method to find the route for.
* @param pathname - The pathname to find the route for.
*/
findRoute(method, pathname) {
const node = this.#nodes.get(method);
if (!node) {
return null;
}
const params = /* @__PURE__ */ new Map();
const data = this.#lookup(node, pathname, params);
if (!data) {
return null;
}
return {
data,
params: params.size > 0 ? params : null
};
}
#lookup(node, pathname, params) {
const i = indexCommonPrefix(node.fragment, pathname);
if (i === pathname.length) {
return node.data;
}
pathname = pathname.slice(i);
let child = node.findChild(pathname[0]);
if (child) {
const data = this.#lookup(child, pathname, params);
if (data) {
return data;
}
}
child = node.findChild(":");
if (child) {
const i2 = indexAny(pathname, "/");
const value = pathname.slice(0, i2);
params.set(child.name, value);
if (i2 === pathname.length) {
return child.data;
}
if (child.children.length === 1) {
const data = this.#lookup(child.children[0], pathname.slice(i2), params);
if (data) {
return data;
}
}
params.delete(child.name);
}
child = node.findChild("*");
if (child) {
params.set(child.name, pathname);
return child.data;
}
return null;
}
};
function indexCommonPrefix(a, b) {
const l = Math.min(a.length, b.length);
for (let i = 0; i < l; i++) {
if (a[i] !== b[i]) {
return i;
}
}
return l;
}
function indexAny(s, chars) {
let earliest = s.length;
for (const char of chars) {
for (let i = 0; i < s.length; i++) {
if (s[i] === char && i < earliest) {
earliest = i;
}
}
}
return earliest;
}
// src/zing.ts
var DEFAULT_404_HANDLER = (_, res) => {
res.text(HTTPStatusCode.NotFound, "Not Found");
};
var DEFAULT_ERROR_HANDLER = (err, _, res) => {
res.header("connection", "close");
if (err instanceof ContentTooLargeError) {
res.text(HTTPStatusCode.ContentTooLarge, "Content Too Large");
return;
}
if (err instanceof UnsupportedContentTypeError) {
res.text(HTTPStatusCode.UnsupportedMediaType, "Unsupported Media Type");
return;
}
if (err instanceof MalformedJSONError) {
res.text(HTTPStatusCode.UnprocessableContent, "Unprocessable Content");
return;
}
res.text(HTTPStatusCode.InternalServerError, "Internal Server Error");
};
var Zing = class {
#isListening = false;
#isShuttingDown = false;
#activeRequestCountPerSocket = /* @__PURE__ */ new Map();
#middleware = [];
#fn404Handler = DEFAULT_404_HANDLER;
#fnErrorHandler = DEFAULT_ERROR_HANDLER;
#options;
#server;
#router;
constructor(options = {}) {
this.#options = {
...DEFAULT_OPTIONS,
...options
};
this.#server = createHTTPServer(this.#dispatch.bind(this));
this.#router = new Router();
}
/**
* Starts listening for incoming connections on the specified port.
*
* @param port - The port to listen on. Defaults to `8080`.
*/
async listen(port = 8080) {
if (this.#isListening) {
return;
}
this.#isListening = true;
await new Promise((resolve, reject) => {
this.#server.on("connection", (socket) => {
this.#activeRequestCountPerSocket.set(socket, 0);
socket.on("close", () => this.#activeRequestCountPerSocket.delete(socket));
});
this.#server.on("request", (req, res) => {
this.#activeRequestCountPerSocket.set(
req.socket,
(this.#activeRequestCountPerSocket.get(req.socket) ?? 0) + 1
);
res.on("finish", () => {
this.#activeRequestCountPerSocket.set(
req.socket,
(this.#activeRequestCountPerSocket.get(req.socket) ?? 0) - 1
);
if (this.#isShuttingDown && this.#activeRequestCountPerSocket.get(req.socket) === 0) {
req.socket.destroy();
}
});
});
this.#server.once("error", (err) => {
reject(err);
});
this.#server.listen(port, resolve);
});
}
/**
* Gracefully shuts down the server by stopping it from accepting new
* connections and closing all idle connections. After a specified timeout,
* all connections will be closed.
*
* @param timeout - The timeout in milliseconds. Defaults to `10000`.
*/
async shutdown(timeout = 1e4) {
if (this.#isShuttingDown) {
return;
}
this.#isShuttingDown = true;
let timeoutRef = null;
try {
await new Promise((resolve, reject) => {
this.#server.close((err) => {
if (err) {
reject(err);
return;
}
resolve();
});
for (const [socket, count] of this.#activeRequestCountPerSocket.entries()) {
if (socket.readyState === "open" && count === 0) {
socket.destroy();
}
}
timeoutRef = setTimeout(() => {
this.#server.closeAllConnections();
}, timeout);
});
} finally {
if (timeoutRef) {
clearTimeout(timeoutRef);
}
}
}
/**
* Adds a route with the specified HTTP method, pattern, optional route-level
* middleware and handler.
*
* @param method - The HTTP method to add the route for.
* @param pattern - The pattern to match the route against.
* @param args - The middleware and handler to call when the route is matched.
*
* @throws {Error} If the given pattern is invalid.
*/
route(method, pattern, ...args) {
let handler = args.at(-1);
const middleware = args.slice(0, -1);
for (let i = middleware.length - 1; i >= 0; i--) {
handler = middleware[i](handler);
}
const result = this.#router.addRoute(method, pattern, { handler });
if (result.isErr()) {
throw result.error;
}
}
/**
* Adds a `GET` route with the specified pattern, optional route-level
* middleware and handler.
*
* @param pattern - The pattern to match the route against.
* @param args - The middleware and handler to call when the route is matched.
*/
get(pattern, ...args) {
this.route("GET", pattern, ...args);
}
/**
* Adds a `HEAD` route with the specified pattern, optional route-level
* middleware and handler.
*
* @param pattern - The pattern to match the route against.
* @param args - The middleware and handler to call when the route is matched.
*/
head(pattern, ...args) {
this.route("HEAD", pattern, ...args);
}
/**
* Adds a `PATCH` route with the specified pattern, optional route-level
* middleware and handler.
*
* @param pattern - The pattern to match the route against.
* @param args - The middleware and handler to call when the route is matched.
*/
patch(pattern, ...args) {
this.route("PATCH", pattern, ...args);
}
/**
* Adds a `POST` route with the specified pattern, optional route-level
* middleware and handler.
*
* @param pattern - The pattern to match the route against.
* @param args - The middleware and handler to call when the route is matched.
*/
post(pattern, ...args) {
this.route("POST", pattern, ...args);
}
/**
* Adds a `PUT` route with the specified pattern, optional route-level
* middleware and handler.
*
* @param pattern - The pattern to match the route against.
* @param args - The middleware and handler to call when the route is matched.
*/
put(pattern, ...args) {
this.route("PUT", pattern, ...args);
}
/**
* Adds a `DELETE` route with the specified pattern, optional route-level
* middleware and handler.
*
* @param pattern - The pattern to match the route against.
* @param args - The middleware and handler to call when the route is matched.
*/
delete(pattern, ...args) {
this.route("DELETE", pattern, ...args);
}
/**
* Adds a `OPTIONS` route with the specified pattern, optional route-level
* middleware and handler.
*
* @param pattern - The pattern to match the route against.
* @param args - The middleware and handler to call when the route is matched.
*/
options(pattern, ...args) {
this.route("OPTIONS", pattern, ...args);
}
/**
* Adds an application-level middleware to be called for each incoming
* request regardless of whether it matches a route or not.
*
* @param middleware - The middleware to be called for each request.
*/
use(...middleware) {
this.#middleware.push(...middleware);
}
/**
* Sets the handler to call when a route is not found.
*
* @param handler - The handler to call when a route is not found.
*/
set404Handler(handler) {
this.#fn404Handler = handler;
}
/**
* Sets the handler to call when an error occurs.
*
* @param handler - The handler to call when an error occurs.
*/
setErrorHandler(handler) {
this.#fnErrorHandler = handler;
}
async #dispatch(nodeReq, nodeRes) {
const req = new Request(nodeReq, this.#options);
const res = new Response(nodeRes);
try {
const route = this.#router.findRoute(req.method, req.pathname);
if (route) {
req.set("_params", route.params);
}
let handler = route ? route.data.handler : this.#fn404Handler;
for (let i = this.#middleware.length - 1; i >= 0; i--) {
handler = this.#middleware[i](handler);
}
await handler(req, res);
} catch (err) {
try {
await this.#fnErrorHandler(err, req, res);
} catch (err2) {
await DEFAULT_ERROR_HANDLER(err2, req, res);
}
}
}
};
export {
BaseError,
ContentTooLargeError,
HTTPStatusCode,
InternalServerError,
MalformedJSONError,
Request,
Response,
UnsupportedContentTypeError,
Zing,
Zing as default
};