@transformgovsg/zing
v0.4.0
Published
A lightweight web framework.
Maintainers
Readme
Zing
A lightweight HTTP framework for Node.js.
🚧 Roadmap
- [x] ~~Cookies~~
- [x] ~~Middleware~~
- [ ] Server-sent events
- [ ] Header, query, and body schema validation
📦 Install
pnpm add @transformgovsg/zingAlternatively, using yarn or npm:
yarn add @transformgovsg/zingnpm install @transformgovsg/zing🚀 Usage
import Zing, { HTTPStatusCode } from '@transformgovsg/zing';
const app = new Zing();
// Simplest route.
app.get('/', (_, res) => {
res.ok();
});
// Route with a dynamic parameter.
app.get('/hello/:name', (req, res) => {
res.text(HTTPStatusCode.OK, `Dynamic parameter: ${req.param('name')}!`);
});
// Route with a catch-all parameter.
app.get('/hello/*name', (req, res) => {
res.text(HTTPStatusCode.OK, `Catch-all parameter: ${req.param('name')}!`);
});
await app.listen(8080);
process.on('SIGTERM', async () => {
await app.shutdown();
});
process.on('SIGINT', async () => {
await app.shutdown();
});🔌 API
- Zing.constructor()
- Zing.listen()
- Zing.shutdown()
- Zing.route()
- Zing.get()
- Zing.head()
- Zing.patch()
- Zing.post()
- Zing.put()
- Zing.delete()
- Zing.options()
- Zing.use()
- Zing.set404Handler()
- Zing.setErrorHandler()
- Request.node
- Request.protocol
- Request.pathname
- Request.method
- Request.get()
- Request.set()
- Request.cookie()
- Request.param()
- Request.query()
- Request.queries()
- Request.header()
- Request.body()
- Request.text()
- Request.json()
- Response.node
- Response.finished
- Response.status()
- Response.ok()
- Response.cookie()
- Response.json()
- Response.text()
- Response.header()
Zing.constructor()
Creates a new Zing application.
Type
constructor(options?: Partial<Options>);Parameters
options- The options for the Zing application.maxBodySize- The maximum size of the body of a request. Default:1_048_576.
Example
const app = new Zing();With custom options:
const app = new Zing({ maxBodySize: 4 * 1024 }); // Limit to 4KB.Zing.listen()
Starts the HTTP server and listens on the given port for incoming requests.
Type
listen(port?: number): Promise<void>;Parameters
port- The port to listen on. Default:8080.
Example
await app.listen(); // Listen on port 8080.
await app.listen(8123); // Listen on port 8123.Zing.shutdown()
Shuts down the HTTP server.
Type
shutdown(timeout?: number): Promise<void>;Parameters
timeout- The time in milliseconds to wait for active requests to finish before forcefully shutting down the HTTP server. Default:10000.
Example
await app.shutdown(); // Shutdown the HTTP server after 10 seconds.
await app.shutdown(5000); // Shutdown the HTTP server after 5 seconds.Zing.route()
Adds a route to the application.
Type
route(method: 'GET' | 'HEAD' | 'PATCH' | 'POST' | 'PUT' | 'DELETE' | 'OPTIONS', pattern: string, ...args: [...Middleware[], Handler]): void;Parameters
method- The HTTP method to match against the request method.pattern- The pattern to match against the request pathname.args- The middleware and handler to call when the route is matched.
Throws
Error- If thepatternis invalid.
Example
app.route('GET', '/', (req, res) => {
res.ok();
});With middleware:
function logger(next: Handler) {
return async (req, res) => {
console.log(`${req.method} ${req.pathname}`);
await next(req, res);
};
}
app.route('GET', '/', logger, (req, res) => {
res.ok();
});Zing.get()
Adds a GET route to the application.
Type
get(pattern: string, ...args: [...Middleware[], Handler]): void;Parameters
pattern- The pattern to match against the request pathname.args- The middleware and handler to call when the route is matched.
Throws
Error- If thepatternis invalid.
Example
app.get('/', (_, res) => {
res.ok();
});With middleware:
function logger(next: Handler) {
return async (req, res) => {
console.log(`${req.method} ${req.pathname}`);
await next(req, res);
};
}
app.get('/', logger, (_, res) => {
res.ok();
});Zing.head()
Adds a HEAD route to the application.
Type
head(pattern: string, ...args: [...Middleware[], Handler]): void;Parameters
pattern- The pattern to match against the request pathname.args- The middleware and handler to call when the route is matched.
Throws
Error- If thepatternis invalid.
Example
app.head('/', (_, res) => {
res.ok();
});With middleware:
function logger(next: Handler) {
return async (req, res) => {
console.log(`${req.method} ${req.pathname}`);
await next(req, res);
};
}
app.head('/', logger, (_, res) => {
res.ok();
});Zing.patch()
Adds a PATCH route to the application.
Type
patch(pattern: string, ...args: [...Middleware[], Handler]): void;Parameters
pattern- The pattern to match against the request pathname.args- The middleware and handler to call when the route is matched.
Throws
Error- If thepatternis invalid.
Example
app.patch('/', (_, res) => {
res.ok();
});With middleware:
function logger(next: Handler) {
return async (req, res) => {
console.log(`${req.method} ${req.pathname}`);
await next(req, res);
};
}
app.patch('/', logger, (_, res) => {
res.ok();
});Zing.post()
Adds a POST route to the application.
Type
post(pattern: string, ...args: [...Middleware[], Handler]): void;Parameters
pattern- The pattern to match against the request pathname.args- The middleware and handler to call when the route is matched.
Throws
Error- If thepatternis invalid.
Example
app.post('/', (_, res) => {
res.ok();
});With middleware:
function logger(next: Handler) {
return async (req, res) => {
console.log(`${req.method} ${req.pathname}`);
await next(req, res);
};
}
app.post('/', logger, (_, res) => {
res.ok();
});Zing.put()
Adds a PUT route to the application.
Type
put(pattern: string, ...args: [...Middleware[], Handler]): void;Parameters
pattern- The pattern to match against the request pathname.args- The middleware and handler to call when the route is matched.
Throws
Error- If thepatternis invalid.
Example
app.put('/', (_, res) => {
res.ok();
});With middleware:
function logger(next: Handler) {
return async (req, res) => {
console.log(`${req.method} ${req.pathname}`);
await next(req, res);
};
}
app.put('/', logger, (_, res) => {
res.ok();
});Zing.delete()
Adds a DELETE route to the application.
Type
delete(pattern: string, ...args: [...Middleware[], Handler]): void;Parameters
pattern- The pattern to match against the request pathname.args- The middleware and handler to call when the route is matched.
Throws
Error- If thepatternis invalid.
Example
app.delete('/', (_, res) => {
res.ok();
});With middleware:
function logger(next: Handler) {
return async (req, res) => {
console.log(`${req.method} ${req.pathname}`);
await next(req, res);
};
}
app.delete('/', logger, (_, res) => {
res.ok();
});Zing.options()
Adds a OPTIONS route to the application.
Type
options(pattern: string, ...args: [...Middleware[], Handler]): void;Parameters
pattern- The pattern to match against the request pathname.args- The middleware and handler to call when the route is matched.
Throws
Error- If thepatternis invalid.
Example
app.options('/', (_, res) => {
res.ok();
});With middleware:
function logger(next: Handler) {
return async (req, res) => {
console.log(`${req.method} ${req.pathname}`);
await next(req, res);
};
}
app.options('/', logger, (_, res) => {
res.ok();
});Zing.use()
Adds an application-level middleware to be called for each incoming request regardless of whether it matches a route or not.
Type
use(...middleware: Middleware[]): void;Parameters
middleware- The middleware to be called for each request.
Example
function logger(next: Handler) {
return async (req, res) => {
console.log(`${req.method} ${req.pathname}`);
await next(req, res);
};
}
app.use(logger);Middleware with configuration:
function logger(options: { prefix: string }): Middleware {
return (next) => async (req, res) => {
console.log(`${options.prefix} ${req.method} ${req.pathname}`);
await next(req, res);
};
}
app.use(logger({ prefix: '[Zing] ' }));Zing.set404Handler()
Sets the handler to call when no route is matched.
By default, Zing has a default 404 handler that sends a 404 Not Found response. If you want to customize the response, you can set your own 404 handler.
Type
set404Handler(handler: Handler): void;Parameters
handler- The handler to call when no route is matched.
Example
app.set404Handler((_, res) => {
res.json(404, { message: 'Not Found' });
});Zing.setErrorHandler()
Sets the handler to call when an error occurs.
By default, Zing has a default error handler that sends a 500 Internal Server Error response. The default error handler also handles a few Zing specific errors:
ContentTooLargeError- When the request body is too large.
- Sends a
413 Content Too Largeresponse.
UnsupportedContentTypeError- When trying to parse a request body with an unsupported content type.
- Sends a
415 Unsupported Media Typeresponse.
MalformedJSONError- When trying to parse a request body as JSON but the body is not valid JSON.
- Sends a
422 Unprocessable Contentresponse.
InternalServerError- When an unknown error occurs.
- Sends a
500 Internal Server Errorresponse.
If you want to customize the response, you can set your own error handler.
Type
setErrorHandler(handler: ErrorHandler): void;Parameters
handler- The handler to call when an error occurs.
Example
app.setErrorHandler((_err, _req, res) => {
res.json(500, { message: 'Internal Server Error' });
});If you just want to handle a few specific errors, you can do something like the following:
app.setErrorHandler((err, _req, res) => {
if (err instanceof XXXError) {
res.json(422, { message: 'Unprocessable Content' });
return;
}
// Rethrow the error so that it can be handled by the default error handler.
throw err;
});Request
Request.node
Returns the underlying Node.js request object.
Type
node: IncomingMessage;Request.protocol
Returns the protocol of the request.
Type
protocol: 'http' | 'https';Example
app.get('/', (req, res) => {
const protocol = req.protocol;
res.ok();
});Request.pathname
Returns the pathname of the request.
Type
pathname: string;Example
app.get('/', (req, res) => {
const pathname = req.pathname;
});Request.method
Returns the method of the request.
Type
method: 'GET' | 'HEAD' | 'PATCH' | 'POST' | 'PUT' | 'DELETE' | 'OPTIONS';Example
app.get('/', (req, res) => {
const method = req.method;
});Request.get()
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.
Type
get<T = unknown>(key: string, defaultValue?: T): T | null;Parameters
key- The key to get the value of.defaultValue- The default value to return if the key is not found.
Example
app.get('/', (req, res) => {
const value = req.get('key', 'defaultValue');
});Request.set()
Stores a value in the request-scoped key-value store.
Type
set(key: string, value: unknown): void;Parameters
key- The key to store the value under.value- The value to store.
Example
app.get('/', (req, res) => {
req.set('key', 'value');
});Request.cookie()
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.
Type
cookie(name: string, defaultValue?: string): string | null;Parameters
name- The name of the cookie to get the value of.defaultValue- The default value to return if the cookie is not found.
Example
app.get('/', (req, res) => {
const value = req.cookie('name', 'defaultValue');
});Request.param()
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.
Type
param(name: string, defaultValue?: string): string | null;Parameters
name- The name of the parameter to get the value of.defaultValue- The default value to return if the parameter is not found.
Example
app.get('/', (req, res) => {
const value = req.param('name', 'defaultValue');
});Request.query()
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.
Type
query(name: string, defaultValue?: string): string | null;Parameters
name- The name of the query to get the value of.defaultValue- The default value to return if the query is not found.
Example
app.get('/', (req, res) => {
const value = req.query('name', 'defaultValue');
});Request.queries()
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, an empty array is returned.
Type
queries(name: string, defaultValue?: string[]): string[];Parameters
name- The name of the query to get the values of.defaultValue- The default value to return if the query is not found.
Example
app.get('/', (req, res) => {
const values = req.queries('name', ['defaultValue1', 'defaultValue2']);
});Request.header()
Returns the value of the first occurrence of the given header name from the request. If the header is not found and no default value is provided, null is returned.
Type
header(name: string, defaultValue?: string): string | null;Parameters
name- The name of the header to get the value of.defaultValue- The default value to return if the header is not found.
Example
app.get('/', (req, res) => {
const value = req.header('name', 'defaultValue');
});Request.body()
Returns the body of the request or null if the HTTP method is not PATCH, POST, or PUT.
Type
async body(): Promise<Result<Uint8Array | null, ContentTooLargeError | InternalServerError>>;Example
app.get('/', async (req, res) => {
const result = await req.body();
if (result.isErr()) {
res.json(500, { message: 'Internal Server Error' });
return;
}
const body = result.value; // The body of the request.
});app.get('/', async (req, res) => {
const result = await req.body();
const body = result.unwrap(); // The body of the request or throws an error if the result is an error.
const body = result.unwrapOr(null); // The body of the request or `null` if the result is an error.Request.text()
Returns the body of the request as a string or null if the HTTP method is not PATCH, POST, or PUT.
Type
async text(): Promise<Result<string | null, ContentTooLargeError | InternalServerError | UnsupportedContentTypeError>>;Example
app.get('/', async (req, res) => {
const result = await req.text();
if (result.isErr()) {
res.json(500, { message: 'Internal Server Error' });
return;
}
const text = result.value; // The body of the request as a string.
});app.get('/', async (req, res) => {
const result = await req.text();
const text = result.unwrap(); // The body of the request as a string or throws an error if the result is an error.
const text = result.unwrapOr(null); // The body of the request as a string or `null` if the result is an error.Request.json()
Returns the body of the request as a JSON object or null if the HTTP method is not PATCH, POST, or PUT.
Type
async json<T = unknown>(): Promise<Result<T | null, ContentTooLargeError | InternalServerError | UnsupportedContentTypeError | MalformedJSONError>>;Example
app.get('/', async (req, res) => {
const result = await req.json();
if (result.isErr()) {
res.json(500, { message: 'Internal Server Error' });
return;
}
const json = result.value; // The body of the request as a JSON object.
});app.get('/', async (req, res) => {
const result = await req.json();
const json = result.unwrap(); // The body of the request as a JSON object or throws an error if the result is an error.
const json = result.unwrapOr(null); // The body of the request as a JSON object or `null` if the result is an error.Response
Response.node
Returns the underlying Node.js response object.
Type
node: ServerResponse;Response.finished
Returns true if the response has been sent, otherwise false.
Type
finished: boolean;Example
app.get('/', (req, res) => {
const finished = res.finished;
});Response.status()
Sets the status code of the response.
Type
status(code: (typeof HTTPStatusCode)[keyof typeof HTTPStatusCode]): void;Parameters
code- The status code to set.
Example
app.get('/', (req, res) => {
res.status(200);
});Response.ok()
Sets the status code of the response to 200.
Type
ok(): void;Example
app.get('/', (req, res) => {
res.ok();
});Response.cookie()
Sets a cookie on the response.
Type
cookie(name: string, value: string, options?: CookieOptions): void;Parameters
name- The name of the cookie to set.value- The value of the cookie to set.options- The options for the cookie.
Example
app.get('/', (req, res) => {
res.cookie('name', 'value', {
path: '/',
maxAge: 1000 * 60 * 60 * 24 * 30, // 30 days
});
});Response.json()
Sets the status code of the response to the given status code and the body to the given JSON object. It will automatically set the Content-Type and Content-Length headers.
Type
json(code: (typeof HTTPStatusCode)[keyof typeof HTTPStatusCode], data: JSONObject): void;Parameters
code- The status code to set.data- The JSON object to set the body to.
Example
app.get('/', (req, res) => {
res.json(200, { message: 'Hello, world!' });
});Response.text()
Sets the status code of the response to the given status code and the body to the given string. It will automatically set the Content-Type and Content-Length headers.
Type
text(code: (typeof HTTPStatusCode)[keyof typeof HTTPStatusCode], data: string): void;Parameters
code- The status code to set.data- The string to set the body to.
Example
app.get('/', (req, res) => {
res.text(200, 'Hello, world!');
});Response.header()
Sets the value of the given header key on the response.
Type
header<Key extends Exclude<HTTPHeaderKey, 'set-cookie'>>(key: Key, value: HTTPHeaderValue<Key>): void;Parameters
key- The key of the header to set.value- The value of the header to set.
Example
app.get('/', (req, res) => {
res.header('Content-Type', 'application/json');
});📜 License
This project is licensed under the MIT License.
