Skip to main content
Dynamic routes let you capture variable segments of a URL path without defining a separate file for every possible value. Anvil uses a folder-naming convention borrowed from Next.js: square brackets for a named parameter, three dots inside brackets for a catch-all.

Named parameters: [param]

Wrap a folder name in square brackets to make it a dynamic segment. The captured value is available on ctx.params under the same name as the folder.
Read the captured value in your handler:
server/routes/users/[id]/get.ts
You can nest multiple dynamic segments. Each one contributes its own key to ctx.params:
server/routes/orgs/[orgId]/repos/[repoId]/get.ts

Catch-all segments: [...param]

Prefix with ... to capture the rest of the path as a single string. Everything after the catch-all folder’s parent is joined with / and stored in ctx.params under the param name.
server/routes/files/[...path]/get.ts
A catch-all directory cannot contain subdirectories. The scanner enforces this at build time: placing a folder inside [...path]/ throws a RouteConflictError.

Router precedence

When multiple route patterns could match the same URL, Anvil resolves the conflict deterministically — insertion order never matters:
  1. Static segments win over dynamic ones (/users/me beats /users/[id] for the path /users/me)
  2. Named parameters win over catch-alls
  3. Backtracking ensures /a/[b]/c still matches when /a/x exists as a static sibling

Validating params with paramsSchema

Export a paramsSchema Zod object from any route file to enable compile-time validation and MCP tool generation. anvil lint checks that every key in the schema corresponds to a real folder param in the path.
server/routes/users/[id]/get.ts
paramsSchema is a compile-time and MCP-exposure hint. At runtime, ctx.params values are always plain strings — they are not coerced or parsed by the schema. Use the schema keys as a contract that anvil lint enforces.

Naming rules

Anvil enforces several rules to prevent subtle bugs and cross-platform inconsistencies:

No duplicate param names

The same param name cannot appear twice in one route path. /[id]/items/[id]/get.ts throws a RouteConflictError at scan time.

No conflicting names at the same position

Two routes cannot use different param names at the same depth. /users/[id]/get.ts and /users/[slug]/post.ts conflict — use the same name ([id]) across all files in that folder.

Case-insensitive collision detection

anvil lint rejects Users/ and users/ in the same parent because Windows treats them as the same directory while Linux does not.

Valid JavaScript identifiers only

Param names must match /^[A-Za-z_$][A-Za-z0-9_$]*$/. [123] and [my-param] are rejected at scan time.