burger-api
Version:
<div align="center"> <a href="https://burger-api.com"> <img src="https://github.com/user-attachments/assets/0d9b376e-1d89-479a-aa7f-e7ee3c6b2342" alt="BurgerAPI"/> </a> </div>
422 lines (421 loc) • 18 kB
JavaScript
// Import stuff from core
import { Server } from './core/server';
import { ApiRouter } from './core/api-router';
import { PageRouter } from './core/page-router';
import { generateOpenAPIDocument } from './core/openapi';
import { swaggerHtml } from './core/swagger-ui';
// Import utils
import { collectRoutes, compareRoutes } from './utils/index';
import { METHOD_NOT_ALLOWED, NOT_FOUND, OPENAPI_ERROR } from './utils/response';
import { extractPathnameFromUrl, extractWildcardParams, } from './utils/wildcard';
// Import middleware
import { createValidationMiddleware } from './middleware/validator';
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 only when using runtime scanning (no prebuilt apiRoutes)
this.apiRouter =
apiDir && !Array.isArray(options.apiRoutes)
? new ApiRouter(apiDir, apiPrefix || 'api')
: undefined;
// Initialize page router only when using runtime scanning (no prebuilt pageRoutes)
this.pageRouter =
pageDir && !Array.isArray(options.pageRoutes)
? 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() {
// Production path: use pre-built page routes (no filesystem scan)
const prebuiltPages = this.options.pageRoutes;
if (Array.isArray(prebuiltPages)) {
// Sort the prebuilt pages
const sorted = [...prebuiltPages].sort((a, b) => compareRoutes(a, b));
for (let i = 0; i < sorted.length; i++) {
const page = sorted[i];
this.routes[page.path] = page.handler;
}
return sorted.length > 0;
}
// Dev path: load from filesystem via PageRouter
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() {
// Production path: use pre-built API routes (no filesystem scan)
let apiRoutes;
if (Array.isArray(this.options.apiRoutes)) {
apiRoutes = [...this.options.apiRoutes].sort((a, b) => compareRoutes(a, b));
}
else {
// Dev path: load from filesystem via ApiRouter
if (!this.apiRouter)
return false;
await this.apiRouter.loadRoutes();
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;
// Pre-compute wildcard info once (used in all paths)
const isWildcard = route.isWildcard;
const baseSegmentCount = isWildcard
? path.split('/').filter(Boolean).length - 1
: 0;
// Create optimized route handler based on middleware count
if (totalMiddlewareCount === 0) {
// Ultra-fast path: no middleware at all
// Inline everything for maximum speed
if (isWildcard) {
// Wildcard route with no middleware
routes[path] = (request) => {
const pathname = extractPathnameFromUrl(request.url);
extractWildcardParams(request, pathname, baseSegmentCount);
const handler = handlers[request.method];
return handler
? handler(request)
: this.METHOD_NOT_ALLOWED;
};
}
else {
// Regular route with no middleware (most common case)
routes[path] = (request) => {
const handler = handlers[request.method];
return handler
? handler(request)
: this.METHOD_NOT_ALLOWED;
};
}
}
else {
// Pre-compute middleware array with exact size (AOT optimization)
const middlewares = new Array(totalMiddlewareCount);
let idx = 0;
// Copy global middleware (manual loop is faster than spread/concat)
for (let j = 0; j < globalMiddlewareLen; j++) {
middlewares[idx++] = globalMiddleware[j];
}
// Add validation middleware if needed
if (hasSchema) {
middlewares[idx++] = createValidationMiddleware(schema);
}
// Add route-specific middlewares
if (routeMiddleware) {
for (let j = 0; j < routeMiddlewareLen; j++) {
middlewares[idx++] = routeMiddleware[j];
}
}
// Create specialized handler based on middleware count
if (totalMiddlewareCount === 1) {
// Single middleware fast path (common: just CORS or just auth)
const singleMiddleware = middlewares[0];
if (isWildcard) {
routes[path] = (request) => {
const pathname = extractPathnameFromUrl(request.url);
extractWildcardParams(request, pathname, baseSegmentCount);
const handler = handlers[request.method];
if (!handler)
return this.METHOD_NOT_ALLOWED;
return this.processSingleMiddleware(request, singleMiddleware, handler);
};
}
else {
routes[path] = (request) => {
const handler = handlers[request.method];
if (!handler)
return this.METHOD_NOT_ALLOWED;
return this.processSingleMiddleware(request, singleMiddleware, handler);
};
}
}
else {
// Multiple middlewares (general case)
if (isWildcard) {
routes[path] = (request) => {
const pathname = extractPathnameFromUrl(request.url);
extractWildcardParams(request, pathname, baseSegmentCount);
const handler = handlers[request.method];
if (!handler)
return this.METHOD_NOT_ALLOWED;
return this.processMiddleware(request, middlewares, handler);
};
}
else {
routes[path] = (request) => {
const handler = handlers[request.method];
if (!handler)
return this.METHOD_NOT_ALLOWED;
return this.processMiddleware(request, middlewares, handler);
};
}
}
}
// For wildcard routes, also register the base path
// Bun's /* wildcard may not match the base path itself, so we register both
if (route.isWildcard && path.endsWith('/*')) {
const basePath = path.slice(0, -2); // Remove "/*" to get base path
// Only register base path if no static route exists (preserve priority)
// Static routes have highest priority: Static > Dynamic > Wildcard
if (!routes[basePath]) {
// No static route found, register wildcard for base path
routes[basePath] = routes[path];
}
// If static route exists, it takes priority (correct behavior)
}
}
// 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 single middleware
* @param request - The request object
* @param middleware - The middleware function
* @param handler - The handler function
* @returns A promise that resolves to a response
*/
async processSingleMiddleware(request, middleware, handler) {
const result = await middleware(request);
// Short-circuit with Response
if (result instanceof Response) {
return result;
}
// Transform response after handler
if (typeof result === 'function') {
return result(await handler(request));
}
// Continue to handler
return handler(request);
}
/**
* Process middleware and handler
*
* How it works:
* 1. Run each middleware in order
* 2. If middleware returns Response → stop and send that response (but still apply "after" functions)
* 3. If middleware returns undefined → continue to next middleware
* 4. If middleware returns function → save it to transform the final response later
* 5. After all middlewares, run the handler
* 6. Apply all saved "after" functions to the response (in reverse order)
*
* Performance optimizations:
* - Pre-allocated array for "after" functions (avoids dynamic resizing)
* - Fast paths for 0, 1, and 2 middlewares (most common cases)
* - Manual loop unrolling for small counts
* - Minimal branching in hot path
*/
async processMiddleware(request, middlewares, handler) {
const len = middlewares.length;
// Fast path: two middlewares (common: CORS + logger, or auth + logger)
if (len === 2) {
// First middleware
const result1 = await middlewares[0](request);
if (result1 instanceof Response) {
return result1;
}
// Second middleware
const result2 = await middlewares[1](request);
if (result2 instanceof Response) {
// Apply first middleware's after function if exists
if (typeof result1 === 'function') {
return result1(result2);
}
return result2;
}
// Run handler
let response = await handler(request);
// Apply after functions in reverse order (manual unroll)
if (typeof result2 === 'function') {
response = await result2(response);
}
if (typeof result1 === 'function') {
response = await result1(response);
}
return response;
}
// General path: 3+ middlewares (less common)
// Pre-allocate array with exact size to avoid dynamic resizing
const afterStack = new Array(len);
let afterCount = 0;
// Run each middleware
for (let i = 0; i < len; i++) {
const result = await middlewares[i](request);
// Short-circuit with Response (check first - most common early exit)
if (result instanceof Response) {
// Apply collected "after" functions in reverse
if (afterCount === 0)
return result;
if (afterCount === 1)
return afterStack[0](result);
// Multiple after functions
let response = result;
for (let j = afterCount - 1; j >= 0; j--) {
response = await afterStack[j](response);
}
return response;
}
// Save function for later (check once, no double typeof check)
if (typeof result === 'function') {
afterStack[afterCount++] = result;
}
// undefined - continue (implicit, no check needed)
}
// All middlewares passed - run handler
let response = await handler(request);
// Apply "after" functions in reverse order
// Fast paths for common cases
if (afterCount === 0)
return response;
if (afterCount === 1)
return afterStack[0](response);
if (afterCount === 2) {
response = await afterStack[1](response);
return afterStack[0](response);
}
// General case: 3+ after functions
for (let i = afterCount - 1; i >= 0; i--) {
response = await afterStack[i](response);
}
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 apiDir/pageDir (for dev) or apiRoutes/pageRoutes (for production builds) when initializing the Burger class.');
}
}
}
// Export utils
export { setDir } from './utils/index';