AutoRouter
AutoRouter discovers routes by scanning a directory tree with RouteConventionEngine and serves them through an Express/Connect-style handler (router.handler()). It extends the client-side Router, so the page routes are also available to getCurrentRoute().
Key Concepts
- Initialization.
AutoRouterdoes not scan on construction. Callinitialize()(or use thecreateAutoRouterfactory, which calls it for you) before the server starts accepting requests. - API routes vs. page routes. An API module's exported
GET,POST,PUT,PATCHandDELETEfunctions handle those methods; its default export handles any other method. A page answersGET(andHEAD): its default export (a class is constructed, a function is called, an object is used as is) receives{ params, query }, and the result'srender()output is sent as HTML, inside the nearest_layoutwhen there is one. - Serving.
router.handler()returns(req, res, next)middleware. It matches the method and path, setsreq.params, and callsnext()for requests no route serves (or answers 404 withoutnext). Errors give a 500. - Middleware pipeline. Each API route handler runs in a
MiddlewarePipeline<HttpContext>. The_middlewarefiles of its directory and the directories above it are loaded once per scan and run before the handler, outermost first. - Legacy middleware compatibility. Three-argument middleware functions (
(req, res, next)) are detected by theirlengthproperty and adapted to the pipeline format automatically. - Hot reload. When
hot: trueis set,AutoRouterwatches the pages directory viachokidarand re-scans on file add, change, or delete, importing fresh copies of the modules. This requireschokidarto be installed.
API Reference
AutoRouterOptions
interface AutoRouterOptions {
appDir: string; // absolute path to the app directory
conventions?: Partial<RoutingConventions>; // custom file/directory name conventions
hot?: boolean; // enable hot reload (default: false)
verbose?: boolean; // log discovered routes (default: false)
}
AutoRouter
class AutoRouter extends Router {
constructor(options: AutoRouterOptions)
async initialize(): Promise<void>
async scanAndRegisterRoutes(): Promise<void>
async registerConvention(convention: RouteConvention): Promise<void>
async registerAPIRoute(convention: RouteConvention, module: any): Promise<void>
registerPageRoute(convention: RouteConvention, module: any): void
wrapAPIHandler(handler: Function, middleware?: Middleware[]): (req, res) => Promise<void>
handler(): (req, res, next?) => Promise<void>
getServedRoutes(): string[] // e.g. ['GET /', 'POST /api/users']
async setupHotReload(): Promise<void>
destroy(): void
}
createAutoRouter(options)
Factory function that constructs an AutoRouter, calls initialize(), and returns the ready instance.
async function createAutoRouter(options: AutoRouterOptions): Promise<AutoRouter>
Basic Usage
import { createAutoRouter } from '@tagna/udiot';
import * as path from 'path';
const router = await createAutoRouter({
appDir: path.resolve('./apps/my-app'),
verbose: true, // logs each discovered route to the logger
});
// Serve the routes, for example from an Express app
app.use(router.handler());
Common Patterns
Generated routes from directory structure
Given this file tree:
apps/shop/
pages/
index.ts
products/
index.ts
[id].ts
api/
products.ts
products/
[id].ts
createAutoRouter will register:
| Route | Source file | Type |
|---|---|---|
/ |
pages/index.ts |
page |
/products |
pages/products/index.ts |
page |
/products/:id |
pages/products/[id].ts |
page |
/api/products |
pages/api/products.ts |
api |
/api/products/:id |
pages/api/products/[id].ts |
api |
API route module format
// pages/api/products.ts
export async function GET(req: any, res: any): Promise<void> {
const products = await productService.findAll();
res.json({ success: true, data: products });
}
export async function POST(req: any, res: any): Promise<void> {
const product = await productService.create(req.body);
res.status(201).json(product);
}
Page route module format
// pages/products/[id].ts
export default class ProductPage {
constructor(private props?: { params?: Record<string, string> }) {}
render(): string {
const id = this.props?.params?.id;
return `<h1>Product ${id}</h1>`;
}
}
Route-level middleware via _middleware.ts
// pages/api/_middleware.ts
export default async function middleware(
req: any,
res: any,
next?: () => Promise<void>
): Promise<boolean> {
if (!req.headers.authorization) {
res.status(401).json({ error: 'Unauthorized' });
return false;
}
if (typeof next === 'function') await next();
return true;
}
This file is discovered automatically and prepended to the pipeline for every route under pages/api/.
Enabling hot reload in development
import { createAutoRouter } from '@tagna/udiot';
import * as path from 'path';
const router = await createAutoRouter({
appDir: path.resolve('./apps/my-app'),
hot: process.env.NODE_ENV === 'development',
verbose: process.env.NODE_ENV === 'development',
});
process.on('SIGTERM', () => {
router.destroy(); // close the file watcher
});
Custom conventions
import { createAutoRouter } from '@tagna/udiot';
import * as path from 'path';
const router = await createAutoRouter({
appDir: path.resolve('./apps/my-app'),
conventions: {
pagesDir: 'routes',
apiDir: 'endpoints',
},
});
Integration Notes
- Route modules are loaded with dynamic
import()of their file URL, so they must be runnable JavaScript: compile TypeScript pages first, or run under a loader such astsx. wrapAPIHandlercreates a newMiddlewarePipeline<HttpContext>for each request; the middleware modules themselves are loaded once per scan.scanAndRegisterRoutesreplaces all routes on each call, so routes removed from disk disappear after the next scan.
Pitfalls
createAutoRoutercallsinitialize()which callsscanAndRegisterRoutes(). If theappDirdoes not contain apages/directory (or the configuredpagesDir), the scan returns zero routes silently.- Hot reload requires
chokidaras an optional peer dependency. If it is not installed,setupHotReloadlogs a warning and continues without watching. The router still works; it just does not auto-update on file changes. - Each hot-reload rescan imports new copies of the route modules; the old copies stay in memory until the process restarts.
destroy()only closes thechokidarwatcher. It does not remove registered routes.
From the canonical UDIOT public documentation.Try it in the playground ↗