UNPKG

@quiltjs/quilt

Version:

Lightweight, type-safe handler and router abstraction for Node HTTP servers.

168 lines 5.57 kB
/* eslint-disable @typescript-eslint/no-explicit-any */ import http from 'node:http'; import { createHandler, } from '../Handler.js'; /** * Minimal adapter that implements the ServerEngineAdapter interface using * Node's built-in `http` module. * * It provides basic routing with support for: * - Static paths, e.g. `/status` * - Parameterized paths, e.g. `/users/:id/orders/:orderId` * - JSON request bodies (when `content-type` includes `application/json`) */ export class NodeHttpEngineAdapter { routes = []; server; port; get(path, handler) { this.routes.push({ method: 'GET', path, handler }); } post(path, handler) { this.routes.push({ method: 'POST', path, handler }); } put(path, handler) { this.routes.push({ method: 'PUT', path, handler }); } patch(path, handler) { this.routes.push({ method: 'PATCH', path, handler }); } delete(path, handler) { this.routes.push({ method: 'DELETE', path, handler }); } options(path, handler) { this.routes.push({ method: 'OPTIONS', path, handler }); } head(path, handler) { this.routes.push({ method: 'HEAD', path, handler }); } listen(port, callback) { // Close any existing server before starting a new one if (this.server) { this.server.close(); this.server = undefined; this.port = undefined; } const server = http.createServer(async (req, res) => { const method = (req.method || 'GET').toUpperCase(); const url = req.url ?? '/'; const requestUrl = new URL(url, 'http://localhost'); const pathname = requestUrl.pathname; const match = this.findRoute(method, pathname); if (!match) { res.statusCode = 404; res.setHeader('content-type', 'text/plain; charset=utf-8'); res.end('Not Found'); return; } const params = match.params; const query = Object.fromEntries(requestUrl.searchParams.entries()); const body = await this.readBody(req); const nodeReq = Object.assign(req, { params, query, body, }); await match.route.handler(nodeReq, res); }); this.server = server; server.listen(port, () => { const address = server.address(); if (typeof address === 'object' && address !== null) { this.port = address.port; } if (callback) { callback(); } }); } getPort() { return this.port; } async close() { if (!this.server) { return; } const server = this.server; this.server = undefined; this.port = undefined; await new Promise((resolve, reject) => { server.close((err) => { if (err) { reject(err); } else { resolve(); } }); }); } findRoute(method, path) { for (const route of this.routes) { if (route.method !== method) continue; const params = this.matchPath(route.path, path); if (params) { return { route, params }; } } return null; } matchPath(routePath, actualPath) { const routeSegments = routePath.split('/').filter(Boolean); const pathSegments = actualPath.split('/').filter(Boolean); if (routeSegments.length !== pathSegments.length) { return null; } const params = {}; for (let i = 0; i < routeSegments.length; i++) { const routeSegment = routeSegments[i]; const pathSegment = pathSegments[i]; if (routeSegment.startsWith(':')) { const key = routeSegment.slice(1); params[key] = decodeURIComponent(pathSegment); } else if (routeSegment !== pathSegment) { return null; } } return params; } readBody(req) { return new Promise((resolve, reject) => { const chunks = []; req.on('data', (chunk) => { chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : Buffer.from(chunk)); }); req.on('end', () => { if (chunks.length === 0) { resolve(undefined); return; } const raw = Buffer.concat(chunks).toString('utf8'); const contentType = req.headers['content-type']; if (typeof contentType === 'string' && contentType.includes('application/json')) { try { resolve(JSON.parse(raw)); } catch { resolve(raw); } } else { resolve(raw); } }); req.on('error', (err) => { reject(err); }); }); } } export function createNodeHttpRouteHandler({ execute, dependencies, }) { return createHandler({ dependencies, execute: (ctx, deps) => execute(ctx, deps), }); } //# sourceMappingURL=NodeHttpEngineAdapter.js.map