UNPKG

@crumbjs/core

Version:

<img src="https://raw.githubusercontent.com/tuplescompany/crumbjs/refs/heads/main/logo/crumbjs.png" alt="CrumbJS Logo" width="200"/> - The tasty way to build fast apis.

259 lines (258 loc) 9.87 kB
import { Router } from './router'; import { createProxyHandler } from './helpers/proxy'; import { asArray } from './helpers/utils'; export class App { #prefix = ''; #tags = []; #hide = false; routes = []; middlewares = []; // global middleware holds all the App instance (bubble up) global middlewares and apply it to all routes on router build globalMiddlewares = {}; onStartTriggers = {}; prefix(prefix) { this.#prefix = prefix; return this; } /** * Asign tag(s) to all App routes, to add more dan one: * @example * ```ts * app.tag('tag1').tag('tag2').tag('tag3'); // the 3 tags will be assigned to all the app routes * ``` * */ tag(tag) { this.#tags.push(tag); return this; } /** Hides all App routes from Openapi */ hide() { this.#hide = true; return this; } getPrefix() { return this.#prefix; } getRoutes() { return this.routes; } onStart(fn, name = 'default') { this.onStartTriggers[name] = fn; return this; } getStartupTriggers() { return this.onStartTriggers; } getGlobalMiddlewares() { return this.globalMiddlewares; } /** * useGlobal force to apply the middleware in all routes even if is within a child App instance. * * Usefull for create App instance plugin-like solution that includes middlewares and routes, and maybe onStartupTriggers * * **Important** No need to set global middleware at root app, all middlewares in root App instance are global by default. */ useGlobal(middleware, name) { this.globalMiddlewares[name] = middleware; return this; } /** * Mounts a middleware function or another {@link App} instance onto the current application. * * - If a **Middleware** is provided: * The function is added to the list of app middlewares. These run for * every request before route-specific middlewares and handlers. * * - If another **App** instance is provided: * - All of its routes are merged into the current app, with this app's prefix * automatically prepended to the child app's route paths. * - All of its static routes are also merged, with prefixes applied. * - All of its middlewares are appended to the each child app routes * - Its `onStart` triggers are merged into the current app's triggers * (overwriting by name to avoid duplication). * * This method is useful for: * - Composing large applications from smaller sub-apps (modular architecture). * - Sharing reusable route/middleware groups across projects. * - Applying global cross-cutting middleware. * * @param usable - Either: * - A {@link Middleware} function to run on every request. * - Another {@link App} instance whose routes, statics, middlewares, * and startup triggers will be merged into this one. * * @returns The current {@link App} instance for chaining. * * @example * // Mount a global middleware * app.use(loggerMiddleware); * * // Mount a sub-application with its own routes * app.use(apiApp); */ use(usable) { if (usable instanceof App) { for (const child of usable.getRoutes()) { // Add child App scoped middleware to route if ('config' in child) { child.config.use = [...usable.getMiddlewares(), ...asArray(child.config.use)]; } child.pathParts = [this.getPrefix(), ...child.pathParts]; this.routes.push(child); } // bubble up global middlewares this.globalMiddlewares = { ...this.globalMiddlewares, ...usable.getGlobalMiddlewares(), }; // Avoid duplication with name index this.onStartTriggers = { ...usable.getStartupTriggers(), ...this.onStartTriggers, // father wins }; } else { this.middlewares.push(usable); } return this; } getMiddlewares() { return this.middlewares; } add(method, path, handler, config) { let methods; if (Array.isArray(method)) { methods = method; } else if (method === '*') { methods = ['POST', 'GET', 'DELETE', 'PATCH', 'PUT', 'OPTIONS', 'HEAD']; } else { methods = [method]; } for (const m of methods) { // shallow-clone config to avoid repeating middlewares on app mounting on multi method cases const cfg = config ? { ...config, use: asArray(config.use) } : {}; // App instance openapi settings inheritance // Only here in the scoped add() - never in use() // Routes inherit tags from App instance if (this.#tags.length) cfg.tags = [...this.#tags, ...asArray(cfg.tags)]; if (this.#hide) cfg.hide = true; this.routes.push({ pathParts: [this.getPrefix(), path], method: m, handler, config: cfg, }); } return this; } /** * Fowards all the trafic from the localPath to dest url. * OpenAPI + validation are **disabled** for these routes. But you still can use middleware(s) * * Behavior: * - Same forwarding rules as `proxy` (prefix handling, headers/body streaming). * - Registers the route as openapi-hidden (`{ hide: true }`). * * @param methods HTTP method(s) or `'*'` for all. * @param localPath Local mount point with all subtrees. * @param dest Upstream base URL. * * @example * proxyAll('/v1', 'https://api.example.com'); // eg. '/v1/auth' will be fowarded to * proxyAll('/v2', 'https://new-api.example.com'); // eg. '/v2/orders' will be fowarded to */ proxyAll(localPath, dest, use) { const ensureWildcard = !localPath.endsWith('/*') ? localPath.concat('/*') : localPath; return this.add('*', ensureWildcard, createProxyHandler(localPath, dest), { use, hide: true }); } /** * Mount a transparent route-2-route proxy, keeping route config (with optional validation + OpenAPI) intact. * * Behavior: * - If `localPath` ends with `/*`, will thrown an error (route-2-route cannot use /* wildcard) * - Forwards method, path, query, headers, and body. * - Strips hop-by-hop headers; forces `Accept-Encoding: identity`. * - Streams request/response; recalculates length/encoding headers. * * @param method One HTTP method (e.g. 'GET'). * @param localPath Local mount point (`/*` proxies a subtree). * @param dest Upstream base URL (e.g. https://api.example.com). * @param config Route config (middlewares, validation, OpenAPI). * * @example * proxy('POST', '/auth', 'https://auth-ms.example.com/v1/auth', { body: authSchema }); */ proxy(method, localPath, dest, config) { if (localPath.endsWith('/*')) { const suggestPath = localPath.replace('/*', ''); const proxyAllExample = `app.proxyAll('${suggestPath}', '${dest}', middlewares)`; throw new Error(`Invalid path '${localPath}': single-method proxy cannot use '/*'. Use '${suggestPath}' for exact match, or use '${proxyAllExample}' for prefix forwarding.`); } return this.add(method, localPath, createProxyHandler(localPath, dest), config); } /** * Registers static string or blob (Bun.file) content to be served at a specific route path. * * ⚡ Performance: Bun caches static paths at server start and serves them via a * zero-overhead fast path (ref {@link https://bun.com/docs/api/http#static-responses}). Middlewares are **not** * invoked for these requests. * * @param path - The request path where the content will be served (relative to the current prefix, if any). * @param content - The string content to serve. * @param type - The Content-Type to send with the response * @returns The current instance (for chaining). */ static(path, content, type) { // Allways GET this.routes.push({ pathParts: [this.getPrefix(), path], content, contentType: type, }); return this; } /** Register route on multiple or all methods (with *) */ on(methods, path, handler, config) { return this.add(methods, path, handler, config); } /** Register a GET route */ get(path, handler, config) { return this.add('GET', path, handler, config); } /** Register a POST route */ post(path, handler, config) { return this.add('POST', path, handler, config); } /** Register a PUT route */ put(path, handler, config) { return this.add('PUT', path, handler, config); } /** Register a PATCH route */ patch(path, handler, config) { return this.add('PATCH', path, handler, config); } /** Register a DELETE route */ delete(path, handler, config) { return this.add('DELETE', path, handler, config); } /** Register a OPTIONS route */ options(path, handler, config) { return this.add('OPTIONS', path, handler, config); } /** Register a HEAD route */ head(path, handler, config) { return this.add('HEAD', path, handler, config); } /** * Builds the Bun.Server */ serve(config) { const router = new Router(this); return router.serve(config ?? undefined); } }