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

FilenameHTTP methodURL
index.page.tsxGET (page)/
about.page.tsxGET (page)/about
blog/index.page.tsxGET (page)/blog
blog/post.page.tsxGET (page)/blog/post
api/users.get.tsGET (API)/api/users
api/users.post.tsPOST (API)/api/users
api/users.put.tsPUT (API)/api/users
api/users.patch.tsPATCH (API)/api/users
api/users.delete.tsDELETE (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:

FilenamePurpose
_layout.tsxWraps all routes at this directory level and below
_middleware.tsRuns before routes in this directory (scoped)
_error.tsxCustom 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:

  1. Exact static segments beat dynamic segments (/blog/archive beats /blog/[slug])
  2. Explicit method routes (.get.ts) beat page routes (.page.tsx) at the same URL
  3. 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 routes
  • src/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.