Appearance
File-System Auto-Routing
Map directory structure to URL routes automatically. File paths become route paths; [param] directories become Express :param segments. Convention over configuration — no manual route registration.
When to Use
- Building APIs where routes mirror a directory layout
- Reducing boilerplate route registration
- Wanting Next.js / Nuxt-style file routing in plain Express
- Projects that benefit from discoverable routes (the file tree is the route table)
The Pattern
Scan a directory for handler files, convert each file path to a URL path, and mount it on an Express app. Mount the auto-router on an Express app by pointing it at a directory:
javascript
import express from "express";
import { autoRouter } from "./auto-router.js";
import { resolve } from "node:path";
const app = express();
const apiDir = resolve(import.meta.dirname, "api");
app.use(autoRouter(apiDir));
app.listen(3000);Pass a custom glob pattern to mount only specific file types:
javascript
// Only mount JavaScript files
app.use(autoRouter(apiDir, {
pattern: "**/*.js",
}));Implementing autoRouter()
The autoRouter function scans for handler files, converts paths to routes, and returns a configured Express app:
javascript
import { join, parse, relative } from "node:path";
import { globSync } from "glob";
import express from "express";
const defaults = {
pattern: "**/*.js",
};
export function autoRouter(dir = process.cwd(), options) {
options = { ...defaults, ...options };
const pattern = join(dir, options.pattern);
const filePaths = globSync(pattern);
const app = express();
for (const filePath of filePaths) {
const browserPath = toBrowserPath(filePath, dir);
const handler = await import(filePath);
app.all(browserPath, handler.default ?? handler);
}
return app;
}Mapping convention
api/
├── index.js → /
├── login.js → /login
└── accounts/
├── index.js → /accounts/
└── [id]/
└── index.js → /accounts/:id/index.jsmaps to the directory path- Named files map to their basename (no extension)
[param]directories become:paramroute segments
Each handler file exports a function that receives the standard Express request and response objects:
javascript
// api/accounts/[id]/index.js
export default function (req, res) {
const { id } = req.params;
res.json({ id, name: `Account ${id}` });
}Path conversion
The toBrowserPath pipeline transforms a filesystem path into a URL route:
javascript
function toBrowserPath(filePath, dir) {
let result = fileSystemToBrowserPath(filePath, dir);
result = toForwardSlashes(result);
result = addRouteParams(result);
result = removeIndexAndExtension(result);
return result;
}
function fileSystemToBrowserPath(filePath, root) {
const rel = relative(root, filePath);
return join("/", rel);
}
function toForwardSlashes(path) {
return String(path).replace(/\\/g, "/");
}
function addRouteParams(path) {
const parsed = parse(path);
parsed.dir = parsed.dir
.split("/")
.map((segment) => {
const match = /\[(?<name>.*)\]/.exec(segment);
return match ? `:${match.groups.name}` : segment;
})
.join("/");
return join(parsed.dir, parsed.base);
}
function removeIndexAndExtension(path) {
const parsed = parse(path);
if (parsed.base === "index.js") {
return join(parsed.dir, "/");
}
return join(parsed.dir, parsed.name);
}A parameterized index file traces through all four steps:
Input: /Users/me/project/api/accounts/[id]/index.js
Root: /Users/me/project/api
Step 1: /accounts/[id]/index.js
Step 2: /accounts/[id]/index.js (no-op on Unix)
Step 3: /accounts/:id/index.js
Step 4: /accounts/:id/A named file skips the parameter step and strips the extension:
Input: /Users/me/project/api/login.js
Root: /Users/me/project/api
Step 1: /login.js
Step 4: /loginOn Windows, the slash normalization step is essential:
Input: C:\project\api\users\[id]\profile.js
Root: C:\project\api
Step 1: \users\[id]\profile.js
Step 2: /users/[id]/profile.js
Step 3: /users/:id/profile.js
Step 4: /users/:id/profileTrade-offs
| Approach | Pros | Cons |
|---|---|---|
| FS routing | Discoverable, zero config | Implicit |
| Manual routes | Explicit, greppable | Boilerplate |
| Config object | Centralized | Indirection |
Works best for CRUD APIs with regular structure. For complex routing (regex, overlapping params), manual registration may be clearer. Zero-config defaults (process.cwd(), **/*.js) keep setup minimal.