At first glance, directory-based routing looks more verbose than file-based alternatives. api/users/[id]/index.ts vs api/users/[id].ts - the extra folder seems unnecessary. It isn't, and the reason becomes obvious as your project grows.
The File-Based Routing Problem
In file-based routing, route handlers and helper files live side by side:
api/
users/
index.ts -> Handler for /users
[id].ts -> Handler for /users/:id
schema.ts -> Validation schemas... for which route?
auth.ts -> Authorization... for which endpoint?
utils.ts -> Helpers... used by what?Which files are route handlers? Which are helpers? Is schema.ts a route at /users/schema or a shared validation file? You can't tell without opening each file or relying on team conventions.
Directory-Based Clarity
With directory-based routing, the rule is simple: only index.ts is a route handler. Everything else in the folder is a helper for that route.
api/
users/
index.ts -> Handler for /users
schema.ts -> Obviously a helper for /users
[id]/
index.ts -> Handler for /users/:id
permissions.ts -> Obviously a helper for this endpoint
posts/
index.ts -> Handler for /users/:id/posts
formatter.ts -> Obviously post-specific logicNo conventions to memorize, no ambiguity. The folder tree is your API map - every folder with an index.ts is a route, everything else is support code.
This scales naturally:
api/
products/
index.ts
[id]/
index.ts
cache.ts
pricing.ts
reviews/
index.ts
moderation.ts
[reviewId]/
index.ts
flags.tsEach route's complexity is isolated in its own folder. New developers understand the structure immediately. Six months later, you can still navigate it without re-reading the codebase.
Helpers shared by several routes
The same rule answers this one: nothing is scanned except index.* and use.ts, so everything else is yours to place. Two spots follow from the tree itself:
api/
helpers.ts -> used across the whole api tree
products/
pricing.ts -> used by products and everything under it
index.ts
[id]/
index.ts
cache.ts -> used by this route onlyPut it in the deepest folder that covers every route using it, and move it up when that stops being true. A helper's position then tells you its reach, the same way a route's position tells you its URL.
For code with no natural place in the route tree - a mailer, a queue client, a domain model - use the folder root and reach it through ~/, which is what the examples in these docs do with ~/types/.
One more step up: code shared by several source folders goes outside src/ entirely and is reached through @/. Same idea, one level higher - the deepest place that covers everyone who needs it. Path mappings ›
The Trade-off
You create a folder even when it only contains index.ts. That's the entire cost.
In return: zero ambiguity, natural colocalization, room to grow without restructuring, and a folder tree that directly mirrors your API surface:
$ tree -d src/front/api
src/front/api/
└── shop
├── cart
├── [category]
│ └── {productId}
├── checkout
│ ├── confirm
│ ├── payment
│ └── shipping
├── orders
│ └── [orderId]
└── products
└── {category}