Route Conventions
RouteConventionEngine scans a directory tree and converts its file structure into an array of RouteConvention objects. These conventions are then consumed by AutoRouter to register live route handlers.
Key Concepts
- Pages directory. By default,
RouteConventionEnginelooks for apages/subdirectory inside the app directory. All.ts,.tsx,.js, and.jsxfiles within it become routes. - File-to-route mapping. File names map to URL segments.
index.tsfiles resolve to the directory path. Dynamic segments use the[param]bracket syntax. Catch-all segments use[...param]. - Route type. Files inside
pages/api/(the configurableapiDir, as the first directory underpages/) becometype: 'api'routes. All other files, includingpages/capital.ts, becometype: 'page'routes. - Layout discovery.
findLayoutwalks up the directory tree from each route file until it finds a_layout.tsorlayout.tsfile. The nearest layout wins. - Middleware discovery.
findMiddlewarecollects all_middleware.tsfiles from the route's directory up to the pages root, ordering them from outermost to innermost. - Sort order. Routes are sorted by specificity: static routes before dynamic ones, longer paths before shorter ones, then alphabetically.
API Reference
RouteConvention
interface RouteConvention {
path: string; // resolved URL (e.g., /users/:id)
filePath: string; // absolute path to source file
type: 'page' | 'api'; // route category
params: string[]; // dynamic parameter names
layout?: string; // absolute path to layout file
middleware?: string[]; // absolute paths to middleware files (outer first)
}
RoutingConventions
Configuration object passed to the engine constructor:
interface RoutingConventions {
pagesDir: string; // default: 'pages'
apiDir: string; // default: 'api'
layoutFiles: string[]; // default: ['_layout.ts', 'layout.ts']
middlewareFiles: string[]; // default: ['_middleware.ts']
indexFiles: string[]; // default: ['index.ts', 'index.tsx']
dynamicPattern: RegExp; // default: /\[([^\]]+)\]/
catchAllPattern: RegExp; // default: /\[\.\.\.([^\]]+)\]/
}
RouteConventionEngine
class RouteConventionEngine {
constructor(conventions?: Partial<RoutingConventions>)
async scanRoutes(appDir: string): Promise<RouteConvention[]>
fileToRoute(filePath, pagesPath, appDir): RouteConvention | null
pathToRoute(filePath: string): string
extractParams(filePath: string): string[]
determineRouteType(filePath: string): 'page' | 'api'
findLayout(filePath, appDir): string | undefined
findMiddleware(filePath, appDir): string[]
sortRoutes(routes: RouteConvention[]): RouteConvention[]
isSpecialFile(filePath: string): boolean
}
createRouteConventions(conventions?)
Factory function that returns a RouteConventionEngine with optional overrides.
function createRouteConventions(
conventions?: Partial<RoutingConventions>
): RouteConventionEngine
describeRoutingConventions()
Returns a multi-line string describing the routing conventions in a human-readable format.
function describeRoutingConventions(): string
File-to-Route Mapping Rules
File path (relative to pages/) |
URL route |
|---|---|
index.ts |
/ |
about.ts |
/about |
users/index.ts |
/users |
users/[id].ts |
/users/:id |
users/[id]/edit.ts |
/users/:id/edit |
blog/[...slug].ts |
/blog/* |
api/users.ts |
/api/users (type: 'api') |
api/users/[id].ts |
/api/users/:id (type: 'api') |
Special files that are not converted to routes:
_layout.ts,layout.ts_middleware.ts_app.ts,_document.ts,_error.ts
Basic Usage
import { createRouteConventions } from '@tagna/udiot';
import * as path from 'path';
const engine = createRouteConventions();
const appDir = path.resolve('./apps/my-app');
const routes = await engine.scanRoutes(appDir);
for (const route of routes) {
console.log(`${route.type.toUpperCase().padEnd(4)} ${route.path}`);
if (route.params.length > 0) {
console.log(` params: ${route.params.join(', ')}`);
}
}
Common Patterns
Directory structure and the routes it produces
apps/my-app/
pages/
index.ts → GET /
about.ts → GET /about
_layout.ts (layout for all pages, not a route)
users/
index.ts → GET /users
[id].ts → GET /users/:id
[id]/
edit.ts → GET /users/:id/edit
posts.ts → GET /users/:id/posts
_middleware.ts (applied to /users/* routes)
api/
users.ts → API /api/users
users/
[id].ts → API /api/users/:id
Custom conventions (different page directory name)
import { createRouteConventions } from '@tagna/udiot';
const engine = createRouteConventions({
pagesDir: 'routes', // use 'routes/' instead of 'pages/'
apiDir: 'handlers', // API files inside 'routes/handlers/'
indexFiles: ['index.ts', 'route.ts'],
});
Inspecting special-file detection
const engine = createRouteConventions();
console.log(engine.isSpecialFile('/app/pages/_middleware.ts')); // true
console.log(engine.isSpecialFile('/app/pages/users/index.ts')); // false
Integration Notes
scanRoutesusesfs.promises.readdirwithwithFileTypes: true, so it requires Node.js 10.10+.findLayoutandfindMiddlewareusefs.existsSyncfor each candidate file. In large directory trees this can involve many synchronous filesystem checks. Consider caching the result.- The
catchAllPatternis applied beforedynamicPatternto avoid[...slug]being partially matched by the dynamic segment regex. The order of substitution is: catch-all first, then single-param.
Pitfalls
- Windows backslash path separators are normalized to forward slashes inside
pathToRoute. However,filePathvalues inRouteConvention.filePathretain the OS-native separator. Pass absolute paths toimport()to avoid resolution issues. - Files whose names start with
_are never routes: besides_layout,_middleware,_app,_documentand_error, use the prefix for helpers kept next to pages. Layout and middleware files are found as.ts,.jsor.mjs. - Route sorting places static routes before dynamic ones within the same depth. Two static routes at the same depth are sorted alphabetically. Ensure your route handlers do not depend on a different priority order.
From the canonical UDIOT public documentation.Try it in the playground ↗