File Conventions
Master TanStack Router-style file conventions for REST APIs. Learn flat routes, nested folders, path params ($id), catch-alls ($), and layouts.
Taser.js adopts modern TanStack Router-style file routing conventions tailored specifically for REST APIs. Your filesystem serves as the single source of truth for URL endpoints, HTTP methods, and cascading middleware pipelines.
Route Files vs Layout Files
Under your src/routes/ directory, files are classified into two distinct types:
- Route Files: Filenames containing an HTTP verb before
.ts(such asusers.get.ts,posts.post.ts,items.$id.delete.ts,tasks/$id.complete.patch.ts). Route files export a default route definition created witht.get(),t.post(), etc. - Layout Files: Filenames ending in
.tswithout an HTTP verb (such as$.ts,admin.ts,_auth.ts,tasks/$id.ts). Layout files export a default middleware pipeline created witht.layout().
Filesystem Path vs. Builder Path String
The filesystem location is the sole runtime source of truth for URL endpoints and cascading
middleware trees. The path string in t.get("/users") or t.layout("/admin/*") exists purely for
TypeScript compile-time type inference (populating ctx.params) and compile-time validation
against the project's ambient RoutePath union.
Source of Truth Comparison
| Aspect | Filesystem Path (src/routes/...) | Builder String (t.get("/path"), t.layout("/*")) |
|---|---|---|
| URL Determination | Sole source of truth for URL matching | No runtime impact on request routing |
| AST Scan Validation | Enforces valid file conventions and factory verb parity | Scanned for factory name (t.get, t.layout), not path identity |
| TypeScript Typecheck | Emits ambient RoutePath / LayoutId types | Validated by tsc against ambient RoutePath; types ctx.params |
| Mismatch Consequence | Determines where requests actually route | tsc compile error if invalid; param type drift if matching another route |
Route Path String Mismatch Error
If the path string in a route builder does not match any valid route path in the emitted ambient union, TypeScript flags the mismatch during tsc:
Type '"/usr/profile"' is not assignable to type 'RoutePath'.HTTP Method Suffixes
Taser.js determines the HTTP verb from the final extension segment before .ts:
| File Name | HTTP Method | Endpoint URL |
|---|---|---|
src/routes/users.get.ts | GET | /users |
src/routes/users.post.ts | POST | /users |
src/routes/users.put.ts | PUT | /users |
src/routes/users.patch.ts | PATCH | /users |
src/routes/users.delete.ts | DELETE | /users |
src/routes/users.options.ts | OPTIONS | /users |
src/routes/users.head.ts | HEAD | /users |
src/routes/users.query.ts | QUERY | /users |
src/routes/users.any.ts | ANY (multi-method) | /users |
src/routes/users.all.ts | ALL (catch-all verb) | /users |
Flat Routes, Directory Routes, and Mixed Notation
Taser.js supports nested directory structures, flat dot notation, and mixed folder + dot notation. You can use whichever structure keeps your codebase cleanest:
/posts/posts/:id/posts/:id/edit/tasks/:id/completeDot Separators in Filenames
A dot . inside a filename (e.g. tasks/$id.complete.patch.ts or posts.$id.edit.get.ts) acts
as a URL segment separator (/), allowing you to define sub-routes flatly without creating deeply
nested folders.
Index Routes (index.<method>.ts)
An index segment targets the root of its parent route without appending /index to the public URL:
//api/products/products/posts/:idIndex Routes vs. Dynamic Parameter Routes ($param)
| Pattern | Notation | Example File | Resolved URL | Recommended Use Case |
|---|---|---|---|---|
| Direct Param Route | Flat or Directory | posts.$id.get.ts or posts/$id.get.ts | GET /posts/:id | Standard: Standalone endpoint for a single parameter leaf. |
| Directory Index Param Route | Directory | posts/$id/index.get.ts | GET /posts/:id | Grouping: Co-locating root parameter handler with nested sub-routes (posts/$id/comments.get.ts, posts/$id/edit.put.ts). |
| Redundant Flat Index | Flat Dot | posts.$id.index.get.ts | GET /posts/:id | Discouraged: Redundant syntax in flat dot notation; prefer posts.$id.get.ts. |
Collision & Duplicate Route Rules
Both posts.$id.get.ts and posts.$id.index.get.ts (as well as posts/$id/index.get.ts) evaluate to the exact same route: GET /posts/:id.
If both files exist simultaneously within the same routes directory, Taser.js halts the build immediately during the route scan phase with a ScanErrorCollection:
ScanError: Duplicate route for GET /posts/:id (posts.$id.get.ts and posts.$id.index.get.ts)Route collisions are never silently overwritten and do not result in last-write-wins or undefined behavior. You must remove one of the duplicate route files to resolve the build error.
Dynamic Path Parameters ($param)
Prefixing a segment with $ denotes a named URL path parameter:
/users/:id/tasks/:id/complete/orgs/:orgId/repos/:repoIdInside your route handler, parameters are automatically typed and validated on ctx.params:
import { json } from "@taserjs/router/reply";
import { z } from "zod";
import { t } from "@taserjs/router";
export default t
.patch("/tasks/:id/complete")
.params(z.object({ id: z.string().uuid() }))
.handler((ctx) => {
// ctx.params.id is strictly typed as a UUID string
return json({ taskId: ctx.params.id, completed: true });
});Layout Inheritance Note
In the example above, src/routes/tasks/$id.complete.patch.ts is the standard
layout-inheriting route for PATCH /tasks/:id/complete. If a layout file
src/routes/tasks/$id.ts exists, this route automatically inherits that layout's middleware chain
($.ts → tasks/$id.ts). See Layout Breakout
Routes for the alternative syntax that
skips this layout.
Wildcard Catch-All Splats ($)
A standalone $ segment captures wildcard rest segments (splats):
/*/files/*/files/*In your handler, the wildcard path matches all trailing sub-paths.
Root Layout Middleware ($.ts)
A top-level file named src/routes/$.ts serves as the Root Layout Middleware. It runs before every route in your application:
import { bodyLimit } from "@taserjs/router/body-limit";
import { secureHeaders } from "@taserjs/router/secure-headers";
import { t } from "@taserjs/router";
export default t
.layout("/*")
.use(secureHeaders())
.use(bodyLimit({ maxSize: 1_000_000 }));Pathless Layouts (Leading Underscore _layout)
A segment starting with an underscore _ (like _auth.ts, _auth/, or _auth.login.post.ts) is pathless. It applies scoped middleware to child routes without adding any segment to the public URL:
/* (scoped _auth)/dashboard/profile/loginIn flat dot notation:
/dashboard/profile/metricsNotice that _auth, _app, and _dashboard are omitted from the public URL.
Layout Breakout Routes (Trailing Underscore segment_)
A segment ending with an underscore _ is a breakout route (un-nested route). It targets the expected URL path, but breaks out of the parent layout hierarchy so it skips that segment's layout middleware.
The Problem Breakout Routes Solve
Suppose you have an authenticated /posts layout in src/routes/posts.ts (or a /tasks/:id layout in src/routes/tasks/$id.ts) that enforces user login or permission checks. You want a specific child endpoint (like /posts/:id/preview or /tasks/:id/complete) to skip that layout's checks.
By adding a trailing underscore to posts_ or $id_, the route keeps the URL segment but skips the layout:
/*/posts/*/posts/posts/:id/posts/:id/preview (skips posts.ts)/tasks/:id/*/tasks/:id/status/tasks/:id/complete (skips $id.ts)Breakout Route vs. Base Route Alternatives
The base route tasks/$id.complete.patch.ts and the breakout route tasks/$id_.complete.patch.ts resolve to the identical endpoint URL (PATCH /tasks/:id/complete). They are mutually exclusive architectural alternatives for the same URL:
| Route Configuration | File Location | Resolved Endpoint | Middleware Chain | Purpose |
|---|---|---|---|---|
| Standard Route (Inheriting) | tasks/$id.complete.patch.ts | PATCH /tasks/:id/complete | $.ts → tasks/$id.ts | Runs the $id.ts layout checks (e.g. verifying task ownership or existence). |
| Breakout Route (Skipping) | tasks/$id_.complete.patch.ts | PATCH /tasks/:id/complete | $.ts only | Bypasses the tasks/$id.ts layout checks while retaining the identical URL structure. |
Breakout Route Collision Rule
These two files cannot coexist in the same routes tree. If both tasks/$id.complete.patch.ts and tasks/$id_.complete.patch.ts are present simultaneously, Taser.js halts compilation with a build error during route scanning:
ScanError: Duplicate route for PATCH /tasks/:id/complete (tasks/$id.complete.patch.ts and tasks/$id_.complete.patch.ts)Taser.js enforces strict uniqueness per [URL + HTTP Method]. Collisions always result in a build-time error, never a silent override or last-write-wins.
Underscore Convention Rules
- Leading underscore (
_auth): Pathless layout / group (adds middleware, removes segment from URL). - Trailing underscore (posts_or$id_): Breakout route (keeps segment in URL, removes parent layout from middleware chain).
Escaping Special Characters ([...])
When you need a literal character that would otherwise trigger a router convention (such as a literal dot ., literal leading underscore _, or literal index), wrap the character in brackets [...]:
| File Name | Resolved URL | Explanation |
|---|---|---|
src/routes/sitemap[.]xml.get.ts | GET /sitemap.xml | Escaped dot [.] prevents splitting into /sitemap/xml |
src/routes/docs/v1[.]0/api.get.ts | GET /docs/v1.0/api | Preserves v1.0 as a single path segment |
src/routes/[_]private.get.ts | GET /_private | Escaped [_] prevents segment from being treated as a pathless layout |
src/routes/tasks/task[_].get.ts | GET /tasks/task_ | Escaped [_] prevents segment from being treated as a layout breakout |
src/routes/items/[index].get.ts | GET /items/index | Escaped [index] prevents trimming to /items |
Ignored & Private Files (- Prefix)
To co-locate utility files, helper functions, schemas, or test fixtures directly alongside route files without generating routes, prefix the file or folder with a dash -:
(not routed)(not routed)/users/users/:id(not routed)Complete Conventions Reference
File Path in src/routes/ | HTTP Method | Resolved API URL | Layout Inheritance | Description |
|---|---|---|---|---|
$.ts | N/A | Global | Root | Root application layout |
index.get.ts | GET | / | /* | Root index route |
posts.ts | N/A | /posts | /* | Scoped layout for posts |
posts.index.get.ts | GET | /posts | /* → /posts | Flat posts index |
posts.$id.get.ts | GET | /posts/:id | /* → /posts | Flat parameterized route |
tasks/$id.complete.patch.ts | PATCH | /tasks/:id/complete | /* → /tasks | Mixed folder + dot nested route |
posts_.$id.edit.get.ts | GET | /posts/:id/edit | /* | Breakout route: skips posts.ts layout |
tasks/$id_.complete.patch.ts | PATCH | /tasks/:id/complete | /* → /tasks | Breakout route: skips tasks/$id.ts layout |
files.$.get.ts | GET | /files/* | /* | Wildcard splat handler |
_auth.ts | N/A | Scoped | /* | Pathless auth layout |
_auth.settings.get.ts | GET | /settings | /* → /_auth | Pathless scoped route |
sitemap[.]xml.get.ts | GET | /sitemap.xml | /* | Bracket-escaped literal dot |
-helpers.ts | N/A | N/A | N/A | Ignored file |
Next Steps
Migration Guide
Incrementally migrate existing Express or Fastify APIs to Taser.js with zero downtime using the host pass-through architecture.
Defining Routes
Learn how to define type-safe route endpoints using Taser.js's fluent route builder. Chain query, params, body schemas, middlewares, and response contracts.