Docs/routingv1.0.0-rc.1

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. AutoRouter does not scan on construction. Call initialize() (or use the createAutoRouter factory, which calls it for you) before the server starts accepting requests.
  • API routes vs. page routes. An API module's exported GET, POST, PUT, PATCH and DELETE functions handle those methods; its default export handles any other method. A page answers GET (and HEAD): its default export (a class is constructed, a function is called, an object is used as is) receives { params, query }, and the result's render() output is sent as HTML, inside the nearest _layout when there is one.
  • Serving. router.handler() returns (req, res, next) middleware. It matches the method and path, sets req.params, and calls next() for requests no route serves (or answers 404 without next). Errors give a 500.
  • Middleware pipeline. Each API route handler runs in a MiddlewarePipeline<HttpContext>. The _middleware files 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 their length property and adapted to the pipeline format automatically.
  • Hot reload. When hot: true is set, AutoRouter watches the pages directory via chokidar and re-scans on file add, change, or delete, importing fresh copies of the modules. This requires chokidar to 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 as tsx.
  • wrapAPIHandler creates a new MiddlewarePipeline<HttpContext> for each request; the middleware modules themselves are loaded once per scan.
  • scanAndRegisterRoutes replaces all routes on each call, so routes removed from disk disappear after the next scan.

Pitfalls

  • createAutoRouter calls initialize() which calls scanAndRegisterRoutes(). If the appDir does not contain a pages/ directory (or the configured pagesDir), the scan returns zero routes silently.
  • Hot reload requires chokidar as an optional peer dependency. If it is not installed, setupHotReload logs 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 the chokidar watcher. It does not remove registered routes.
From the canonical UDIOT public documentation.Try it in the playground ↗