UNPKG

exstack

Version:

A utility library designed to simplify and enhance express.js applications.

469 lines (364 loc) โ€ข 12 kB
# โšก๏ธ Exstack [![npm version](https://img.shields.io/npm/v/exstack.svg)](https://www.npmjs.com/package/exstack) [![npm downloads](https://img.shields.io/npm/dm/exstack.svg)](https://www.npmjs.com/package/exstack) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) > A lightweight, fast, and flexible utility library for **Express.js** โ€” designed to simplify development with async-safe handlers, built-in validation, standardized responses, and clean error handling. ## ๐Ÿงญ Table of Contents - [๐Ÿš€ Features](#-features) - [๐Ÿ“ฆ Installation](#-installation) - [โšก Quick Start](#-quick-start) - [๐Ÿง  Core Concepts](#-core-concepts) - [๐Ÿช„ Handler](#-handler) - [๐Ÿ“ฆ ApiRes](#-apires) - [๐Ÿšจ HttpError](#-httperror) - [โœ… HttpStatus](#-httpstatus) - [๐Ÿ” Zod Validator](#-zod-validator) - [๐Ÿงฑ Middleware](#-middleware) - [๐Ÿš€ Serve](#-serve) - [๐Ÿค Contributing](#-contributing) - [๐Ÿ“„ License](#-license) ## ๐Ÿš€ Features - ๐Ÿง  **Async-Friendly Handlers** โ€” Simplify async route logic with automatic error propagation and standardized responses. - ๐Ÿงฉ **Standardized Responses** โ€” Use `ApiRes` and `HttpError` for clean, consistent, and typed responses. - โœ… **Zod-Based Validation** โ€” Validate request body, query, and params seamlessly. - ๐Ÿงฑ **Essential Middleware** โ€” Includes `errorHandler`, `notFound`, and `poweredBy` out of the box. - ๐Ÿงพ **HttpStatus Enum** โ€” Access standardized HTTP status codes and names with clear constants. - ๐Ÿš€ **Graceful Shutdown** โ€” Built-in server with graceful shutdown support for production deployments. ## ๐Ÿ“ฆ Installation ```bash npm install exstack ``` ## โšก Quick Start ```typescript import * as z from 'zod'; import express from 'express'; import {validator} from 'exstack/zod'; import {handler, errorHandler, notFound, ApiRes} from 'exstack'; import {serve} from 'exstack/serve'; const app = express(); // Middleware app.use(express.json()); // Validation schema const schema = z.object({ name: z.string(), }); // Define routes with handler app.get( '/ping', handler(() => 'pong'), ); app.post( '/users', validator.body(schema), handler(req => { const user = req.valid<typeof schema>('body'); return ApiRes.created(user, 'User created successfully'); }), ); // Error middleware app.use(notFound('*splat')); app.use(errorHandler(process.env.NODE_ENV === 'development')); // Start server with graceful shutdown serve(app, {port: 3000}); ``` ## ๐Ÿง  Core Concepts ### ๐Ÿช„ Handler The `handler` utility wraps route logic to **automatically catch errors** and **send responses** cleanly. ```typescript import {handler, ApiRes} from 'exstack'; // Without handler (classic) app.get('/user/:id', async (req, res, next) => { try { const user = await getUserById(req.params.id); res.status(200).json(user); } catch (err) { next(err); } }); // With handler (cleaner) app.get( '/user/:id', handler(async req => { const user = await getUserById(req.params.id); return ApiRes.ok(user, 'User fetched successfully'); }), ); ``` ### ๐Ÿ“ฆ ApiRes `ApiRes` standardizes and simplifies success response formatting. ```typescript app.get( '/user', handler(() => ApiRes.ok({name: 'John Doe'}, 'User found')), ); app.post( '/user', handler(req => { const newUser = createUser(req.body); return ApiRes.created(newUser, 'User created'); }), ); // Chainable example app.post( '/user', handler(req => { const newUser = createUser(req.body); return ApiRes.status(200).msg('User created').data(newUser); }), ); ``` **Available Methods:** | Method | Description | | --------------------------------------- | -------------------------- | | `ApiRes.ok(data, message)` | 200 OK response | | `ApiRes.created(data, message)` | 201 Created response | | `ApiRes.paginated(data, meta, message)` | Paginated success response | | `.status(code)` | Chainable status setter | | `.msg(message)` | Chainable message setter | | `.data(data)` | Chainable data setter | ### ๐Ÿšจ HttpError The `HttpError` class provides a **consistent and structured way to handle HTTP errors**. ```typescript import {HttpError, HttpStatus} from 'exstack'; app.get( '*', handler((req, res) => new HttpError(HttpStatus.NOT_FOUND, { message: 'Not Found', }).toJson(res), ), ); app.post( '/example/:id', handler(req => { if (!req.params.id) throw new BadRequestError('Id is required'); }), ); ``` **Extended Options:** ```typescript const err = new HttpError(400, { message: 'Validation Error', meta: { username: 'Username is required', password: 'Password is required', }, cause: new Error('Invalid input'), }); ``` > _If no custom name is provided, `HttpError` automatically assigns one based on the status code._ #### Common Errors: - `BadRequestError` - `UnauthorizedError` - `NotFoundError` - `ConflictError` - `ForbiddenError` - `PaymentRequiredError` - `NotImplementedError` - `InternalServerError` - `ContentTooLargeError` #### `HttpError.isHttpError(value)` Check whether a value is an instance of `HttpError`. ```typescript // If it is an HttpError, send a JSON response with the error details if (HttpError.isHttpError(err)) return err.toJson(res); else // If it's not an HttpError, pass it to the next middleware for further handling next(err); ``` #### Custom Error Handler Example ```typescript export const errorHandler: ErrorRequestHandler = (err, req, res, next) => { // Handle known HttpError instances if (HttpError.isHttpError(err)) { // Log the cause if it exists if (err.options.cause) console.error('HttpError Cause:', err.options.cause); return err.toJson(res); } // Write unknown errors if a write function is provided console.error('Unknown Error:', err); // Standardized error response for unknown exceptions const unknown = { status: HttpStatus.INTERNAL_SERVER_ERROR, error: 'InternalServerError', message: isDev ? err.message || 'Unexpected error' : 'Something went wrong', stack: isDev ? err.stack : undefined, }; res.status(unknown.status).json(unknown); }; ``` ### โœ… HttpStatus `HttpStatus` provides readable constants for all standard HTTP status codes. ```typescript import {HttpStatus} from 'exstack'; // Example: Basic usage in a route app.get('/status-example', (req, res) => { res.status(HttpStatus.OK).json({message: 'All good!'}); }); // Example: Custom error handling middleware app.use((req, res) => { res.status(HttpStatus.NOT_FOUND).json({ error: 'Resource not found', }); }); // Example: Response with a 201 Created status app.post('/create', (req, res) => { const resource = createResource(req.body); res.status(HttpStatus.CREATED).json({ message: 'Resource created successfully', data: resource, }); }); ``` ### Commonly Used HTTP Status Codes: - **2xx: Success** - `HttpStatus.OK`: 200 โ€” Request succeeded. - `HttpStatus.CREATED`: 201 โ€” Resource created. - `HttpStatus.ACCEPTED`: 202 โ€” Request accepted for processing. - `HttpStatus.NO_CONTENT`: 204 โ€” No content to send. - and more .... - **3xx: Redirection** - `HttpStatus.MOVED_PERMANENTLY`: 301 โ€” Resource moved permanently. - `HttpStatus.FOUND`: 302 โ€” Resource found at another URI. - `HttpStatus.NOT_MODIFIED`: 304 โ€” Resource not modified. - and more .... - **4xx: Client Error** - `HttpStatus.BAD_REQUEST`: 400 โ€” Bad request. - `HttpStatus.UNAUTHORIZED`: 401 โ€” Authentication required. - `HttpStatus.FORBIDDEN`: 403 โ€” Access forbidden. - `HttpStatus.NOT_FOUND`: 404 โ€” Resource not found. - and more .... - **5xx: Server Error** - `HttpStatus.INTERNAL_SERVER_ERROR`: 500 โ€” Internal server error. - `HttpStatus.NOT_IMPLEMENTED`: 501 โ€” Not implemented. - `HttpStatus.SERVICE_UNAVAILABLE`: 503 โ€” Service unavailable. - and more .... ### ๐Ÿ” Zod Validator The `validator` middleware provides an easy way to validate incoming requests using Zod schemas. It can validate the request `body`, `query`, `params` and `all`. ### Installation ```bash # node runtime npm install zod # bun runtime bun install zod ``` ### Examples ```typescript import * as z from 'zod'; import {validator} from 'exstack/zod'; const createUserSchema = z.object({ email: z.string().email(), password: z.string().min(6), }); app.post( '/users', validator.body(createUserSchema), handler(req => { const body = req.valid('body'); // body is guaranteed to match the schema return ApiRes.created(body, 'User created'); }), ); ``` ```typescript app.post( '/users', validator.body(createUserSchema), handler(req => { // Option 1: Automatically inferred from schema const user = req.valid('body'); // ^? { name: string; email: string } // Option 2: Explicitly infer from schema const user2 = req.valid<typeof createUserSchema>('body'); // ^? z.infer<typeof createUserSchema> // Option 3: Manually provide a type if needed const user3 = req.valid<{name: string; email: string}>('body'); // ^? { name: string; email: string } return ApiRes.created(user, 'User created successfully'); }), ); // Multi-part Validation Example const multiSchema = { body: z.object({name: z.string()}), query: z.object({page: z.string().optional()}), params: z.object({id: z.string().uuid()}), }; app.put( '/users/:id', validator.all(multiSchema), handler(req => { const result = req.valid('all'); return ApiRes.ok(result); }), ); ``` ### ๐Ÿงฑ Middleware #### ๐Ÿ› ๏ธ errorHandler Handles `HttpError` and unknown exceptions with standardized JSON output. ```typescript import {errorHandler} from 'exstack'; app.use(errorHandler(process.env.NODE_ENV === 'development')); ``` #### ๐Ÿšซ notFound Automatically throws a 404 for unmatched routes. ```typescript app.use(notFound('*splat')); ``` #### โš™๏ธ poweredBy Adds an `X-Powered-By` header to responses. ```typescript app.use(poweredBy('Exstack')); ``` ### ๐Ÿš€ Serve Start your Express app or HTTP/HTTPS server with built-in graceful shutdown support. ```typescript import express from 'express'; import {serve} from 'exstack/serve'; const app = express(); // ... configure routes serve(app, {port: 3000}); ``` **Features:** - โœ… Graceful shutdown on SIGINT/SIGTERM - โœ… Countdown timer with force close option - โœ… Support for HTTP, HTTPS, and HTTP2 servers - โœ… Auto-disabled in CI/TEST environments **Options:** ```typescript serve(app, { port: 3000, // Default: 3000 or PORT env host: 'localhost', // Default: 'localhost' or HOST env silent: false, // Suppress startup logs gracefulShutdown: true, // true | false | number (timeout in seconds) }); // Examples: serve(app, {gracefulShutdown: 10}); // 10 second timeout serve(app, {gracefulShutdown: 0}); // Disable (same as false) serve(app, {gracefulShutdown: false}); // Disable ``` **HTTPS Example:** ```typescript import express from 'express'; import https from 'node:https'; import fs from 'node:fs'; import {serve} from 'exstack/serve'; const app = express(); const httpsServer = https.createServer( { cert: fs.readFileSync('./cert.pem'), key: fs.readFileSync('./key.pem'), }, app, ); serve(httpsServer, {port: 443}); ``` **Graceful Shutdown Behavior:** - Press `Ctrl+C` โ†’ Server stops accepting new connections and waits for active requests - Shows countdown timer (default 5 seconds) - Press `Ctrl+C` again โ†’ Force close immediately - After timeout โ†’ Automatically force closes all connections ## ๐Ÿค Contributing Contributions are welcome! Please open an issue or submit a pull request to help improve **Exstack**. ## ๐Ÿ“„ License Licensed under the [MIT License](./LICENSE).