Skip to Content
FrameworkFilesystem grammar

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:

ExtensionRuntimeBundle
.tsxBrowser onlyShipped to the client
.tsServer onlyDeployed to the Worker
*.cron.tsServer onlyWorker (scheduled)

Rules:

  • A .tsx page cannot import a .ts handler, 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 .tsx because 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 /users

File Type Reference

FileRuntimeEditableExportsPurpose
page.tsxBrowser✅default React componentUI for this route
layout.tsxBrowser✅default React componentShared layout — must render <Outlet />
get.ts / post.ts / …Server✅schema, default handlerHTTP endpoint (file name = method, lowercase)
middleware.tsServer✅default (fn or fn array)Pre-handler logic for this folder and below
*.cron.tsServer✅schedule, default fnScheduled job — no HTTP response
*.email.tsServer✅default fn (mail)Inbound email to app+<slug>.<name>@in.manh.run
*.mcp.tsServer✅description, input, default fnMCP tool at https://<slug>.manh.run/mcp
<resource>.table.tsServer✅Drizzle tablesDB schema for that resource (one per resource)
db.tsServer❌db, operatorsDB client + Drizzle helpers (auto-generated)
hooks.tsBrowser❌useRequest, useApi, …Frontend hooks (auto-generated)
api.tsBrowser❌useApiClientTyped client for your handlers (auto-generated)

Naming Conventions

ElementConventionExample
Folderskebab-caseapp/order-items/
Fileslowercasepage.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.tsapp/orders/daily-report.cron.ts
Inbound email*.email.tsapp/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.

SyntaxRuntime validatorNotes
[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