HTTP routes are inferred from where a Flow file lives. Put `http.get()` in
`src/flows/links/[code]/get.ts` and the compiler stamps `GET /links/:code`, names the
Flow `links.get`, and the client calls `api.links.get({ code })`.

<Callout title="The one rule">
  On a tree file, omit path and name: `on(http.get(), flow({ do }))`. The file
  tree stamps both. Pass either only for control — barrels, a URL that must not
  follow the folder, or a stable name for `fx.call`. Explicit always wins.
</Callout>

## When to omit · when to pass

|               | Omit (default)                                                       | Pass explicitly                                                                                                  |
| ------------- | -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| **HTTP path** | Tree file under `src/flows/<unit>/` — `http.get()`, `http.post()`, … | Barrel `index.ts`; a public URL that must not match the folder; `http.resource(path, ops)`; custom live SSE path |
| **Flow name** | Same tree file — `flow({ do })` stamps `unit.export`                 | Barrel (optional — export still stamps); stable name across moves; call-only Flow you `fx.call` by name          |

**Consequence:** create-oke `shorter` is this tree — `http.get()` + `flow({ … })` with no
string path and no string name, except where the public URL must not follow the folder
(`GET /:code`).

## Smallest Example

<Steps>

<Step>
### Place the file in the tree

`oke dev` / `oke build` regenerate `src/flows/index.ts`. Import it before
`oke()` so pathless triggers receive their stamp:

```typescript title="src/app.ts"
import "@/core";
import "@/flows";
import { oke } from "okengine/http";

export const app = oke({ name: "app" });
```

</Step>

<Step>
### Use pathless `http.get()`

The tree stamps `GET /links/:code`. `:code` merges into `in`. The export name is the
client method (`api.links.get`):

```typescript title="src/flows/links/[code]/get.ts"
import { on, flow, http } from "okengine";
import { z } from "zod";

export const get = on(
  http.get({
    in: z.object({ code: z.string() }),
    out: z.object({ code: z.string(), url: z.string() }),
  }),
  flow({
    do: async ({ code }) => ({ code, url: "https://example.com" }),
  }),
);
```

</Step>

<Step>
### Call the endpoint

```bash
curl -X GET http://localhost:6530/links/ok -H "accept: application/json"
```

Response:

```json
{
  "data": { "code": "ok", "url": "https://example.com" },
  "error": null
}
```

</Step>

</Steps>

<Callout title="Method is not the filename">
  `list.ts` does not become `GET` by itself. Reserved leaves only omit a URL segment. Bind
  `http.get()`, `http.post()`, `http.patch()`, or `http.delete()` yourself — see [Reserved
  Leaves](#reserved-leaves).
</Callout>

## Progressive Patterns

From a pathless tree file to an explicit barrel, an action leaf, and a public URL that must
not follow the folder:

<Tabs items={["Tree", "Barrel", "Action", "Override"]}>

<Tab value="Tree">

One file per route. Skip the path argument. The generated barrel calls
`stampHttpPath` / `stampFlowName` after import:

```typescript title="src/flows/links/list.ts"
import { on, flow, http } from "okengine";

export const list = on(
  http.get(),
  flow({
    do: () => [],
  }),
);
```

Stamped to `GET /links`, Flow `links.list`, client `api.links.list()`.

</Tab>

<Tab value="Barrel">

A unit that is **only** `index.ts` (plus skip-list files) is a barrel. The
generated file re-exports it **without** stamping — pass explicit paths:

```typescript title="src/flows/links/index.ts"
import { on, flow, http } from "okengine";
import { z } from "zod";

export const list = on(
  http.get("/links").public(),
  flow("links.list", {
    do: () => [],
  }),
);

export const create = on(
  http.post("/links", { in: z.object({ url: z.string().url() }) }),
  flow("links.create", {
    do: async ({ url }, fx) => ({ id: fx.id(), url }),
  }),
);
```

Pathless `http.get()` inside a barrel stays unresolved and fails boot
(**OKE1040**).

</Tab>

<Tab value="Action">

A leaf that is not reserved **adds** a segment. `archive.ts` is
`POST /links/:code/archive`, not `POST /links/:code`:

```typescript title="src/flows/links/[code]/archive.ts"
import { on, flow, http } from "okengine";
import { z } from "zod";

export const archive = on(
  http.post({ in: z.object({ code: z.string() }) }),
  flow({
    do: async ({ code }) => ({ code, archived: true }),
  }),
);
```

</Tab>

<Tab value="Override">

Pass the path when the folder would stamp the wrong URL. `redirect.ts` would be
`GET /links/redirect`. Short URLs must be root-level:

```typescript title="src/flows/links/redirect.ts"
import { on, flow, http } from "okengine";
import { z } from "zod";

export const redirect = on(
  http.get("/:code", { in: z.object({ code: z.string() }) }).public(),
  flow({
    do: async ({ code }) =>
      new Response(null, { status: 302, headers: { Location: `https://example.com/${code}` } }),
  }),
);
```

The tree never overwrites an explicit path. Static `/health` · `/links` · `/auth`
still win over `/:code`.

</Tab>

</Tabs>

Catch-all `[...slug]` becomes `*` on the URL. The request param is always `"*"`, never
`slug`:

```typescript title="src/flows/docs/[...slug]/get.ts"
import { on, flow, http } from "okengine";
import { z } from "zod";

export const get = on(
  http.get({ in: z.object({ "*": z.string() }) }).public(),
  flow({
    do: async (input) => ({ path: input["*"] }),
  }),
);
```

```bash
curl -X GET http://localhost:6530/docs/getting-started/install \
  -H "accept: application/json"
```

`do` receives `{ "*": "getting-started/install" }`. Call
`api.docs.get({ "*": "a/b/c" })` — not `{ slug }`.

## Convention Reference

| Convention    | File                                       | Stamped route               | Client                         |
| ------------- | ------------------------------------------ | --------------------------- | ------------------------------ |
| Dynamic param | `links/[code]/get.ts` + `http.get()`       | `GET /links/:code`          | `api.links.get({ code })`      |
| Reserved leaf | `links/list.ts` + `http.get()`             | `GET /links`                | `api.links.list()`             |
| Action leaf   | `links/[code]/archive.ts` + `http.post()`  | `POST /links/:code/archive` | `api.links.archive({ code })`  |
| Explicit path | `links/redirect.ts` + `http.get("/:code")` | `GET /:code`                | `api.links.redirect({ code })` |
| Catch-all     | `docs/[...slug]/get.ts` + `http.get()`     | `GET /docs/*`               | `api.docs.get({ "*": "a/b" })` |
| Route group   | `links/(ops)/archive.ts`                   | `/links/archive`            | `api.links.archive`            |
| Root unit     | `main/health.ts` + `http.get()`            | `GET /health`               | `api.main.health()`            |
| Folder root   | `main/route.ts` + `http.get()`             | `GET /`                     | `api.main.root()`              |

## Units

The first folder under `src/flows/` is the **client unit**. Nested folders and
the leaf file build the URL. The `export const` name is the method on that unit.

create-oke `-t shorter` ships this tree (`-t blank` is `main/` only):

```text
src/flows/
├── links/                     # Unit: links → api.links.*
│   ├── list.ts                # GET /links          (reserved leaf)
│   ├── create.ts              # POST /links         (reserved leaf)
│   ├── redirect.ts            # explicit GET /:code
│   ├── expire.ts              # Clock — not HTTP
│   ├── reach.ts               # Clock — not HTTP
│   ├── shapes.ts              # skipped (contracts)
│   ├── signals.ts             # skipped as a route; on() still joins the unit
│   ├── _shared.ts             # skipped (_ prefix)
│   └── [code]/
│       ├── get.ts             # GET /links/:code
│       ├── archive.ts         # POST /links/:code/archive
│       └── report.ts          # GET /links/:code/report
└── main/                      # Unit: main — prefix omitted from the URL
    ├── health.ts              # GET /health
    ├── route.ts               # GET /
    └── shapes.ts
```

A file sitting directly in `src/flows/` (no unit folder) is not a route.

Unit folder names must be valid JS identifiers (`links`, `main`, `_` allowed
inside; `my-links` is skipped). Folders starting with `_`, `[`, or `(` are not
units.

**Consequence:** `export const getLink` from `get.ts` is `api.links.getLink`, not
`api.links.get`. Match the export to the name you want on the client.

## Path Conventions

**Default — pathless.** Omit the path so `src/flows/index.ts` stamps the URL from disk:

```typescript
http.get(); // pending until stampHttpPath runs
```

**Control — explicit path.** Pass the URL template when the folder should not
own the route (barrel, public API shape, resource mount). The tree never
overwrites an explicit path:

```typescript
http.get("/:code");
```

Path params, query string, and JSON body still merge into one object checked by
`in` — same order as [HTTP · Request Parsing](/docs/elements/flow/http#request-parsing).

### Dynamic parameters

`[code]` → `:code`. The folder name is the param key. Declare the same key on `in`:

```typescript title="src/flows/orgs/[orgId]/members/[memberId]/get.ts"
import { on, flow, http } from "okengine";
import { z } from "zod";

export const get = on(
  http.get({ in: z.object({ orgId: z.string(), memberId: z.string() }) }),
  flow({
    do: async ({ orgId, memberId }) => ({ orgId, memberId }),
  }),
);
```

Stamped to `GET /orgs/:orgId/members/:memberId`.

### Catch-alls

`[...slug]` → `*` (greedy rest). Optional `[[...slug]]` is **not** supported —
generate fails with `Optional catch-all "[[...slug]]" is not supported — use [...slug] (param is "*").`

A route that includes `*` selects the Trie matcher automatically (the compiled
RegExp matcher cannot express wildcards).

### Route groups

A folder `(ops)` is omitted from the URL. Use it to group files without adding a
segment:

```text
src/flows/links/(ops)/archive.ts  →  /links/archive
```

`(ops)` never enters the Flow name either (`links.archive`).

### The `main` unit

`main` is omitted from the URL prefix. Flow names still use `main.`:

| File             | URL           | Flow name                              |
| ---------------- | ------------- | -------------------------------------- |
| `main/health.ts` | `/health`     | `main.health`                          |
| `main/route.ts`  | `/`           | `main.root` (from `export const root`) |
| `main/index.ts`  | `/` (extract) | barrel — pass `http.get("/")`          |

### Skip list

These files are never HTTP routes (path inference returns nothing). Walk skips
them except `signals.ts`, which still contributes `on()` consumers:

| Pattern                     | Why                                                       |
| --------------------------- | --------------------------------------------------------- |
| `src/flows/index.ts`        | Adopt barrel (`oke dev` / `oke build` writes it)          |
| `shapes.ts`                 | Shared Zod / contracts                                    |
| `signals.ts`                | Declarations. `on()` in the same file still join the unit |
| `*.test.ts` / `*.test.tsx`  | Tests                                                     |
| `_` prefix (file or folder) | Private helpers (`links/_shared.ts`)                      |

Skip-list files may sit next to tree routes. They do **not** turn a tree into a
barrel. Clock files (`expire.ts`, `reach.ts`) still join the unit — they are not
HTTP unless they bind `http.*`.

## Reserved Leaves

To avoid `/links/get` and `/orders/list`, these filenames add **no** URL
segment — the same five CRUD names as `http.resource`, plus folder roots:

| Leaf     | Typical trigger | Example file                        | Stamped path    |
| -------- | --------------- | ----------------------------------- | --------------- |
| `list`   | `http.get()`    | `links/list.ts`                     | `/links`        |
| `create` | `http.post()`   | `links/create.ts`                   | `/links`        |
| `get`    | `http.get()`    | `links/[code]/get.ts`               | `/links/:code`  |
| `update` | `http.patch()`  | `links/[code]/update.ts`            | `/links/:code`  |
| `remove` | `http.delete()` | `links/[code]/remove.ts`            | `/links/:code`  |
| `index`  | _(barrel only)_ | `links/index.ts`                    | `/links`        |
| `route`  | any             | `links/route.ts` or `main/route.ts` | `/links` or `/` |

Any other leaf **is** a segment: `query.ts` → `/links/query`. `redirect.ts` with
pathless `http.get()` would stamp `/links/redirect` — pass `http.get("/:code")`
instead.

In a tree unit, do **not** add `index.ts` beside other route files — that is a
generate error. Use `route.ts` (or `list.ts` / `create.ts`) for the collection
root.

## Barrel vs Tree

`oke dev` / `oke build` scans each `src/flows/<unit>/` folder and writes
`src/flows/index.ts`. Two shapes, never mixed:

<Tabs items={["Tree", "Barrel", "App entry"]}>

<Tab value="Tree">

`[param]` folders, `(group)` folders, or extra route files. The barrel imports
each file and stamps path + name:

```typescript
const links = {
  get: stampHttpPath(stampFlowName(links_$code$_get.get, "links.get"), "/links/:code"),
  list: stampHttpPath(stampFlowName(links_list.list, "links.list"), "/links"),
};
export { links };
registerFlowUnits({ links });
```

`oke()` drains `registerFlowUnits` into `$routes`. `.adopt({ links })` is
optional and additive.

</Tab>

<Tab value="Barrel">

Only `index.ts` (+ skip-list). Re-export, no stamp:

```typescript
import * as links from "./links/index.ts";
export { links };
registerFlowUnits({ links });
```

Declare `http.get("/links")` and `flow("links.list", {…})` (or rely on adopt to
stamp the name from the export). Pathless HTTP fails **OKE1040**.

</Tab>

<Tab value="App entry">

```typescript title="src/app.ts"
import "@/core";
import "@/flows";
import { oke } from "okengine/http";

export const app = oke({ name: "app" });
export type App = typeof app;
```

Do not edit `src/flows/index.ts` by hand. Adding a unit folder without regenerating
leaves a stale barrel — **OKE1030** in prod / `oke dev` with Compose.

</Tab>

</Tabs>

<Accordions>

<Accordion title="Mixed barrel + tree">
  `index.ts` plus `[code]/get.ts` (or any other route file) throws at generate:

```text
Unit "links" mixes a barrel index.ts with tree route files. Use only index.ts (barrel), or move the collection path to route.ts and keep [id]/ beside it.
```

Fix: delete `index.ts` and use `list.ts` / `route.ts`, or fold every route into
`index.ts` with explicit paths.

</Accordion>

<Accordion title="Export collisions">
  Two files in the same unit cannot share an `export const` name:

```text
Unit "links" exports "get" from both list.ts and route.ts.
```

Rename one export. The client method is the export name, not the filename.

</Accordion>

<Accordion title="Unit-prefix drift">
  `flow("tasks.get", {…})` living under `src/flows/links/` throws:

```text
flow("tasks.…") in links/get.ts does not match the folder "links".
```

Use `flow("links.get", {…})`, a nameless `flow({ do })` (stamped `links.get`
from the export), or move the file.

</Accordion>

</Accordions>

## Names

Three names, one file:

| Surface   | Source                                                              | Example                   |
| --------- | ------------------------------------------------------------------- | ------------------------- |
| HTTP path | File tree (default) or explicit `http.get("/x")`                    | `/links/:code`            |
| Flow name | `unit.export` from `flow({ do })` (default), or `flow("links.get")` | `links.get`               |
| Client    | Unit folder + `export const`                                        | `api.links.get({ code })` |

Nameless `flow({ do })` is the tree default — same rule as pathless HTTP. Pass
`flow("links.get")` only for control (stable name, barrel, or matching unit
prefix). Wrong-unit prefixes fail generate.

Non-HTTP files still join the unit. A signal consumer in `links/signals.ts` is
`api.links.onCreated` over RPC (`POST /_oke/links/onCreated`), not HTTP.

Signal / Clock consumers pass an explicit `flow("…")` name (**OKE1072** if nameless
outside `src/flows/<unit>/`; **OKE1070** on collision). Clock may write `clock.every(…)`
inside `on()` — [Clock · Inline or named export](/docs/elements/clock#inline-or-named-export).

## Runtime Matching

All adopted HTTP bindings go into one matcher. Wrong method on a known path is
**405** with `Allow`. No match is a bare **404** `Not Found`.

| `oke({ router })` | Default | What it does                                                                                                |
| ----------------- | ------- | ----------------------------------------------------------------------------------------------------------- |
| `"default"`       | yes     | Compiled RegExp (O(1) static map + per-bucket regex for `:id`). Falls back to Trie when a path includes `*` |
| `"edge"`          |         | Linear scan, then Trie. No RegExp compile — cold-start / isolates                                           |

Static paths win over `:param` on the same method (`GET /health` beats
`GET /:code`), including the edge linear scan. `okengine/http` defaults to
`"edge"`.

p99 match stays under **1 ms** on the compiled matcher. You do not pick buckets
by hand — a catch-all in the table selects Trie for the whole app.

Duplicate `METHOD + path` fails boot (**OKE1041**), including a resource mount
plus a handwritten `http.get("/links")`.

## Troubleshooting

<Accordions>

<Accordion title="404 Not Found — route missing">
  No Flow is bound to that method + path. Check the explicit path, or for pathless routes the
  file-tree stamp (`links/[code]/get.ts` → `GET /links/:code`). A bare `404` with body `Not Found`
  means the router found no match.
</Accordion>

<Accordion title="405 Method Not Allowed on valid route">
  The path exists but has not been bound to the requested verb. `Allow` lists the methods that are.
  `list.ts` with `http.get()` is GET-only — POST that URL is 405, not a missing file.
</Accordion>

<Accordion title="OKE1040 — pathless trigger never stamped">
  Cause: `Flow "{flow}" bound {method} with no path — the file-tree stamp never ran.` Import
  `@/flows`, run `oke dev` / `oke build`, or pass `http.get("/…")`. Barrels do not stamp — they need
  the explicit path.
</Accordion>

<Accordion title="OKE1030 — adopt barrel stale">
  Cause: `src/flows/{unit} exists on disk but adopted no flows — the .adopt() barrel is stale.` Run
  `oke dev` or `oke build` after adding a unit folder. Prod and Compose `oke dev` refuse to boot
  this way.
</Accordion>

<Accordion title="OKE1041 — method + path bound twice">
  Cause: `{method} {path} is bound twice (flow "{flow}").` Two tree files stamped the same verb +
  path (`list.ts` and `route.ts` both `http.get()`), or a resource mount plus a handwritten route.
  Drop one binding.
</Accordion>

<Accordion title="OKE1045 — HTTP flow unnamed">
  Cause: `An HTTP flow on {method} {path} has no name.`
  Use `flow("unit.export", {…})` or `export const` from a `src/flows/<unit>/`
  file so the tree can stamp `unit.export`. `export default` is not picked up.
</Accordion>

<Accordion title="OKE1070 — flow name defined twice">
  Cause: `Flow "{flow}" is defined twice.` Two explicit `flow("…")` calls collide, or two tree
  exports stamp the same `unit.export`. Give at least one a distinct name or tree export.
</Accordion>

<Accordion title="OKE1072 — Signal or Clock flow unnamed">
  Cause: `A {kind} flow on "{trigger}" has no name.`
  Fix: pass an explicit name — `on(handle, flow("unit.export", { do }))`.
</Accordion>

<Accordion title="422 — path param missing from in">
  `[code]` stamps `:code`. `in` must declare `code` (same key). A schema that expects `id` while the
  path is `:code` fails validation before `do`.
</Accordion>

<Accordion title="Catch-all input is empty / wrong key">
  `[...slug]` does not bind `slug`. The router param is `"*"`. Declare
  `in: z.object({ "*": z.string() })` and read `input["*"]`. Optional
  `[[...slug]]` is rejected at generate.
</Accordion>

<Accordion title="Unit mixes index.ts with tree files">
  Generate: `Unit "…" mixes a barrel index.ts with tree route files.` Use only `index.ts` (explicit
  paths), or move the collection path to `route.ts` and keep `[code]/` beside it.
</Accordion>

<Accordion title="GET /:code swallowed /health">
  Static paths win. If `/health` 404s, a catch-all `/:code` registered first on the edge matcher
  used to win — upgrade. Shorter binds `GET /:code` as an explicit override; `GET /health` still
  matches `main.health`.
</Accordion>

</Accordions>

## Learn more

- [The Architecture](/docs/understand/the-architecture) — derived routes, no hand-written table
- [Try it](/docs/understand/try-it) — scaffold, run, and hit a stamped path
- [HTTP](/docs/elements/flow/http) — verbs, envelopes, `http.resource`, live SSE
- [Client](/docs/client/calling) — `api.links.get`, REST vs RPC, `$routes`
- [Errors](/docs/reference/errors) — OKE1040 · OKE1030 · OKE1041 · OKE1045 · OKE1070 · OKE1072
- [Gate](/docs/elements/gate) — `.gate(...)` / `.public()` on the same trigger

## Next

<Cards>
  <Card
    title="The Architecture"
    description="One law, eight elements, derived Manifest — where routes come from."
    href="/docs/understand/the-architecture"
  />
  <Card
    title="Try it"
    description="Scaffold shorter, run oke dev, and call a stamped path."
    href="/docs/understand/try-it"
  />
  <Card
    title="HTTP"
    description="REST verbs, RFC 10008 QUERY, CRUD mounts, and live SSE on Flow."
    href="/docs/elements/flow/http"
  />
</Cards>
