UNPKG

burger-api

Version:

<p align="center"> <img src="https://github.com/user-attachments/assets/0d9b376e-1d89-479a-aa7f-e7ee3c6b2342" alt="BurgerAPI logo"/> </p>

288 lines (287 loc) 11.3 kB
// Import stuff from core import { Server } from './core/server.js'; import { ApiRouter } from './core/api-router.js'; import { PageRouter } from './core/page-router.js'; import { generateOpenAPIDocument } from './core/openapi.js'; import { swaggerHtml } from './core/swagger-ui.js'; // Import utils import { collectRoutes } from './utils/index.js'; import { METHOD_NOT_ALLOWED, NOT_FOUND, OPENAPI_ERROR } from './utils/response'; // Import middleware import { createValidationMiddleware } from './middleware/validator.js'; export class Burger { options; /** * The server instance */ server; /** * The API router instance */ apiRouter; /** * The page router instance */ pageRouter; /** * The global middleware */ globalMiddleware = []; /** * The OpenAPI document */ openApiDoc = null; /** * The routes object */ routes = {}; /** * Pre-computed responses for reuse */ METHOD_NOT_ALLOWED = METHOD_NOT_ALLOWED; /** * The not found response */ NOT_FOUND = NOT_FOUND; /** * The OpenAPI error response */ OPENAPI_ERROR = OPENAPI_ERROR; /** * Constructor for the Burger class. * @param options - The options for the server and router. * The options object should contain the following properties: * - port: The port number to listen on. * - apiDir: The directory path to load API routes from. * - pageDir: The directory path to load page routes from. * - middleware: An array of global middleware functions. */ constructor(options) { this.options = options; // Create server instance this.server = new Server(options); // Fast initialization for routers with nullish coalescing const { apiDir, pageDir, apiPrefix, pagePrefix, globalMiddleware } = options; // Initialize API router if apiDir is provided this.apiRouter = apiDir ? new ApiRouter(apiDir, apiPrefix || 'api') : undefined; // Initialize page router if pageDir is provided this.pageRouter = pageDir ? new PageRouter(pageDir, pagePrefix || '') : undefined; // Add global middleware if any this.globalMiddleware = globalMiddleware?.length ? globalMiddleware.slice() : []; } /** * Process the page routes and add them to the routes object * @returns A promise that resolves to a boolean */ async processPageRoutes() { // If no page router, return false if (!this.pageRouter) return false; // Load pages routes await this.pageRouter.loadPages(); // If there are any page routes, add them to the routes object const pages = this.pageRouter.pages; // Get the length of the pages routes const pageCount = pages.length; // If no pages, return false if (pageCount === 0) return false; // Loop through the pages for (let i = 0; i < pageCount; i++) { // Get the current page const page = pages[i]; // Add the page to the routes this.routes[page.path] = page.handler; } // Return true if there are any pages return true; } /** * Process the API routes and add them to the routes object * @returns A promise that resolves to a boolean */ async processApiRoutes() { // If no api router, return false if (!this.apiRouter) return false; // Load API routes await this.apiRouter.loadRoutes(); // Collect API routes const apiRoutes = collectRoutes(this.apiRouter.routes); // Get the length of the API routes const routeCount = apiRoutes.length; // If there are no API routes, return false if (routeCount === 0) return false; // Generate OpenAPI document and cache it this.openApiDoc = generateOpenAPIDocument(apiRoutes, this.options); // Cache frequently accessed properties const routes = this.routes; // Get the global middleware const globalMiddleware = this.globalMiddleware; // Get the length of the global middleware const globalMiddlewareLen = globalMiddleware.length; // Process each route with optimized handler creation for (let i = 0; i < routeCount; i++) { /** * ================================================ * Pre-compute the required functionality start * ================================================ */ // Get the current route object const route = apiRoutes[i]; // Destructure the route object const { path, schema, middleware: routeMiddleware, handlers, } = route; // Get length of route specific middleware const routeMiddlewareLen = routeMiddleware?.length || 0; // Check if schema exists const hasSchema = !!schema; // Optimize middleware array initialization by pre-allocating size const totalMiddlewareCount = globalMiddlewareLen + (hasSchema ? 1 : 0) + routeMiddlewareLen; // Create optimized route handler if (totalMiddlewareCount === 0) { // Ultra-fast path: no middleware routes[path] = (request) => { const handler = handlers[request.method]; return handler ? handler(request) : this.METHOD_NOT_ALLOWED; }; } else { // Pre-compute middleware array with known size const middlewares = new Array(totalMiddlewareCount); // Initialize index to track the current middleware let index = 0; // Copy global middleware (most common case) for (let j = 0; j < globalMiddlewareLen; j++) { middlewares[index++] = globalMiddleware[j]; } // Add validation middleware if needed if (hasSchema) { middlewares[index++] = createValidationMiddleware(schema); } // Add route-specific middlewares if (routeMiddleware) { for (let j = 0; j < routeMiddlewareLen; j++) { middlewares[index++] = routeMiddleware[j]; } } /** * ================================================ * Pre-compute the required functionality end * ================================================ */ // Create handler with middleware routes[path] = (request) => { const handler = handlers[request.method]; if (!handler) return this.METHOD_NOT_ALLOWED; return this.processMiddleware(request, middlewares, handler); }; } } // Add special routes for OpenAPI routes['/openapi.json'] = () => this.openApiDoc ? Response.json(this.openApiDoc) : this.OPENAPI_ERROR; // Add special route for Swagger UI routes['/docs'] = () => new Response(swaggerHtml, { headers: { 'Content-Type': 'text/html' }, }); return true; } /** * Process the middleware and handler * @param request - The request object * @param middlewares - The middleware array * @param handler - The handler function * @returns A promise that resolves to a response */ async processMiddleware(request, middlewares, handler) { // Get the length of the middleware array const middlewareLen = middlewares.length; // Fast path: single middleware with no after functions if (middlewareLen === 1) { // Get the first middleware const result = await middlewares[0](request); // If the result is a response, return it if (result instanceof Response) { return result; } // If the result is not a function, return the handler if (typeof result !== 'function') { return handler(request); } // If the result is a function, return the result of the handler return result(await handler(request)); } // Stack to store "after" functions // Regular path with pre-allocated afterStack const afterStack = new Array(middlewareLen); // Initialize the after middleware counter let afterCount = 0; // Process "before" logic for (let i = 0; i < middlewareLen; i++) { // Get the current middleware const result = await middlewares[i](request); // If the result is a response, return it if (result instanceof Response) { return result; // Short-circuit with a response } // If the result is a function, save it to the afterStack else if (typeof result === 'function') { afterStack[afterCount++] = result; } // If undefined, proceed to next middleware } // Get response from handler let response = await handler(request); // Fast path: no after functions if (afterCount === 0) { return response; } // Fast path: single after function if (afterCount === 1) { return afterStack[0](response); } // Process "after" logic in reverse order for (let i = afterCount - 1; i >= 0; i--) { response = await afterStack[i](response); } // Return the responseF return response; } /** * Starts the server and begins listening for incoming requests. * @param port - The port number to listen on. Defaults to `4000`. * @param cb - An optional cb function to be executed when the server is listening. * @returns A Promise that resolves when the server has started listening. */ async serve(port = 4000, cb) { // Process routes in parallel if possible const [pagesConfigured, apiConfigured] = await Promise.all([ this.processPageRoutes(), this.processApiRoutes(), ]); // Flag to track if any routes were loaded const routesConfigured = pagesConfigured || apiConfigured; // If routes were configured, start the server if (routesConfigured) { // Start the server this.server.start(this.routes, async () => { return this.NOT_FOUND; }, port, cb); } else { // If no routes were configured, log an error console.error('Error: No routes configured! Please provide either apiDir with route.ts files or pageDir with html files when initializing the Burger Class.'); } } } // Export utils export { setDir } from './utils/index.js';