create-daloy 0.38.2 → 0.39.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-daloy",
3
- "version": "0.38.2",
3
+ "version": "0.39.0",
4
4
  "description": "Scaffold a new DaloyJS project. Run with `pnpm create daloy`, `npm create daloy@latest`, `yarn create daloy`, or `bun create daloy`.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/sbom.cdx.json CHANGED
@@ -1,25 +1,25 @@
1
1
  {
2
2
  "bomFormat": "CycloneDX",
3
3
  "specVersion": "1.5",
4
- "serialNumber": "urn:uuid:2bbd5e7e-5035-5e09-92c8-3e1aec60f5a1",
4
+ "serialNumber": "urn:uuid:ed023484-5e0b-5534-bab9-c4cb2527b814",
5
5
  "version": 1,
6
6
  "metadata": {
7
- "timestamp": "2026-06-16T14:42:42.888Z",
7
+ "timestamp": "2026-06-17T18:08:50.010Z",
8
8
  "tools": [
9
9
  {
10
10
  "vendor": "DaloyJS",
11
11
  "name": "daloy-generate-sbom",
12
- "version": "0.38.2"
12
+ "version": "0.39.0"
13
13
  }
14
14
  ],
15
15
  "authors": [],
16
16
  "component": {
17
17
  "type": "library",
18
- "bom-ref": "pkg:npm/create-daloy@0.38.2",
18
+ "bom-ref": "pkg:npm/create-daloy@0.39.0",
19
19
  "name": "create-daloy",
20
- "version": "0.38.2",
20
+ "version": "0.39.0",
21
21
  "description": "Scaffold a new DaloyJS project. Run with `pnpm create daloy`, `npm create daloy@latest`, `yarn create daloy`, or `bun create daloy`.",
22
- "purl": "pkg:npm/create-daloy@0.38.2",
22
+ "purl": "pkg:npm/create-daloy@0.39.0",
23
23
  "licenses": [
24
24
  {
25
25
  "license": {
@@ -42,9 +42,9 @@
42
42
  }
43
43
  ],
44
44
  "swid": {
45
- "tagId": "swidtag-create-daloy-0.38.2",
45
+ "tagId": "swidtag-create-daloy-0.39.0",
46
46
  "name": "create-daloy",
47
- "version": "0.38.2",
47
+ "version": "0.39.0",
48
48
  "tagVersion": 0,
49
49
  "patch": false
50
50
  }
@@ -53,7 +53,7 @@
53
53
  "components": [],
54
54
  "dependencies": [
55
55
  {
56
- "ref": "pkg:npm/create-daloy@0.38.2",
56
+ "ref": "pkg:npm/create-daloy@0.39.0",
57
57
  "dependsOn": []
58
58
  }
59
59
  ]
package/sbom.spdx.json CHANGED
@@ -2,10 +2,10 @@
2
2
  "spdxVersion": "SPDX-2.3",
3
3
  "dataLicense": "CC0-1.0",
4
4
  "SPDXID": "SPDXRef-DOCUMENT",
5
- "name": "create-daloy-0.38.2",
6
- "documentNamespace": "https://github.com/daloyjs/daloy/sbom/create-daloy-0.38.2-2bbd5e7e-5035-5e09-92c8-3e1aec60f5a1",
5
+ "name": "create-daloy-0.39.0",
6
+ "documentNamespace": "https://github.com/daloyjs/daloy/sbom/create-daloy-0.39.0-ed023484-5e0b-5534-bab9-c4cb2527b814",
7
7
  "creationInfo": {
8
- "created": "2026-06-16T14:42:42.888Z",
8
+ "created": "2026-06-17T18:08:50.010Z",
9
9
  "creators": [
10
10
  "Tool: daloy-generate-sbom",
11
11
  "Organization: DaloyJS"
@@ -16,7 +16,7 @@
16
16
  {
17
17
  "SPDXID": "SPDXRef-Package-create-daloy",
18
18
  "name": "create-daloy",
19
- "versionInfo": "0.38.2",
19
+ "versionInfo": "0.39.0",
20
20
  "downloadLocation": "https://github.com/daloyjs/daloy",
21
21
  "filesAnalyzed": false,
22
22
  "licenseConcluded": "MIT",
@@ -27,7 +27,7 @@
27
27
  {
28
28
  "referenceCategory": "PACKAGE-MANAGER",
29
29
  "referenceType": "purl",
30
- "referenceLocator": "pkg:npm/create-daloy@0.38.2"
30
+ "referenceLocator": "pkg:npm/create-daloy@0.39.0"
31
31
  }
32
32
  ]
33
33
  }
@@ -17,7 +17,7 @@
17
17
  "audit": "pnpm audit --prod"
18
18
  },
19
19
  "dependencies": {
20
- "@daloyjs/core": "^0.38.2",
20
+ "@daloyjs/core": "^0.39.0",
21
21
  "zod": "^4.4.3"
22
22
  },
23
23
  "devDependencies": {
@@ -40,19 +40,15 @@ export function buildApp(): App {
40
40
  // `info.title` / `info.version` are pulled from package.json by default;
41
41
  // set `openapi.info` here to override them.
42
42
  openapi: {
43
- // Advertise the public origin so the Scalar "Try it" panel calls the
44
- // deployed URL (and stays within the connect-src 'self' CSP) instead of
45
- // localhost. Resolves PUBLIC_URL, then Railway's injected domain, then
46
- // the local dev port.
47
- servers: [
48
- {
49
- url:
50
- process.env.PUBLIC_URL ??
51
- (process.env.RAILWAY_PUBLIC_DOMAIN
52
- ? `https://${process.env.RAILWAY_PUBLIC_DOMAIN}`
53
- : `http://localhost:${process.env.PORT ?? 3000}`),
54
- },
55
- ],
43
+ // Leave `servers` unset so the Scalar "Try it" panel and the generated
44
+ // client target the origin the docs are served from — the deployed
45
+ // domain in production, localhost in dev which keeps "Try it" within
46
+ // the connect-src 'self' CSP on every platform. Never hardcode a URL
47
+ // here: a localhost default breaks "Try it" once deployed. Set PUBLIC_URL
48
+ // to pin an absolute base URL (e.g. for client codegen).
49
+ ...(process.env.PUBLIC_URL
50
+ ? { servers: [{ url: process.env.PUBLIC_URL }] }
51
+ : {}),
56
52
  },
57
53
  docs: true,
58
54
  // daloy-minimal:strip-end docs
@@ -1,6 +1,6 @@
1
1
  # AGENTS.md
2
2
 
3
- A [DaloyJS](https://daloyjs.dev) REST API deployed to **Cloudflare Workers**. **Contract-first**: routes are defined with Zod schemas and OpenAPI 3.1 is generated from them. This starter omits `docs: true` to keep the Worker bundle small — enable it explicitly via `new App({ docs: true })` to auto-mount `GET /openapi.json`, `GET /openapi.yaml`, and `GET /docs` (Scalar UI).
3
+ A [DaloyJS](https://daloyjs.dev) REST API deployed to **Cloudflare Workers**. **Contract-first**: routes are defined with Zod schemas and OpenAPI 3.1 is generated from them. `docs: true` is set in `new App({...})`, so `GET /openapi.json`, `GET /openapi.yaml`, and `GET /docs` (Scalar UI) are auto-mounted. DaloyJS is dependency-free and the Scalar UI loads from a CDN, so this adds negligible Worker bundle size; drop `docs` (and the `openapi` block) if you want the smallest possible bundle.
4
4
 
5
5
  - Package manager: pnpm (use `pnpm` unless the project's `package.json` was rewritten for npm/yarn/bun).
6
6
  - Runtime: Cloudflare Workers (Web Standard `Request`/`Response`).
@@ -70,12 +70,13 @@ pnpm audit # supply-chain audit
70
70
 
71
71
  Always run `pnpm typecheck` and `pnpm test` before declaring a task done.
72
72
 
73
- ## OpenAPI & docs routes (opt-in)
73
+ ## OpenAPI & docs routes
74
74
 
75
- This Worker starter omits `docs: true` to keep the bundle small (Workers
76
- have a hard size limit). When you opt in via `new App({ docs: true })`,
77
- three routes are auto-mounted off the spec generated from your route
78
- definitions:
75
+ This Worker starter sets `docs: true` in `new App({...})`, so three routes
76
+ are auto-mounted off the spec generated from your route definitions.
77
+ DaloyJS is dependency-free and the Scalar UI loads from a CDN, so the bundle
78
+ cost is negligible; drop `docs` (and the `openapi` block) if you need the
79
+ smallest possible Worker. The routes:
79
80
 
80
81
  - `GET /openapi.json` — OpenAPI 3.1 spec as JSON.
81
82
  - `GET /openapi.yaml` — OpenAPI 3.1 spec as YAML (served inline as
@@ -11,7 +11,7 @@
11
11
  "audit": "pnpm audit --prod"
12
12
  },
13
13
  "dependencies": {
14
- "@daloyjs/core": "^0.38.2",
14
+ "@daloyjs/core": "^0.39.0",
15
15
  "zod": "^4.4.3"
16
16
  },
17
17
  "devDependencies": {
@@ -6,6 +6,22 @@ const app = new App({
6
6
  bodyLimitBytes: 256 * 1024,
7
7
  requestTimeoutMs: 5_000,
8
8
  production: true,
9
+ // Cloudflare Workers always run behind Cloudflare's edge, which sets
10
+ // X-Forwarded-For. Declare that single trusted hop so DaloyJS reads the real
11
+ // client IP instead of refusing the (otherwise spoofable) header and
12
+ // returning 500 in production. Increase the hop count if you put an
13
+ // additional proxy in front of the Worker.
14
+ behindProxy: { hops: 1 },
15
+ // daloy-minimal:strip-start docs
16
+ // Auto-mounted docs (since `docs: true`): GET /openapi.json, /openapi.yaml,
17
+ // and /docs (Scalar UI). DaloyJS is dependency-free and the Scalar UI loads
18
+ // from a CDN, so this adds negligible Worker bundle size. Drop `docs` (and
19
+ // this `openapi` block) if you want the smallest possible bundle.
20
+ openapi: {
21
+ info: { title: "My Daloy Cloudflare API", version: "0.0.1" },
22
+ },
23
+ docs: true,
24
+ // daloy-minimal:strip-end docs
9
25
  });
10
26
 
11
27
  app.use(requestId());
@@ -8,10 +8,10 @@
8
8
  "gen:openapi": "deno run --allow-net --allow-env --allow-read --allow-write scripts/dump-openapi.ts"
9
9
  },
10
10
  "imports": {
11
- "@daloyjs/core": "jsr:@daloyjs/daloy@^0.38.2",
12
- "@daloyjs/core/banner": "jsr:@daloyjs/daloy@^0.38.2/banner",
13
- "@daloyjs/core/deno": "jsr:@daloyjs/daloy@^0.38.2/deno",
14
- "@daloyjs/core/openapi": "jsr:@daloyjs/daloy@^0.38.2/openapi",
11
+ "@daloyjs/core": "jsr:@daloyjs/daloy@^0.39.0",
12
+ "@daloyjs/core/banner": "jsr:@daloyjs/daloy@^0.39.0/banner",
13
+ "@daloyjs/core/deno": "jsr:@daloyjs/daloy@^0.39.0/deno",
14
+ "@daloyjs/core/openapi": "jsr:@daloyjs/daloy@^0.39.0/openapi",
15
15
  "zod": "npm:zod@^4.4.3"
16
16
  },
17
17
  "compilerOptions": {
@@ -41,19 +41,15 @@ export function buildApp(): App {
41
41
  // `info.title` / `info.version` are pulled from deno.json by default;
42
42
  // set `openapi.info` here to override them.
43
43
  openapi: {
44
- // Advertise the public origin so the Scalar "Try it" panel calls the
45
- // deployed URL (and stays within the connect-src 'self' CSP) instead of
46
- // localhost. Resolves PUBLIC_URL, then Railway's injected domain, then
47
- // the local dev port.
48
- servers: [
49
- {
50
- url:
51
- Deno.env.get("PUBLIC_URL") ??
52
- (Deno.env.get("RAILWAY_PUBLIC_DOMAIN")
53
- ? `https://${Deno.env.get("RAILWAY_PUBLIC_DOMAIN")}`
54
- : `http://localhost:${Deno.env.get("PORT") ?? "3000"}`),
55
- },
56
- ],
44
+ // Leave `servers` unset so the Scalar "Try it" panel and the generated
45
+ // client target the origin the docs are served from — the deployed
46
+ // domain in production, localhost in dev which keeps "Try it" within
47
+ // the connect-src 'self' CSP on every platform (Deno Deploy, etc.).
48
+ // Never hardcode a URL here: a localhost default breaks "Try it" once
49
+ // deployed. Set PUBLIC_URL to pin an absolute base URL (e.g. for codegen).
50
+ ...(Deno.env.get("PUBLIC_URL")
51
+ ? { servers: [{ url: Deno.env.get("PUBLIC_URL")! }] }
52
+ : {}),
57
53
  },
58
54
  docs: true,
59
55
  // daloy-minimal:strip-end docs
@@ -18,7 +18,7 @@
18
18
  "audit": "pnpm audit --prod"
19
19
  },
20
20
  "dependencies": {
21
- "@daloyjs/core": "^0.38.2",
21
+ "@daloyjs/core": "^0.39.0",
22
22
  "zod": "^4.4.3"
23
23
  },
24
24
  "devDependencies": {
@@ -45,19 +45,15 @@ export function buildApp(): App {
45
45
  // `info.title` / `info.version` are pulled from package.json by default;
46
46
  // set `openapi.info` here to override them.
47
47
  openapi: {
48
- // Advertise the public origin so the Scalar "Try it" panel calls the
49
- // deployed URL (and stays within the connect-src 'self' CSP) instead of
50
- // localhost. Resolves PUBLIC_URL, then Railway's injected domain, then
51
- // the local dev port.
52
- servers: [
53
- {
54
- url:
55
- process.env.PUBLIC_URL ??
56
- (process.env.RAILWAY_PUBLIC_DOMAIN
57
- ? `https://${process.env.RAILWAY_PUBLIC_DOMAIN}`
58
- : `http://localhost:${process.env.PORT ?? 3000}`),
59
- },
60
- ],
48
+ // Leave `servers` unset so the Scalar "Try it" panel and the generated
49
+ // client target the origin the docs are served from — the deployed
50
+ // domain in production, localhost in dev which keeps "Try it" within
51
+ // the connect-src 'self' CSP on every platform. Never hardcode a URL
52
+ // here: a localhost default breaks "Try it" once deployed. Set PUBLIC_URL
53
+ // to pin an absolute base URL (e.g. for client codegen).
54
+ ...(process.env.PUBLIC_URL
55
+ ? { servers: [{ url: process.env.PUBLIC_URL }] }
56
+ : {}),
61
57
  },
62
58
  docs: true,
63
59
  // daloy-minimal:strip-end docs
@@ -7,7 +7,7 @@ A [DaloyJS](https://daloyjs.dev) REST API deployed to **Vercel** on the **Node.j
7
7
 
8
8
  ## Commands
9
9
 
10
- - `pnpm dev` — local Vercel dev server on http://localhost:3000
10
+ - `pnpm dev` — local Node dev server (`src/dev.ts`) on http://localhost:3000 (no `vercel dev` / login needed; serves the same app the Vercel Function runs)
11
11
  - `pnpm typecheck` — `tsc --noEmit`
12
12
  - `pnpm test` — run test suite
13
13
  - `pnpm deploy` — deploy to Vercel
@@ -15,8 +15,9 @@ A [DaloyJS](https://daloyjs.dev) REST API deployed to **Vercel** on the **Node.j
15
15
 
16
16
  ## Project shape
17
17
 
18
- - `api/[...path].ts` — Vercel Node.js Functions entrypoint. Builds the `App`, registers routes/middleware, and exports `default toFetchHandler(app)` from `@daloyjs/core/vercel` (Node.js Functions expect a default export with a `fetch` method; Node.js is the default runtime, so no `runtime` export is needed). **Keep it a catch-all** so DaloyJS owns routing. If you specifically need the Edge runtime, add `export const runtime = "edge"` and switch to `default toWebHandler(app)`.
19
- - `vercel.json` — Vercel build/runtime configuration.
18
+ - `api/index.ts` — the single Vercel Node.js Functions entrypoint. Builds the `App`, registers routes/middleware, and exports `default toFetchHandler(app)` from `@daloyjs/core/vercel` (Node.js Functions expect a default export with a `fetch` method; Node.js is the default runtime, so no `runtime` export is needed). `vercel.json` rewrites every path (`/(.*)` → `/api`) to this one function, so DaloyJS owns all routing and the app's routes are served at the site root (`/healthz`, `/docs`, …), not under `/api/*`. If you specifically need the Edge runtime, add `export const runtime = "edge"` and switch to `default toWebHandler(app)`.
19
+ - `vercel.json` — Vercel config. The `rewrites` rule routing all paths to `/api` is **required** for root routing; do not remove it. (`functions` sets memory / maxDuration.)
20
+ - `src/dev.ts` — local Node dev server (`pnpm dev`). Imports the `app` exported from `api/index.ts` and serves it via `@daloyjs/core/node` at the root, so you get fast local iteration without `vercel dev`. Dev-only; it is not under `api/`, so Vercel never deploys it.
20
21
  - `tests/` — test files.
21
22
 
22
23
  ## Imports
@@ -24,7 +25,7 @@ A [DaloyJS](https://daloyjs.dev) REST API deployed to **Vercel** on the **Node.j
24
25
  This project uses TypeScript with `"allowImportingTsExtensions"`, so relative imports use the **`.ts` extension** — the actual file you see on disk:
25
26
 
26
27
  ```ts
27
- import handler from "../api/[...path].ts";
28
+ import handler from "../api/index.ts";
28
29
  ```
29
30
 
30
31
  You import the file you see. Vercel bundles the `api/` functions at deploy time and resolves `.ts` directly, and the test runner (tsx) does too. Bare-specifier imports from packages (`@daloyjs/core`, `zod`, …) do not need an extension.
@@ -37,7 +38,7 @@ You import the file you see. Vercel bundles the `api/` functions at deploy time
37
38
  4. Throw typed errors (`NotFoundError`, `BadRequestError`, etc.) from `@daloyjs/core`.
38
39
  5. Keep `requestId()`, `secureHeaders()`, and `rateLimit()` enabled. For production traffic, back rate-limiting with Vercel KV or another shared store (the in-memory limiter resets per instance).
39
40
  6. On the Node.js runtime the full Node API is available (`node:*`, `Buffer`, `fs`), but prefer Web Standards (`Request`/`Response`, `fetch`, Web Crypto) so the same app can also run on the Edge runtime or another adapter unchanged. If you opt into the Edge runtime, drop `node:` modules entirely.
40
- 7. The catch-all `api/[...path].ts` must remain a catch-all so DaloyJS handles routing.
41
+ 7. Keep a single `api/index.ts` entry and the `vercel.json` `/(.*)` → `/api` rewrite so DaloyJS handles all routing at the site root.
41
42
  8. Every new route ships with a test that covers a happy path and at least one unhappy path.
42
43
 
43
44
  ## Secure-by-default (do not let an AI strip these)
@@ -49,7 +50,7 @@ Per Supabase + Aikido on [secure-by-default development](https://www.aikido.dev/
49
50
  - Every protected route attaches an auth `beforeHandle` and ships an unhappy-path test proving an unauthenticated request returns `401` (and wrong scope returns `403`) — the HTTP-boundary equivalent of Supabase's pgTAP policy tests.
50
51
  - JWT verifiers keep an explicit `algorithms` allowlist; never trust the token's `alg` header, never allow `none`, always check `exp` / `nbf`.
51
52
  - Credential / HMAC comparisons use a constant-time comparison (the framework's `timingSafeEqual`), never `===`. Throw typed errors from `@daloyjs/core` so problem+json redacts in prod; never return raw stack traces.
52
- - Keep `api/[...path].ts` a catch-all so DaloyJS owns routing — do not split into per-path files that bypass the middleware chain.
53
+ - Keep the single `api/index.ts` entry and the `vercel.json` rewrite so DaloyJS owns routing — do not split into per-path files that bypass the middleware chain, and do not remove the rewrite (the root domain would 404).
53
54
  - `.env`, `.env.local`, secrets, private keys: never commit. Use `vercel env` for production secrets.
54
55
 
55
56
  ## Process expectations
@@ -27,7 +27,7 @@ curl http://localhost:3000/books/1
27
27
  - OpenAPI 3.1 YAML: <http://localhost:3000/openapi.yaml>
28
28
 
29
29
  After deploying, the same routes serve `/docs`, `/openapi.json`, and `/openapi.yaml` from your Vercel deployment URL.
30
- To brand Scalar, change `docs: true` in `api/[...path].ts` to `docs: { scalar: { theme, customCss } }`.
30
+ To brand Scalar, change `docs: true` in `api/index.ts` to `docs: { scalar: { theme, customCss } }`.
31
31
 
32
32
  <!-- daloy-minimal:strip-end docs -->
33
33
 
@@ -37,7 +37,7 @@ To brand Scalar, change `docs: true` in `api/[...path].ts` to `docs: { scalar: {
37
37
  pnpm deploy
38
38
  ```
39
39
 
40
- The API entry lives at `api/[...path].ts` and uses `@daloyjs/core/vercel`:
40
+ The API entry lives at `api/index.ts` and uses `@daloyjs/core/vercel`:
41
41
 
42
42
  ```ts
43
43
  import { toFetchHandler } from "@daloyjs/core/vercel";
@@ -59,14 +59,24 @@ export const runtime = "edge";
59
59
  export default toWebHandler(app);
60
60
  ```
61
61
 
62
- That catch-all API route lets DaloyJS own routing while Vercel handles the runtime.
62
+ `vercel.json` rewrites every path to this single function:
63
+
64
+ ```json
65
+ { "rewrites": [{ "source": "/(.*)", "destination": "/api" }] }
66
+ ```
67
+
68
+ So DaloyJS owns all routing and the app's routes are served at the **site root**
69
+ (`/healthz`, `/docs`, `/openapi.json`, …) rather than under `/api/*`. Without
70
+ this rewrite the function only answers `/api/*` and the root domain returns a
71
+ Vercel 404. (The demo defines no `/` route, so the bare root returns the app's
72
+ problem+json 404 — visit `/docs` or `/healthz`.)
63
73
 
64
74
  ## Imports
65
75
 
66
76
  This project uses TypeScript with `"allowImportingTsExtensions"`. Relative imports use the `.ts` extension — the actual file on disk:
67
77
 
68
78
  ```ts
69
- import handler from "../api/[...path].ts";
79
+ import handler from "../api/index.ts";
70
80
  ```
71
81
 
72
82
  Vercel bundles the `api/` functions at deploy time and resolves `.ts` directly, and the test runner (tsx) does too.
@@ -4,7 +4,7 @@ description: >-
4
4
  Best practices for building, testing, and hardening this DaloyJS REST API on
5
5
  Vercel (Node.js runtime). Use when adding or changing HTTP routes, Zod
6
6
  schemas, middleware, or error handling; regenerating the OpenAPI spec or the
7
- typed Hey API client; keeping the catch-all Vercel Functions entrypoint and
7
+ typed Hey API client; keeping the single Vercel Functions entrypoint and
8
8
  Web-Standard handler; or working on auth, rate limits, secrets, and the
9
9
  project's quality gates.
10
10
  license: MIT
@@ -49,22 +49,28 @@ DaloyJS is a **contract-first** framework. On Vercel, additionally:
49
49
  in-memory rate limiter resets per instance — for high-traffic
50
50
  deployments, back it with an external shared store (e.g. Upstash
51
51
  Redis).
52
- 6. **One catch-all entrypoint.** `api/[...path].ts` owns all routing so
53
- DaloyJS can generate a unified OpenAPI spec.
52
+ 6. **One entrypoint + a rewrite.** `api/index.ts` is the only function,
53
+ and `vercel.json` rewrites every path (`/(.*)` → `/api`) to it, so
54
+ DaloyJS owns all routing at the site root and generates a unified
55
+ OpenAPI spec. Removing the rewrite makes the root domain 404.
54
56
 
55
57
  ## Project shape
56
58
 
57
- - `api/[...path].ts` — the Vercel Functions entrypoint. Builds the `App`,
59
+ - `api/index.ts` — the Vercel Functions entrypoint. Builds the `App`,
58
60
  registers routes/middleware, and exports `default toFetchHandler(app)`
59
61
  (Node.js Functions expect a default export with a `fetch` method; Node.js
60
62
  is the default runtime, so no `runtime` export is needed).
61
- - `vercel.json` — Vercel build/runtime configuration.
63
+ - `vercel.json` — Vercel config. The `rewrites` rule (`/(.*)` → `/api`) is
64
+ required so DaloyJS routes at the site root; do not remove it.
65
+ - `src/dev.ts` — local Node dev server (`pnpm dev`). Serves the `app`
66
+ exported from `api/index.ts` via `@daloyjs/core/node`; dev-only, never
67
+ deployed (it lives outside `api/`).
62
68
  - `tests/` — test files (`*.test.ts`).
63
69
 
64
70
  ## Commands cheat-sheet
65
71
 
66
72
  ```bash
67
- pnpm dev # local Vercel dev server on http://localhost:3000
73
+ pnpm dev # local Node dev server (src/dev.ts) on http://localhost:3000
68
74
  pnpm typecheck # tsc --noEmit
69
75
  pnpm test # run test suite
70
76
  pnpm deploy # deploy to Vercel
@@ -93,7 +99,7 @@ from `@daloyjs/core/openapi`.
93
99
 
94
100
  ## Workflow: add a new route
95
101
 
96
- 1. **Open `api/[...path].ts`.**
102
+ 1. **Open `api/index.ts`.**
97
103
  2. **Design schemas first.** Use `z.object({...}).strict()` for inputs.
98
104
  3. **Call `app.route({...})`** with `method`, `path`, `operationId`,
99
105
  `tags`, `responses`, `handler` (plus `request` when accepting input).
@@ -162,7 +168,7 @@ needed for unit tests.
162
168
  ```ts
163
169
  import { test } from "node:test";
164
170
  import assert from "node:assert/strict";
165
- import handler from "../api/[...path].ts";
171
+ import handler from "../api/index.ts";
166
172
 
167
173
  // Either import the underlying app, or test via the handler's fetch
168
174
  // method (the default export is the Vercel `{ fetch }` object) by
@@ -213,10 +219,11 @@ Aim for **100% line and function coverage** on the routes you add.
213
219
 
214
220
  ## Pitfalls and guardrails
215
221
 
216
- - The catch-all `api/[...path].ts` must remain a catch-all so DaloyJS
217
- handles routing. Do not split routes into multiple Vercel API files
218
- unless the user explicitly asks (it disables shared middleware and a
219
- unified OpenAPI).
222
+ - Keep the single `api/index.ts` entry and the `vercel.json` `/(.*)`
223
+ `/api` rewrite so DaloyJS handles routing at the site root. Do not
224
+ remove the rewrite (the root domain would 404) and do not split routes
225
+ into multiple Vercel API files unless the user explicitly asks (it
226
+ disables shared middleware and a unified OpenAPI).
220
227
  - Use `toFetchHandler(app)` from `@daloyjs/core/vercel` for Node.js
221
228
  Functions — never hand-roll a `fetch(req)` adapter. If you opt into the
222
229
  Edge runtime, use `toWebHandler(app)` with `export const runtime = "edge"`.
@@ -6,15 +6,30 @@ import { toFetchHandler } from "@daloyjs/core/vercel";
6
6
  // recommends for standalone functions (it runs on Fluid Compute, with the
7
7
  // performance of the edge network but full Node API support). Node.js is the
8
8
  // default runtime, so no `runtime` export is needed. Vercel Node.js Functions
9
- // in `/api` expect a default export with a `fetch` method, which is exactly
10
- // what `toFetchHandler(app)` returns. If you specifically need the Edge runtime
9
+ // expect a default export with a `fetch` method, which is exactly what
10
+ // `toFetchHandler(app)` returns. If you specifically need the Edge runtime
11
11
  // instead, add `export const runtime = "edge"` and switch the default export to
12
12
  // `toWebHandler(app)` from "@daloyjs/core/vercel".
13
+ //
14
+ // This single function owns ALL routing: `vercel.json` rewrites every path to
15
+ // `/api`, and DaloyJS matches the original request path (`/healthz`, `/docs`,
16
+ // …). So the app's routes are served at the site root, not under `/api/*`.
17
+ // Do not split this into per-path files — keep one entry so the middleware
18
+ // chain and the generated OpenAPI spec stay unified.
13
19
 
14
- const app = new App({
20
+ // Exported so the local dev server (`src/dev.ts`, run by `pnpm dev`) can serve
21
+ // the same app over a plain Node listener. Vercel uses the default export below.
22
+ export const app = new App({
15
23
  bodyLimitBytes: 256 * 1024,
16
24
  requestTimeoutMs: 5_000,
17
25
  production: process.env.NODE_ENV === "production",
26
+ // Reverse-proxy posture. On Vercel, every request reaches the function
27
+ // through Vercel's edge, which sets `x-forwarded-for`. The app must declare
28
+ // that trusted hop, otherwise DaloyJS refuses the spoofable header and
29
+ // returns 500 in production. Vercel is exactly one edge hop, so the default
30
+ // is 1; override TRUST_PROXY_HOPS only if you put another proxy in front
31
+ // (e.g. set it to 2 behind Cloudflare -> Vercel).
32
+ behindProxy: { hops: Number(process.env.TRUST_PROXY_HOPS ?? "1") },
18
33
  // daloy-minimal:strip-start docs
19
34
  // Auto-mounted docs (when `docs: true`):
20
35
  // GET /openapi.json — OpenAPI 3.1 spec (JSON)
@@ -4,14 +4,14 @@
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "scripts": {
7
- "dev": "vercel dev",
7
+ "dev": "node --import tsx/esm src/dev.ts",
8
8
  "deploy": "vercel deploy",
9
9
  "typecheck": "tsc --noEmit",
10
10
  "test": "node --import tsx/esm --test tests/**/*.test.ts",
11
11
  "audit": "pnpm audit --prod"
12
12
  },
13
13
  "dependencies": {
14
- "@daloyjs/core": "^0.38.2",
14
+ "@daloyjs/core": "^0.39.0",
15
15
  "zod": "^4.4.3"
16
16
  },
17
17
  "devDependencies": {
@@ -0,0 +1,16 @@
1
+ // Local development server.
2
+ //
3
+ // Vercel runs `api/index.ts` as a Function in production; this serves the very
4
+ // same app over a plain Node listener so you get fast local iteration without
5
+ // `vercel dev` (no Vercel login, no edge emulation). Routes are served at the
6
+ // root here (`/healthz`, `/docs`, …), exactly as on the deployed site — in
7
+ // production the `vercel.json` rewrite maps every path to the Function.
8
+ //
9
+ // Run with: `pnpm dev` (or `npm run dev`).
10
+ import { serve } from "@daloyjs/core/node";
11
+ import { app } from "../api/index.ts";
12
+
13
+ const port = Number(process.env.PORT ?? 3000);
14
+ serve(app, { port });
15
+ // eslint-disable-next-line no-console
16
+ console.log(`DaloyJS dev server listening on http://localhost:${port}`);
@@ -1,6 +1,6 @@
1
1
  import assert from "node:assert/strict";
2
2
  import test from "node:test";
3
- import handler from "../api/[...path].ts";
3
+ import handler from "../api/index.ts";
4
4
 
5
5
  test("Vercel Node.js handler responds through DaloyJS", async () => {
6
6
  // Vercel Node.js Functions invoke the default export's `fetch` method.
@@ -15,5 +15,5 @@
15
15
  "noEmit": true,
16
16
  "allowImportingTsExtensions": true
17
17
  },
18
- "include": ["api/**/*.ts", "tests/**/*.ts"]
18
+ "include": ["api/**/*.ts", "src/**/*.ts", "tests/**/*.ts"]
19
19
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://openapi.vercel.sh/vercel.json",
3
- "cleanUrls": true,
3
+ "rewrites": [{ "source": "/(.*)", "destination": "/api" }],
4
4
  "functions": {
5
- "api/[...path].ts": { "memory": 1024, "maxDuration": 30 }
5
+ "api/index.ts": { "memory": 1024, "maxDuration": 30 }
6
6
  }
7
7
  }