Routing
Routa routing is filesystem-based. The location of a file under src/routes determines
its URL, and the file owns every HTTP method for that URL. There is no route registry to
maintain and no import order that affects matching.
src/routes/users/route.ts -> /userssrc/routes/users/$userId/route.ts -> /users/:userIdsrc/routes/(admin)/audit/route.ts -> /auditsrc/routes/users.$userId.posts.ts -> /users/:userId/postsHow It Works
Section titled “How It Works”Routa walks src/routes, converts each route file’s location into a list of segments, and
applies three rules to those segments: $name becomes the dynamic parameter :name,
(name) is removed from the URL and recorded as a group, and everything else is used
literally. The resulting path is what you pass to createRouteRoot(), and Routa reports a
diagnostic when the two disagree.
File Conventions
Section titled “File Conventions”Directory Style
Section titled “Directory Style”A folder per segment, with the endpoint in route.ts. This is what the scaffolder
generates and what the examples use.
Directorysrc/routes/
Directoryusers/
- route.ts
/users Directory$userId/
- route.ts
/users/:userId Directoryposts/
- route.ts
/users/:userId/posts
- route.ts
- route.ts
- route.ts
Flat Style
Section titled “Flat Style”Dots in a filename separate segments. Useful for shallow endpoints where a folder per segment adds more nesting than it is worth.
Directorysrc/routes/
- users.ts
/users - users.$userId.ts
/users/:userId - users.$userId.posts.ts
/users/:userId/posts
- users.ts
Both styles can be mixed in one project, and a flat file can live inside a directory:
src/routes/admin/audit.$eventId.ts serves /admin/audit/:eventId.
Dynamic Segments
Section titled “Dynamic Segments”A segment beginning with $ becomes a path parameter. $userId produces :userId, which
is the key you receive in input.params after parsing:
import { createRoute, createRouteRoot } from "@routa-ts/core";import { z } from "zod";
const route = createRouteRoot("/users/:userId");
export default route({ get: createRoute({ input: { params: z.object({ userId: z.string() }), }, // ... }),});Declaring params is what makes the value validated and typed. A dynamic segment without
a matching schema is still routable, but its value never reaches input.
Route Groups
Section titled “Route Groups”A folder wrapped in parentheses organizes files without contributing a URL segment.
src/routes/(admin)/audit/route.ts serves /audit, not /admin/audit.
Directorysrc/routes/
Directory(public)/
Directorystatus/
- route.ts
/status
- route.ts
Directory(internal)/
- middleware.ts applies to
/auditonly Directoryaudit/
- route.ts
/audit
- route.ts
- middleware.ts applies to
Groups are the tool for applying one middleware chain to a set of routes that do not share
a URL prefix. The group name is recorded in generated metadata and in routa routes
output, so it stays visible even though it is invisible in the URL.
Reserved File Names
Section titled “Reserved File Names”Inside src/routes, three names are never treated as routes:
| File | Role |
|---|---|
middleware.ts |
Middleware applied to that folder and everything below it |
schemas.ts |
Zod schemas for nearby routes |
*.test.ts |
Tests, ignored by route discovery |
Any other .ts file is a route file.
- One path may be served by exactly one file. Two files resolving to the same path is a
ROUTA_DUPLICATE_ROUTEerror, which is the usual way a directory-style and flat-style file collide. - The path passed to
createRouteRoot()uses:namefor parameters, while the filesystem uses$name. OPTIONSis never declared. Routa generates it for every known path and derives theAllowheader from the methods that path declares.HEADis implicit for any path with aGET. Declareheadexplicitly only when it needs its own contract.
Limits
Section titled “Limits”Routa v0 has no catch-all or optional segments, no regex constraints in path syntax, and no explicit route priority. Constrain a parameter with its Zod schema instead. Matching is handled by Hono’s router, so static segments take precedence over dynamic ones.