Concepts
Filesystem as the Source of Truth
A file’s path + name determines its behavior:
- A directory under
app/is a route segment. page.tsx→ the page at that URL.get.ts/post.ts/put.ts/patch.ts/delete.ts→ HTTP handlers for that URL.layout.tsx→ shared layout for this folder and below.middleware.ts→ runs before every handler in this folder and below.*.cron.ts→ a scheduled job.
Runtime Split: Browser vs Server
Files in the same folder can belong to different runtimes — Manh splits them at build time:
| Extension | Runtime | Bundle |
|---|---|---|
.tsx | Browser only | Shipped to the client |
.ts | Server only | Deployed to the Worker |
*.cron.ts | Server only | Worker (scheduled) |
Rules:
- A
.tsxpage cannot import a.tshandler, and vice versa. - No shared modules. FE and BE cannot share files. If both need the same enum/constant, duplicate it in each side.
- Columns files (
*-columns.tsx) use.tsxbecause they contain JSX render functions.
Directory Structure
The app/ Directory
app/
├── layout.tsx # Optional root layout
├── page.tsx # "/" frontend page
├── db.ts # DB client (auto-generated, read-only)
├── hooks.ts # Frontend hooks (auto-generated, read-only)
│
├── orders/
│ ├── layout.tsx # Layout for /orders/*
│ ├── page.tsx # "/orders"
│ ├── get.ts # GET /orders
│ ├── post.ts # POST /orders
│ ├── middleware.ts # Runs on every /orders/* request
│ └── [id]/
│ ├── page.tsx # "/orders/:id"
│ ├── get.ts # GET /orders/:id
│ ├── put.ts # PUT /orders/:id
│ ├── patch.ts # PATCH /orders/:id
│ └── delete.ts # DELETE /orders/:id
│
└── (admin)/ # Route group — NOT part of the URL
├── middleware.ts # Applies to every route below (admin)/
└── users/
├── page.tsx # "/users" (NOT "/admin/users")
├── get.ts # GET /users
└── post.ts # POST /usersFile Type Reference
| File | Runtime | Editable | Exports | Purpose |
|---|---|---|---|---|
page.tsx | Browser | ✅ | default React component | UI for this route |
layout.tsx | Browser | ✅ | default React component | Shared layout — must render <Outlet /> |
get.ts / post.ts / … | Server | ✅ | schema, default handler | HTTP endpoint (file name = method, lowercase) |
middleware.ts | Server | ✅ | default (fn or fn array) | Pre-handler logic for this folder and below |
*.cron.ts | Server | ✅ | schedule, default fn | Scheduled job — no HTTP response |
*.email.ts | Server | ✅ | default fn (mail) | Inbound email to app+<slug>.<name>@in.manh.run |
*.mcp.ts | Server | ✅ | description, input, default fn | MCP tool at https://<slug>.manh.run/mcp |
<resource>.table.ts | Server | ✅ | Drizzle tables | DB schema for that resource (one per resource) |
db.ts | Server | ❌ | db, operators | DB client + Drizzle helpers (auto-generated) |
hooks.ts | Browser | ❌ | useRequest, useApi, … | Frontend hooks (auto-generated) |
api.ts | Browser | ❌ | useApiClient | Typed client for your handlers (auto-generated) |
Naming Conventions
| Element | Convention | Example |
|---|---|---|
| Folders | kebab-case | app/order-items/ |
| Files | lowercase | page.tsx, get.ts |
| Dynamic segments | [name] or [name:type] | app/orders/[id:uuid]/page.tsx |
| Route groups | (group) | app/(admin)/users/page.tsx |
| Cron jobs | *.cron.ts | app/orders/daily-report.cron.ts |
| Inbound email | *.email.ts | app/orders/orders.email.ts |
Typed dynamic segments
Path params are auto-injected into c.req.valid("param") from the filepath — never declared in schema. Append :type for runtime validation + OpenAPI hints.
| Syntax | Runtime validator | Notes |
|---|---|---|
[id] | z.string() | Default, no validation |
[id:uuid] | z.string().uuid() | UUID v4 format |
[slug:slug] | z.string().regex(/^[a-z0-9-]+$/) | URL-safe slug |
[n:number] | z.coerce.number().int() | Coerced int from URL string |
[n:int] | z.coerce.number().int() | Alias of number |
The type hint is stripped from the URL (/orders/[id:uuid]/get.ts → /orders/:id). Frontend pages get matching runtime validation via react-router loader — invalid params render a 404 before the page component mounts.
Last updated on