File-based Routing
Davaux uses your file system as the router. Every file inside src/routes/ that follows the naming convention below automatically becomes an HTTP endpoint — no manual route registration needed.
Filename → URL + method
| Filename | HTTP method | URL |
|---|---|---|
index.page.tsx | GET (page) | / |
about.page.tsx | GET (page) | /about |
blog/index.page.tsx | GET (page) | /blog |
blog/post.page.tsx | GET (page) | /blog/post |
api/users.get.ts | GET (API) | /api/users |
api/users.post.ts | POST (API) | /api/users |
api/users.put.ts | PUT (API) | /api/users |
api/users.patch.ts | PATCH (API) | /api/users |
api/users.delete.ts | DELETE (API) | /api/users |
The distinction between .page.tsx and .get.ts is purely semantic — a page route returns JSX and goes through the layout chain, while an API route (defineHandler) returns a raw Response or calls ctx.res directly.
Dynamic segments
Wrap a segment name in square brackets to create a dynamic route parameter:
src/routes/blog/[slug].page.tsx → /blog/:slug
src/routes/users/[id]/posts.get.ts → /users/:id/posts
You can have multiple dynamic segments in a path:
src/routes/shop/[category]/[product].page.tsx
→ /shop/:category/:product
Access the captured values through ctx.params:
import { definePage } from 'davaux'
export default definePage((ctx) => {
const { slug } = ctx.params // string
return <h1>Post: {slug}</h1>
})
Type-safe params
Use the ExtractParams utility type to get typed params inferred directly from the route path string:
import { defineHandler } from 'davaux'
import type { ExtractParams } from 'davaux'
type Params = ExtractParams<'users/[id]/posts'>
// → { id: string }
export default defineHandler((ctx) => {
const { id } = ctx.params as Params
return Response.json({ userId: id })
})
Catch-all routes
Prefix a segment with ... inside brackets to match any number of path segments:
src/routes/wiki/[...slug].page.tsx → /wiki/*
The matched segments are joined and available as a single string in ctx.params:
import { definePage } from 'davaux'
export default definePage((ctx) => {
const { slug } = ctx.params // e.g. "getting-started/installation"
const parts = slug.split('/') // ["getting-started", "installation"]
return <h1>Wiki: {slug}</h1>
})
Catch-all routes have the lowest priority — static and dynamic segments at the same prefix always win:
/wiki/changelog → wiki/changelog.page.tsx (static, wins)
/wiki/[id].page.tsx → /wiki/:id (dynamic, wins over catch-all)
/wiki/[...slug] → /wiki/* (catch-all, last resort)
Framework-reserved filenames
These files do not become HTTP endpoints; they wire up the application structure:
| Filename | Purpose |
|---|---|
_layout.tsx | Wraps all routes at this directory level and below |
_middleware.ts | Runs before routes in this directory (scoped) |
_error.tsx | Custom error/404 page for this directory |
For middleware that runs on every request (including 404s), create src/middleware.ts outside the routes directory. See the Middleware page for details.
Defining a page route
// src/routes/dashboard.page.tsx
import { definePage } from 'davaux'
export default definePage(async (ctx) => {
ctx.head.title = 'Dashboard'
const data = await fetchDashboardData()
return (
<main>
<h1>Dashboard</h1>
<p>Welcome back!</p>
</main>
)
})
Defining an API route
// src/routes/api/ping.get.ts
import { defineHandler } from 'davaux'
export default defineHandler((ctx) => {
return Response.json({ pong: true, ts: Date.now() })
})
// src/routes/api/items.post.ts
import { defineHandler } from 'davaux'
export default defineHandler(async (ctx) => {
const body = await ctx.json()
// ... save body to database
return Response.json({ created: true }, { status: 201 })
})
Route priority
When two routes resolve to the same URL, the more specific one wins:
- Exact static segments beat dynamic segments (
/blog/archivebeats/blog/[slug]) - Explicit method routes (
.get.ts) beat page routes (.page.tsx) at the same URL - Deeper directory matches beat shallower ones
The index convention
A file named index represents the root of its directory:
src/routes/index.page.tsx → /
src/routes/blog/index.page.tsx → /blog
This lets you have both /blog (the listing) and /blog/[slug] (individual posts) as siblings in the same directory.
Co-locating components and utilities
The scanner only processes files that match a route naming convention. Everything else is silently ignored — which means you can safely co-locate non-route files next to your routes.
Recommended layout
src/
routes/
blog/
[slug].page.tsx # /blog/:slug
index.page.tsx # /blog
BlogCard.tsx # co-located component — NOT a route, ignored by scanner
dashboard/
index.page.tsx # /dashboard
_middleware.ts # scoped middleware
widgets/
Chart.tsx # co-located component
Stats.tsx
components/ # shared components used across multiple routes
Button.tsx
Nav.tsx
Footer.tsx
lib/ # shared utilities, helpers, db clients
db.ts
auth.ts
Rules of thumb
src/components/— shared UI components used across multiple routessrc/lib/— utilities, database clients, auth helpers, and other non-UI shared code- Subdirectory co-location — components used only by one route group can live alongside those routes
The scanner warning
If you accidentally place a .tsx file directly in the src/routes/ root (not in a subdirectory) without a route suffix, Davaux will warn you at startup:
[davaux] warning: MyComponent.tsx is not a route file and will be ignored.
If this is a shared component, move it to src/components/ or co-locate it
in a subdirectory alongside the routes that use it.
The warning only triggers for .tsx files at the routes root, since those are the most likely accidental placements. Files in subdirectories are silently ignored — subdirectory co-location is intentional and encouraged.