create-daloy 0.38.1 → 0.38.3

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.
Files changed (41) hide show
  1. package/README.md +13 -8
  2. package/bin/create-daloy.mjs +24 -4
  3. package/package.json +1 -1
  4. package/sbom.cdx.json +9 -9
  5. package/sbom.spdx.json +5 -5
  6. package/templates/bun-basic/_vscode/mcp.json +8 -0
  7. package/templates/bun-basic/package.json +1 -1
  8. package/templates/bun-basic/src/build-app.ts +22 -1
  9. package/templates/bun-basic/src/index.ts +4 -1
  10. package/templates/cloudflare-worker/AGENTS.md +1 -1
  11. package/templates/cloudflare-worker/_agents/skills/daloyjs-best-practices/SKILL.md +6 -5
  12. package/templates/cloudflare-worker/_vscode/mcp.json +8 -0
  13. package/templates/cloudflare-worker/package.json +1 -1
  14. package/templates/cloudflare-worker/src/index.ts +16 -0
  15. package/templates/deno-basic/_vscode/mcp.json +8 -0
  16. package/templates/deno-basic/deno.json +4 -4
  17. package/templates/deno-basic/src/build-app.ts +22 -1
  18. package/templates/node-basic/_vscode/mcp.json +8 -0
  19. package/templates/node-basic/package.json +1 -1
  20. package/templates/node-basic/src/build-app.ts +22 -1
  21. package/templates/node-basic/tsconfig.build.json +2 -1
  22. package/templates/{vercel-edge → vercel}/AGENTS.md +11 -10
  23. package/templates/{vercel-edge → vercel}/README.md +29 -15
  24. package/templates/{vercel-edge → vercel}/_Dockerfile +2 -2
  25. package/templates/{vercel-edge → vercel}/_agents/skills/daloyjs-best-practices/SKILL.md +65 -49
  26. package/templates/vercel/_vscode/mcp.json +8 -0
  27. package/templates/vercel/api/index.ts +89 -0
  28. package/templates/{vercel-edge → vercel}/package.json +2 -2
  29. package/templates/vercel/src/dev.ts +16 -0
  30. package/templates/vercel/tests/app.test.ts +10 -0
  31. package/templates/{vercel-edge → vercel}/tsconfig.json +1 -1
  32. package/templates/vercel/vercel.json +7 -0
  33. package/templates/vercel-edge/api/[...path].ts +0 -70
  34. package/templates/vercel-edge/tests/app.test.ts +0 -9
  35. package/templates/vercel-edge/vercel.json +0 -4
  36. /package/templates/{vercel-edge → vercel}/CLAUDE.md +0 -0
  37. /package/templates/{vercel-edge → vercel}/_dockerignore +0 -0
  38. /package/templates/{vercel-edge → vercel}/_env.example +0 -0
  39. /package/templates/{vercel-edge → vercel}/_gitignore +0 -0
  40. /package/templates/{vercel-edge → vercel}/_npmrc +0 -0
  41. /package/templates/{vercel-edge → vercel}/pnpm-workspace.yaml +0 -0
package/README.md CHANGED
@@ -22,7 +22,7 @@ bun create daloy my-api
22
22
  The CLI is interactive when arguments are missing. It will ask you for:
23
23
 
24
24
  - A project directory name (defaults to `my-daloy-app`)
25
- - A template (`node-basic`, `vercel-edge`, `cloudflare-worker`, `bun-basic`, or `deno-basic`)
25
+ - A template (`node-basic`, `vercel`, `cloudflare-worker`, `bun-basic`, or `deno-basic`)
26
26
  - A package manager (`pnpm`, `npm`, `yarn`, or `bun`) — not asked for the
27
27
  `deno-basic` runtime template
28
28
  - Whether to install dependencies
@@ -50,7 +50,7 @@ pnpm create daloy@latest my-api \
50
50
 
51
51
  | Flag | Description |
52
52
  | --- | --- |
53
- | `--template <name>` | `node-basic` (default), `vercel-edge`, `cloudflare-worker`, `bun-basic`, or `deno-basic`. |
53
+ | `--template <name>` | `node-basic` (default), `vercel`, `cloudflare-worker`, `bun-basic`, or `deno-basic`. (`vercel-edge` is a deprecated alias for `vercel`.) |
54
54
  | `--package-manager <pm>` | `pnpm` (default), `npm`, `yarn`, or `bun`. Ignored for `deno-basic`. |
55
55
  | `--list-templates` | Print available templates with descriptions. |
56
56
  | `--install` / `--no-install` | Install dependencies after scaffolding. Defaults to **Y** for npm/yarn/bun and **N** for pnpm so you can review the hardened `.npmrc` / `pnpm-workspace.yaml` and aren't blocked by the 24h `minimumReleaseAge` embargo on the first run. |
@@ -88,17 +88,20 @@ A minimal Cloudflare Worker bootstrap using `@daloyjs/core/cloudflare` with:
88
88
  - `secureHeaders` and `requestId` enabled by default, with smaller edge-friendly body and timeout limits.
89
89
  - A Zod-validated `/healthz` route and contract-first `/books/:id` route exposed via `toFetchHandler(app)`.
90
90
 
91
- ### `vercel-edge`
91
+ ### `vercel`
92
92
 
93
- A Vercel Edge API bootstrap using `@daloyjs/core/vercel` with:
93
+ A Vercel API bootstrap on the **Node.js runtime** (Vercel's recommended runtime
94
+ for standalone functions, on Fluid Compute) using `@daloyjs/core/vercel` with:
94
95
 
95
96
  - `api/[...path].ts` catch-all routing so DaloyJS owns the API surface.
96
- - `export const config = { runtime: "edge" }` ready for Vercel Edge.
97
- - Node.js migration notes using Vercel's default `{ fetch }` export shape.
97
+ - `export default toFetchHandler(app)` the `{ fetch }` shape Vercel Node.js Functions expect (no `runtime` export needed; Node.js is the default).
98
+ - Notes for opting into the Edge runtime (`export const runtime = "edge"` + `toWebHandler(app)`) if you specifically need it.
98
99
  - `vercel dev` / `vercel deploy` scripts.
99
- - `secureHeaders` and `requestId` enabled by default, with smaller edge-friendly body and timeout limits.
100
+ - `secureHeaders` and `requestId` enabled by default, with smaller serverless-friendly body and timeout limits.
100
101
  - A health route and bookstore route mirroring the Node starter.
101
102
 
103
+ > The previous template name `vercel-edge` still works as a deprecated alias for `vercel`.
104
+
102
105
  ### `bun-basic`
103
106
 
104
107
  A [Bun](https://bun.sh) runtime starter using `@daloyjs/core/bun` with:
@@ -260,7 +263,7 @@ not need.
260
263
  - Zero runtime dependencies (uses only Node built-ins) for a clean supply-chain footprint.
261
264
  - A modern terminal experience with Unicode/color capability detection and ASCII fallbacks.
262
265
  - Templates are copied verbatim from this package's `templates/` directory.
263
- - Files and folders prefixed with `_` are renamed on copy (`_gitignore` → `.gitignore`, `_npmrc` → `.npmrc`, `_github/` → `.github`, `_agents/` → `.agents/`, `_Dockerfile` → `Dockerfile`, `_dockerignore` → `.dockerignore`, `_env.example` → `.env.example`) to survive npm packing.
266
+ - Files and folders prefixed with `_` are renamed on copy (`_gitignore` → `.gitignore`, `_npmrc` → `.npmrc`, `_github/` → `.github`, `_agents/` → `.agents/`, `_vscode/` → `.vscode/`, `_Dockerfile` → `Dockerfile`, `_dockerignore` → `.dockerignore`, `_env.example` → `.env.example`) to survive npm packing.
264
267
  - pnpm-specific `.npmrc` hardening is kept only when you choose `pnpm`; other package managers get a clean project without unsupported config warnings.
265
268
  - pnpm projects ship with `ignore-scripts=true`, `minimum-release-age=1440`, `verify-store-integrity=true`, `prefer-frozen-lockfile=true`, and `strict-peer-dependencies=true` by default.
266
269
  - `--with-ci` projects ship with pinned GitHub Actions workflows, CODEOWNERS, Dependabot, SECURITY.md, and lockfile-source verification.
@@ -274,3 +277,5 @@ Every scaffolded project ships with two files that help AI coding agents (Copilo
274
277
  - `.agents/skills/daloyjs-best-practices/SKILL.md` — comprehensive operational guidance following the open `agents/skills/<skill-name>/SKILL.md` convention: when to use the skill, project structure, core workflows (adding routes, regenerating the OpenAPI spec and client), schema and validation conventions, error-handling patterns, middleware order, testing best practices (happy and unhappy paths), security best practices, logging and observability notes, configuration and secrets handling, deployment notes, pitfalls and guardrails, and process expectations.
275
278
 
276
279
  Both files are tailored to the chosen template (Node, Bun, Deno, Vercel Edge, or Cloudflare Workers), and Node-style templates rewrite their commands to match your selected package manager. They follow the "instruction budget" advice — small root file, progressive disclosure for the rest — so they don't waste agent tokens. Edit or delete them freely; the framework does not depend on them at runtime.
280
+
281
+ Every scaffold also ships a `.vscode/mcp.json` (authored as `_vscode/mcp.json` in the template) that wires VS Code and other MCP-aware editors to the DaloyJS documentation MCP server (`https://daloyjs.dev/mcp`) over HTTP. AI assistants in your editor can then pull current DaloyJS docs with no manual setup. Delete the file or remove the server entry to opt out; it is editor configuration only and the framework does not depend on it at runtime.
@@ -23,9 +23,9 @@ const TEMPLATE_OPTIONS = [
23
23
  description: "Traditional REST API with secure defaults and Hey API codegen",
24
24
  },
25
25
  {
26
- value: "vercel-edge",
27
- title: "Vercel Edge",
28
- description: "Catch-all Vercel Edge route with Node.js migration notes",
26
+ value: "vercel",
27
+ title: "Vercel",
28
+ description: "Catch-all Vercel Functions REST API on the Node.js runtime (Fluid Compute)",
29
29
  },
30
30
  {
31
31
  value: "cloudflare-worker",
@@ -54,6 +54,15 @@ const PACKAGE_MANAGER_OPTIONS = [
54
54
  const TEMPLATES = TEMPLATE_OPTIONS.map((option) => option.value);
55
55
  const PACKAGE_MANAGERS = PACKAGE_MANAGER_OPTIONS.map((option) => option.value);
56
56
 
57
+ // Deprecated template names kept as aliases so existing
58
+ // `--template <old>` commands (and published docs/blog posts) keep working.
59
+ // Each maps to its current canonical template value.
60
+ const TEMPLATE_ALIASES = new Map([
61
+ // `vercel-edge` shipped before Vercel deprecated standalone Edge Functions;
62
+ // the template now targets the recommended Node.js runtime as `vercel`.
63
+ ["vercel-edge", "vercel"],
64
+ ]);
65
+
57
66
  const RENAME_ON_COPY = new Map([
58
67
  ["_gitignore", ".gitignore"],
59
68
  ["_npmrc", ".npmrc"],
@@ -65,6 +74,11 @@ const RENAME_ON_COPY = new Map([
65
74
  // `.agents/skills/<skill-name>/SKILL.md`. Templates author this as
66
75
  // `_agents/` so npm pack does not drop the dotfolder during publish.
67
76
  ["_agents", ".agents"],
77
+ // Directory: holds editor configuration such as `mcp.json`, which wires
78
+ // VS Code (and compatible editors) to the DaloyJS docs MCP server.
79
+ // Templates author this as `_vscode/` so npm pack does not drop the
80
+ // dotfolder during publish.
81
+ ["_vscode", ".vscode"],
68
82
  ]);
69
83
 
70
84
  // Templates that target a runtime instead of an npm package manager.
@@ -972,7 +986,7 @@ function cloudflareDeploySteps(packageManager) {
972
986
  }
973
987
 
974
988
  function renderDeployConfig({ template, packageManager, needsBunRuntime }) {
975
- if (template === "vercel-edge") {
989
+ if (template === "vercel") {
976
990
  return {
977
991
  header: vercelDeployHeader(),
978
992
  jobName: "Deploy to Vercel",
@@ -1669,6 +1683,12 @@ async function main() {
1669
1683
  if (!template) {
1670
1684
  template = rl ? await askChoice(rl, "Choose a starter template:", TEMPLATE_OPTIONS, "node-basic") : "node-basic";
1671
1685
  }
1686
+ // Resolve deprecated template aliases (e.g. `vercel-edge` -> `vercel`).
1687
+ if (TEMPLATE_ALIASES.has(template)) {
1688
+ const canonical = TEMPLATE_ALIASES.get(template);
1689
+ logWarn(`Template "${template}" is deprecated; using "${canonical}" instead.`);
1690
+ template = canonical;
1691
+ }
1672
1692
  if (!TEMPLATES.includes(template)) {
1673
1693
  logError(`Unknown template "${template}". Available: ${TEMPLATES.join(", ")}`);
1674
1694
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-daloy",
3
- "version": "0.38.1",
3
+ "version": "0.38.3",
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:5874d6d1-4f7c-5889-b1dc-59abaf86bc40",
4
+ "serialNumber": "urn:uuid:2a24975c-f4c4-542d-80c9-be6056ddc994",
5
5
  "version": 1,
6
6
  "metadata": {
7
- "timestamp": "2026-06-11T09:54:26.989Z",
7
+ "timestamp": "2026-06-16T19:48:12.143Z",
8
8
  "tools": [
9
9
  {
10
10
  "vendor": "DaloyJS",
11
11
  "name": "daloy-generate-sbom",
12
- "version": "0.38.1"
12
+ "version": "0.38.3"
13
13
  }
14
14
  ],
15
15
  "authors": [],
16
16
  "component": {
17
17
  "type": "library",
18
- "bom-ref": "pkg:npm/create-daloy@0.38.1",
18
+ "bom-ref": "pkg:npm/create-daloy@0.38.3",
19
19
  "name": "create-daloy",
20
- "version": "0.38.1",
20
+ "version": "0.38.3",
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.1",
22
+ "purl": "pkg:npm/create-daloy@0.38.3",
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.1",
45
+ "tagId": "swidtag-create-daloy-0.38.3",
46
46
  "name": "create-daloy",
47
- "version": "0.38.1",
47
+ "version": "0.38.3",
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.1",
56
+ "ref": "pkg:npm/create-daloy@0.38.3",
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.1",
6
- "documentNamespace": "https://github.com/daloyjs/daloy/sbom/create-daloy-0.38.1-5874d6d1-4f7c-5889-b1dc-59abaf86bc40",
5
+ "name": "create-daloy-0.38.3",
6
+ "documentNamespace": "https://github.com/daloyjs/daloy/sbom/create-daloy-0.38.3-2a24975c-f4c4-542d-80c9-be6056ddc994",
7
7
  "creationInfo": {
8
- "created": "2026-06-11T09:54:26.989Z",
8
+ "created": "2026-06-16T19:48:12.143Z",
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.1",
19
+ "versionInfo": "0.38.3",
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.1"
30
+ "referenceLocator": "pkg:npm/create-daloy@0.38.3"
31
31
  }
32
32
  ]
33
33
  }
@@ -0,0 +1,8 @@
1
+ {
2
+ "servers": {
3
+ "daloyjs-docs": {
4
+ "type": "http",
5
+ "url": "https://daloyjs.dev/mcp"
6
+ }
7
+ }
8
+ }
@@ -17,7 +17,7 @@
17
17
  "audit": "pnpm audit --prod"
18
18
  },
19
19
  "dependencies": {
20
- "@daloyjs/core": "^0.38.1",
20
+ "@daloyjs/core": "^0.38.3",
21
21
  "zod": "^4.4.3"
22
22
  },
23
23
  "devDependencies": {
@@ -18,6 +18,19 @@ export function buildApp(): App {
18
18
  bodyLimitBytes: 1024 * 1024,
19
19
  requestTimeoutMs: 5_000,
20
20
  production: process.env.NODE_ENV === "production",
21
+ // Reverse-proxy posture. When the app runs behind a trusted edge proxy
22
+ // (Railway, Render, Fly, Heroku, a single nginx / load balancer), set the
23
+ // TRUST_PROXY_HOPS env var to the number of proxy hops in front of it — a
24
+ // single PaaS edge is 1. DaloyJS then reads the real client IP from the
25
+ // matching X-Forwarded-For slot (used by rateLimit, requestId, and audit
26
+ // logs). Leave it unset when the app is exposed directly to the public
27
+ // internet: DaloyJS refuses to honor spoofable X-Forwarded-* headers
28
+ // (returning 500 on the first forwarded request) rather than trust a
29
+ // header an attacker can set. See the DaloyJS deployment guide for the
30
+ // per-platform hop counts.
31
+ ...(process.env.TRUST_PROXY_HOPS
32
+ ? { behindProxy: { hops: Number(process.env.TRUST_PROXY_HOPS) } }
33
+ : {}),
21
34
  // daloy-minimal:strip-start docs
22
35
  // Auto-mounted docs (when `docs: true`):
23
36
  // GET /openapi.json — OpenAPI 3.1 spec (JSON)
@@ -27,7 +40,15 @@ export function buildApp(): App {
27
40
  // `info.title` / `info.version` are pulled from package.json by default;
28
41
  // set `openapi.info` here to override them.
29
42
  openapi: {
30
- servers: [{ url: `http://localhost:${process.env.PORT ?? 3000}` }],
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
+ : {}),
31
52
  },
32
53
  docs: true,
33
54
  // daloy-minimal:strip-end docs
@@ -23,4 +23,7 @@ const links: StartupBannerLink[] = [
23
23
 
24
24
  printStartupBanner({ name: "DaloyJS API", url, runtime: "Bun", links });
25
25
 
26
- export default app;
26
+ // NOTE: This module intentionally has no default export. Bun auto-starts a
27
+ // server from any module whose default-exported object has a `fetch` method,
28
+ // which would collide with the explicit `serve()` call above on the same port
29
+ // (EADDRINUSE) and crash on startup (Railway's "Uncaught exception" loop).
@@ -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
@@ -0,0 +1,8 @@
1
+ {
2
+ "servers": {
3
+ "daloyjs-docs": {
4
+ "type": "http",
5
+ "url": "https://daloyjs.dev/mcp"
6
+ }
7
+ }
8
+ }
@@ -11,7 +11,7 @@
11
11
  "audit": "pnpm audit --prod"
12
12
  },
13
13
  "dependencies": {
14
- "@daloyjs/core": "^0.38.1",
14
+ "@daloyjs/core": "^0.38.3",
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());
@@ -0,0 +1,8 @@
1
+ {
2
+ "servers": {
3
+ "daloyjs-docs": {
4
+ "type": "http",
5
+ "url": "https://daloyjs.dev/mcp"
6
+ }
7
+ }
8
+ }
@@ -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.1",
12
- "@daloyjs/core/banner": "jsr:@daloyjs/daloy@^0.38.1/banner",
13
- "@daloyjs/core/deno": "jsr:@daloyjs/daloy@^0.38.1/deno",
14
- "@daloyjs/core/openapi": "jsr:@daloyjs/daloy@^0.38.1/openapi",
11
+ "@daloyjs/core": "jsr:@daloyjs/daloy@^0.38.3",
12
+ "@daloyjs/core/banner": "jsr:@daloyjs/daloy@^0.38.3/banner",
13
+ "@daloyjs/core/deno": "jsr:@daloyjs/daloy@^0.38.3/deno",
14
+ "@daloyjs/core/openapi": "jsr:@daloyjs/daloy@^0.38.3/openapi",
15
15
  "zod": "npm:zod@^4.4.3"
16
16
  },
17
17
  "compilerOptions": {
@@ -19,6 +19,19 @@ export function buildApp(): App {
19
19
  bodyLimitBytes: 1024 * 1024,
20
20
  requestTimeoutMs: 5_000,
21
21
  production: Deno.env.get("DENO_ENV") === "production",
22
+ // Reverse-proxy posture. When the app runs behind a trusted edge proxy
23
+ // (Railway, Render, Fly, Heroku, a single nginx / load balancer), set the
24
+ // TRUST_PROXY_HOPS env var to the number of proxy hops in front of it — a
25
+ // single PaaS edge is 1. DaloyJS then reads the real client IP from the
26
+ // matching X-Forwarded-For slot (used by rateLimit, requestId, and audit
27
+ // logs). Leave it unset when the app is exposed directly to the public
28
+ // internet: DaloyJS refuses to honor spoofable X-Forwarded-* headers
29
+ // (returning 500 on the first forwarded request) rather than trust a
30
+ // header an attacker can set. See the DaloyJS deployment guide for the
31
+ // per-platform hop counts.
32
+ ...(Deno.env.get("TRUST_PROXY_HOPS")
33
+ ? { behindProxy: { hops: Number(Deno.env.get("TRUST_PROXY_HOPS")) } }
34
+ : {}),
22
35
  // daloy-minimal:strip-start docs
23
36
  // Auto-mounted docs (when `docs: true`):
24
37
  // GET /openapi.json — OpenAPI 3.1 spec (JSON)
@@ -28,7 +41,15 @@ export function buildApp(): App {
28
41
  // `info.title` / `info.version` are pulled from deno.json by default;
29
42
  // set `openapi.info` here to override them.
30
43
  openapi: {
31
- servers: [{ url: `http://localhost:${Deno.env.get("PORT") ?? "3000"}` }],
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
+ : {}),
32
53
  },
33
54
  docs: true,
34
55
  // daloy-minimal:strip-end docs
@@ -0,0 +1,8 @@
1
+ {
2
+ "servers": {
3
+ "daloyjs-docs": {
4
+ "type": "http",
5
+ "url": "https://daloyjs.dev/mcp"
6
+ }
7
+ }
8
+ }
@@ -18,7 +18,7 @@
18
18
  "audit": "pnpm audit --prod"
19
19
  },
20
20
  "dependencies": {
21
- "@daloyjs/core": "^0.38.1",
21
+ "@daloyjs/core": "^0.38.3",
22
22
  "zod": "^4.4.3"
23
23
  },
24
24
  "devDependencies": {
@@ -20,6 +20,19 @@ export function buildApp(): App {
20
20
  bodyLimitBytes: 1024 * 1024,
21
21
  requestTimeoutMs: 5_000,
22
22
  production: process.env.NODE_ENV === "production",
23
+ // Reverse-proxy posture. When the app runs behind a trusted edge proxy
24
+ // (Railway, Render, Fly, Heroku, a single nginx / load balancer), set the
25
+ // TRUST_PROXY_HOPS env var to the number of proxy hops in front of it — a
26
+ // single PaaS edge is 1. DaloyJS then reads the real client IP from the
27
+ // matching X-Forwarded-For slot (used by rateLimit, requestId, and audit
28
+ // logs). Leave it unset when the app is exposed directly to the public
29
+ // internet: DaloyJS refuses to honor spoofable X-Forwarded-* headers
30
+ // (returning 500 on the first forwarded request) rather than trust a
31
+ // header an attacker can set. See the DaloyJS deployment guide for the
32
+ // per-platform hop counts.
33
+ ...(process.env.TRUST_PROXY_HOPS
34
+ ? { behindProxy: { hops: Number(process.env.TRUST_PROXY_HOPS) } }
35
+ : {}),
23
36
  // daloy-minimal:strip-start docs
24
37
  // Auto-mounted docs (when `docs: true`):
25
38
  // GET /openapi.json — OpenAPI 3.1 spec (JSON)
@@ -32,7 +45,15 @@ export function buildApp(): App {
32
45
  // `info.title` / `info.version` are pulled from package.json by default;
33
46
  // set `openapi.info` here to override them.
34
47
  openapi: {
35
- servers: [{ url: `http://localhost:${process.env.PORT ?? 3000}` }],
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
+ : {}),
36
57
  },
37
58
  docs: true,
38
59
  // daloy-minimal:strip-end docs
@@ -3,8 +3,9 @@
3
3
  "compilerOptions": {
4
4
  "declaration": true,
5
5
  "outDir": "dist",
6
+ "rootDir": "src",
6
7
  "sourceMap": true
7
8
  },
8
- "include": ["src/**/*", "scripts/**/*"],
9
+ "include": ["src/**/*"],
9
10
  "exclude": ["node_modules", "dist", "tests"]
10
11
  }
@@ -1,13 +1,13 @@
1
1
  # AGENTS.md
2
2
 
3
- A [DaloyJS](https://daloyjs.dev) REST API deployed to **Vercel Edge**. **Contract-first**: routes are defined with Zod schemas and OpenAPI 3.1 is generated from them. When `docs: true` is set in `new App({...})`, three routes are auto-mounted: `GET /openapi.json`, `GET /openapi.yaml`, and `GET /docs` (Scalar UI).
3
+ A [DaloyJS](https://daloyjs.dev) REST API deployed to **Vercel** on the **Node.js runtime** (Vercel's recommended runtime for standalone functions, running on Fluid Compute). **Contract-first**: routes are defined with Zod schemas and OpenAPI 3.1 is generated from them. When `docs: true` is set in `new App({...})`, three routes are auto-mounted: `GET /openapi.json`, `GET /openapi.yaml`, and `GET /docs` (Scalar UI).
4
4
 
5
5
  - Package manager: pnpm (use `pnpm` unless the project's `package.json` was rewritten for npm/yarn/bun).
6
- - Runtime: Vercel Edge (Web Standard `Request`/`Response`).
6
+ - Runtime: Vercel Node.js Functions on Fluid Compute (Web Standard `Request`/`Response`).
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 Edge**. **Contrac
15
15
 
16
16
  ## Project shape
17
17
 
18
- - `api/[...path].ts` — Vercel Edge entrypoint. Builds the `App`, registers routes/middleware, and exports `default toWebHandler(app)` plus `export const config = { runtime: "edge" }`. **Keep it a catch-all** so DaloyJS owns routing. For Vercel's recommended Node.js runtime, remove the Edge config and export `default toFetchHandler(app)` from `@daloyjs/core/vercel`.
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 Edge**. **Contrac
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.
@@ -36,8 +37,8 @@ You import the file you see. Vercel bundles the `api/` functions at deploy time
36
37
  3. Preserve literal types in responses: `status: 200 as const`, `z.literal(...)` on discriminator fields.
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
- 6. Stay on the Edge runtime: only Web Standards APIs. No `node:` modules, no `fs`, no `Buffer`. If a feature requires Node, switch to a Node-runtime template.
40
- 7. The catch-all `api/[...path].ts` must remain a catch-all so DaloyJS handles routing.
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.
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)
@@ -48,8 +49,8 @@ Per Supabase + Aikido on [secure-by-default development](https://www.aikido.dev/
48
49
  - Keep Zod `.strict()` on top-level request objects; do not switch to `.passthrough()`. Keep `responses[N].body` schemas tight; never widen to `z.any()` to let a privileged field escape.
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
- - Credential / HMAC comparisons use `crypto.subtle.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.
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.
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
@@ -1,6 +1,6 @@
1
1
  # my-daloy-vercel-api
2
2
 
3
- A [DaloyJS](https://daloyjs.dev) Vercel Edge API starter.
3
+ A [DaloyJS](https://daloyjs.dev) Vercel API starter on the Node.js runtime.
4
4
 
5
5
  ## Develop
6
6
 
@@ -26,8 +26,8 @@ curl http://localhost:3000/books/1
26
26
  - OpenAPI 3.1 JSON: <http://localhost:3000/openapi.json>
27
27
  - OpenAPI 3.1 YAML: <http://localhost:3000/openapi.yaml>
28
28
 
29
- After deploying, the same routes serve `/docs`, `/openapi.json`, and `/openapi.yaml` from your Vercel Edge URL.
30
- To brand Scalar, change `docs: true` in `api/[...path].ts` to `docs: { scalar: { theme, customCss } }`.
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/index.ts` to `docs: { scalar: { theme, customCss } }`.
31
31
 
32
32
  <!-- daloy-minimal:strip-end docs -->
33
33
 
@@ -37,32 +37,46 @@ 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
- export const config = { runtime: "edge" };
44
- export default toWebHandler(app);
43
+ import { toFetchHandler } from "@daloyjs/core/vercel";
44
+
45
+ // Node.js is the default runtime — no `runtime` export needed.
46
+ export default toFetchHandler(app);
45
47
  ```
46
48
 
47
- This starter defaults to Vercel's Edge runtime for compatibility with the
48
- `vercel-edge` template name. Vercel now recommends Node.js for new projects;
49
- for Node.js Functions, remove the `config` export and use the official default
50
- `{ fetch }` shape instead:
49
+ This starter targets Vercel's Node.js runtime (on Fluid Compute), which Vercel
50
+ now recommends for standalone functions. Node.js Functions expect a default
51
+ export with a `fetch` method, which is exactly what `toFetchHandler(app)`
52
+ returns. If you specifically need the Edge runtime, add the `runtime` export and
53
+ switch to the bare web handler:
51
54
 
52
55
  ```ts
53
- import { toFetchHandler } from "@daloyjs/core/vercel";
56
+ import { toWebHandler } from "@daloyjs/core/vercel";
54
57
 
55
- export default toFetchHandler(app);
58
+ export const runtime = "edge";
59
+ export default toWebHandler(app);
60
+ ```
61
+
62
+ `vercel.json` rewrites every path to this single function:
63
+
64
+ ```json
65
+ { "rewrites": [{ "source": "/(.*)", "destination": "/api" }] }
56
66
  ```
57
67
 
58
- That catch-all API route lets DaloyJS own routing while Vercel handles the runtime.
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`.)
59
73
 
60
74
  ## Imports
61
75
 
62
76
  This project uses TypeScript with `"allowImportingTsExtensions"`. Relative imports use the `.ts` extension — the actual file on disk:
63
77
 
64
78
  ```ts
65
- import handler from "../api/[...path].ts";
79
+ import handler from "../api/index.ts";
66
80
  ```
67
81
 
68
82
  Vercel bundles the `api/` functions at deploy time and resolves `.ts` directly, and the test runner (tsx) does too.
@@ -70,7 +84,7 @@ Vercel bundles the `api/` functions at deploy time and resolves `.ts` directly,
70
84
  ## What's included
71
85
 
72
86
  - `@daloyjs/core/vercel` with starter security middleware: `secureHeaders` and `requestId`.
73
- - Smaller edge-friendly body and timeout limits in the generated app.
87
+ - Smaller serverless-friendly body and timeout limits in the generated app.
74
88
  <!-- daloy-minimal:strip-start books -->
75
89
  - A health route and a contract-first `/books/:id` route with Zod validation.
76
90
  <!-- daloy-minimal:strip-end books -->
@@ -1,8 +1,8 @@
1
1
  # syntax=docker/dockerfile:1.7
2
- # Containerized local-dev / CI environment for a DaloyJS Vercel Edge API.
2
+ # Containerized local-dev / CI environment for a DaloyJS Vercel API.
3
3
  #
4
4
  # Production deploys go through `vercel deploy` to Vercel's managed
5
- # Edge runtime — **this Dockerfile is not a production runtime**. Ship
5
+ # Node.js runtime — **this Dockerfile is not a production runtime**. Ship
6
6
  # the function to Vercel; use this image for:
7
7
  # - Reproducible local dev (devcontainers, GitHub Codespaces).
8
8
  # - CI smoke tests that exercise the handler against `vercel dev`
@@ -2,20 +2,21 @@
2
2
  name: daloyjs-best-practices
3
3
  description: >-
4
4
  Best practices for building, testing, and hardening this DaloyJS REST API on
5
- Vercel Edge. Use when adding or changing HTTP routes, Zod schemas,
6
- middleware, or error handling; regenerating the OpenAPI spec or the typed
7
- Hey API client; keeping the catch-all Edge entrypoint and Web-Standard
8
- runtime constraints; or working on auth, rate limits, secrets, and the
5
+ Vercel (Node.js runtime). Use when adding or changing HTTP routes, Zod
6
+ schemas, middleware, or error handling; regenerating the OpenAPI spec or the
7
+ typed Hey API client; keeping the single Vercel Functions entrypoint and
8
+ Web-Standard handler; or working on auth, rate limits, secrets, and the
9
9
  project's quality gates.
10
10
  license: MIT
11
11
  ---
12
12
 
13
- # SKILL.md — DaloyJS best practices (Vercel Edge)
13
+ # SKILL.md — DaloyJS best practices (Vercel, Node.js runtime)
14
14
 
15
15
  Operational guidance and best practices for AI coding agents working in this
16
- DaloyJS **Vercel Edge** project. This is the project's **single source of
17
- truth** for how to add routes, write tests, ship secure defaults, and run
18
- the quality gates. Read this in full before making non-trivial changes.
16
+ DaloyJS **Vercel** project on the **Node.js runtime** (Fluid Compute). This is
17
+ the project's **single source of truth** for how to add routes, write tests,
18
+ ship secure defaults, and run the quality gates. Read this in full before
19
+ making non-trivial changes.
19
20
 
20
21
  ## When to use this skill
21
22
 
@@ -24,18 +25,21 @@ Use this skill when you need to:
24
25
  - Add, modify, or remove HTTP routes in this project.
25
26
  - Adjust middleware, validation, or error handling.
26
27
  - Run tests or typecheck the project.
27
- - Deploy or troubleshoot the Edge runtime entrypoint.
28
+ - Deploy or troubleshoot the Vercel Functions entrypoint.
28
29
  - Harden the API (auth, CORS, rate limits, secrets, dependency hygiene).
29
30
 
30
31
  Do **not** use this skill for tasks unrelated to the API itself.
31
32
 
32
33
  ## Core principles
33
34
 
34
- DaloyJS is a **contract-first** framework. On Vercel Edge, additionally:
35
+ DaloyJS is a **contract-first** framework. On Vercel, additionally:
35
36
 
36
- 1. **Stay on the Edge runtime.** Only Web Standards APIs (no `node:`
37
- modules, no `fs`, no `Buffer`). If a feature requires Node APIs, the
38
- user must switch to a Node template.
37
+ 1. **Node.js runtime by default.** The full Node API is available
38
+ (`node:*`, `Buffer`, `fs`), but prefer Web Standards (`Request` /
39
+ `Response`, `fetch`, Web Crypto) so the same app can also run on the
40
+ Edge runtime or another adapter unchanged. Opt into Edge only when you
41
+ need it (`export const runtime = "edge"` + `toWebHandler(app)`), and
42
+ then drop `node:` modules.
39
43
  2. **The route definition is the contract.** Method, path, request
40
44
  schemas, and response schemas live in one place (`app.route({...})`).
41
45
  3. **Zod schemas validate at every boundary.**
@@ -43,23 +47,30 @@ DaloyJS is a **contract-first** framework. On Vercel Edge, additionally:
43
47
  5. **Secure by default.** `requestId()`, `secureHeaders()`, and
44
48
  `rateLimit()` are registered before route definitions. Note the
45
49
  in-memory rate limiter resets per instance — for high-traffic
46
- deployments, prefer Vercel's native rate-limiting (e.g.
47
- `@vercel/edge` + KV) or an external store.
48
- 6. **One catch-all entrypoint.** `api/[...path].ts` owns all routing so
49
- DaloyJS can generate a unified OpenAPI spec.
50
+ deployments, back it with an external shared store (e.g. Upstash
51
+ Redis).
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.
50
56
 
51
57
  ## Project shape
52
58
 
53
- - `api/[...path].ts` — the Edge entrypoint. Builds the `App`, registers
54
- routes/middleware, and exports `default toWebHandler(app)` plus
55
- `export const config = { runtime: "edge" }`.
56
- - `vercel.json` Vercel build/runtime configuration.
59
+ - `api/index.ts` — the Vercel Functions entrypoint. Builds the `App`,
60
+ registers routes/middleware, and exports `default toFetchHandler(app)`
61
+ (Node.js Functions expect a default export with a `fetch` method; Node.js
62
+ is the default runtime, so no `runtime` export is needed).
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/`).
57
68
  - `tests/` — test files (`*.test.ts`).
58
69
 
59
70
  ## Commands cheat-sheet
60
71
 
61
72
  ```bash
62
- pnpm dev # local Vercel dev server on http://localhost:3000
73
+ pnpm dev # local Node dev server (src/dev.ts) on http://localhost:3000
63
74
  pnpm typecheck # tsc --noEmit
64
75
  pnpm test # run test suite
65
76
  pnpm deploy # deploy to Vercel
@@ -82,13 +93,13 @@ definitions:
82
93
  Customize via `docs: { openapiPath, openapiYamlPath, path, ui }`. Set
83
94
  `openapiYamlPath: false` to disable just the YAML route, `docs: "auto"` to
84
95
  mount only outside production, or `docs: false` to disable all three.
85
- On Vercel Edge the YAML serializer is pure-string (no Node deps) and
86
- adds <1KB to the bundle. For hand-rolled mounting, `openapiToYAML` is
87
- exported from `@daloyjs/core/openapi`.
96
+ On Vercel the YAML serializer is pure-string (no extra deps) and adds
97
+ <1KB to the bundle. For hand-rolled mounting, `openapiToYAML` is exported
98
+ from `@daloyjs/core/openapi`.
88
99
 
89
100
  ## Workflow: add a new route
90
101
 
91
- 1. **Open `api/[...path].ts`.**
102
+ 1. **Open `api/index.ts`.**
92
103
  2. **Design schemas first.** Use `z.object({...}).strict()` for inputs.
93
104
  3. **Call `app.route({...})`** with `method`, `path`, `operationId`,
94
105
  `tags`, `responses`, `handler` (plus `request` when accepting input).
@@ -151,18 +162,19 @@ Add CORS only when needed, with an explicit `origin` allowlist.
151
162
 
152
163
  ## Testing best practices
153
164
 
154
- Tests use in-process `app.request(...)` — no port, no Edge runtime
165
+ Tests use in-process `app.request(...)` — no port, no Vercel runtime
155
166
  needed for unit tests.
156
167
 
157
168
  ```ts
158
169
  import { test } from "node:test";
159
170
  import assert from "node:assert/strict";
160
- import handler from "../api/[...path].ts";
171
+ import handler from "../api/index.ts";
161
172
 
162
- // Either import the underlying app, or test via the Edge handler's
163
- // fetch interface by passing a Web Request.
173
+ // Either import the underlying app, or test via the handler's fetch
174
+ // method (the default export is the Vercel `{ fetch }` object) by
175
+ // passing a Web Request.
164
176
  test("GET /healthz returns ok", async () => {
165
- const res = await handler(new Request("http://local/healthz"));
177
+ const res = await handler.fetch(new Request("http://local/healthz"));
166
178
  assert.equal(res.status, 200);
167
179
  });
168
180
  ```
@@ -180,23 +192,25 @@ Aim for **100% line and function coverage** on the routes you add.
180
192
  production traffic, back rate-limiting with Vercel KV or another
181
193
  shared store so limits apply across instances.
182
194
  - Never log secrets — filter `authorization`, `cookie`, etc.
183
- - Read secrets from `process.env` (available on Edge). Validate via Zod
184
- at module load.
195
+ - Read secrets from `process.env` (available on Node.js Functions).
196
+ Validate via Zod at module load.
185
197
  - For auth, verify JWT signatures with the Web Crypto API
186
- (`crypto.subtle`). Never trust the `alg` header from the token.
198
+ (`crypto.subtle`, available on both Node.js and Edge). Never trust the
199
+ `alg` header from the token.
187
200
  - Validate redirects against an allowlist.
188
201
  - Set `bodyLimitBytes` and `requestTimeoutMs` on `new App({...})` to
189
202
  mitigate DoS.
190
- - Edge functions have small bundle and CPU limits; be cautious about
191
- adding heavy dependencies. Inspect bundle size during deploy.
192
- - Pin Vercel project settings (regions, runtime version) explicitly in
193
- `vercel.json` rather than relying on dashboard defaults.
203
+ - Serverless functions still have bundle-size and cold-start costs; be
204
+ cautious about adding heavy dependencies. Inspect bundle size during
205
+ deploy.
206
+ - Pin Vercel project settings (regions, memory, maxDuration) explicitly
207
+ in `vercel.json` rather than relying on dashboard defaults.
194
208
 
195
209
  ## Logging & observability
196
210
 
197
211
  - Use `ctx.log` — it carries the request id.
198
- - `console.log` on Edge shows up in Vercel's runtime logs; the framework
199
- logger emits structured JSON for log aggregators.
212
+ - `console.log` shows up in Vercel's runtime logs; the framework logger
213
+ emits structured JSON for log aggregators.
200
214
 
201
215
  ## Configuration & secrets
202
216
 
@@ -205,17 +219,19 @@ Aim for **100% line and function coverage** on the routes you add.
205
219
 
206
220
  ## Pitfalls and guardrails
207
221
 
208
- - The catch-all `api/[...path].ts` must remain a catch-all so DaloyJS
209
- handles routing. Do not split routes into multiple Vercel API files
210
- unless the user explicitly asks (it disables shared middleware and a
211
- unified OpenAPI).
212
- - Use `toWebHandler(app)` from `@daloyjs/core/vercel` for Edge — never
213
- hand-roll a `fetch(req)` adapter. For Vercel's recommended Node.js
214
- runtime, remove the Edge config and export `default toFetchHandler(app)`.
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).
227
+ - Use `toFetchHandler(app)` from `@daloyjs/core/vercel` for Node.js
228
+ Functions never hand-roll a `fetch(req)` adapter. If you opt into the
229
+ Edge runtime, use `toWebHandler(app)` with `export const runtime = "edge"`.
215
230
  - Do not import `@daloyjs/core/node`, `@daloyjs/core/bun`, etc. — only
216
231
  `@daloyjs/core` and `@daloyjs/core/vercel`.
217
- - Avoid Node-only APIs (`Buffer`, `fs`, full `process` API). If a
218
- feature needs Node, switch to a Node-runtime template.
232
+ - Node APIs (`Buffer`, `fs`, full `process`) are available on the Node.js
233
+ runtime, but keep handlers Web-Standard where practical so the app can
234
+ also run on the Edge runtime unchanged.
219
235
  - Do not weaken response literal types (`as const`).
220
236
  - Do not return errors as `{ status: 4xx, body }`. Throw a typed error.
221
237
  - Do not add runtime dependencies without checking the hardened `.npmrc` (installs wait 24h after publish by default).
@@ -0,0 +1,8 @@
1
+ {
2
+ "servers": {
3
+ "daloyjs-docs": {
4
+ "type": "http",
5
+ "url": "https://daloyjs.dev/mcp"
6
+ }
7
+ }
8
+ }
@@ -0,0 +1,89 @@
1
+ import { z } from "zod";
2
+ import { App, NotFoundError, requestId, secureHeaders } from "@daloyjs/core";
3
+ import { toFetchHandler } from "@daloyjs/core/vercel";
4
+
5
+ // This template targets Vercel's Node.js runtime — the runtime Vercel now
6
+ // recommends for standalone functions (it runs on Fluid Compute, with the
7
+ // performance of the edge network but full Node API support). Node.js is the
8
+ // default runtime, so no `runtime` export is needed. Vercel Node.js Functions
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
+ // instead, add `export const runtime = "edge"` and switch the default export to
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.
19
+
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({
23
+ bodyLimitBytes: 256 * 1024,
24
+ requestTimeoutMs: 5_000,
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") },
33
+ // daloy-minimal:strip-start docs
34
+ // Auto-mounted docs (when `docs: true`):
35
+ // GET /openapi.json — OpenAPI 3.1 spec (JSON)
36
+ // GET /openapi.yaml — OpenAPI 3.1 spec (YAML, served inline as text/yaml)
37
+ // GET /docs — Scalar API reference UI that loads the spec
38
+ openapi: {
39
+ info: { title: "My Daloy Vercel API", version: "0.0.1" },
40
+ },
41
+ docs: true,
42
+ // daloy-minimal:strip-end docs
43
+ });
44
+
45
+ app.use(requestId());
46
+ app.use(secureHeaders());
47
+
48
+ app.route({
49
+ method: "GET",
50
+ path: "/healthz",
51
+ operationId: "healthz",
52
+ tags: ["Ops"],
53
+ responses: {
54
+ 200: {
55
+ description: "Service is healthy",
56
+ body: z.object({ ok: z.literal(true), runtime: z.literal("vercel") }),
57
+ },
58
+ },
59
+ handler: async () => ({
60
+ status: 200,
61
+ body: { ok: true as const, runtime: "vercel" as const },
62
+ }),
63
+ });
64
+
65
+ // daloy-minimal:strip-start books
66
+ const Book = z.object({ id: z.string(), title: z.string() });
67
+ const books = new Map<string, z.infer<typeof Book>>([
68
+ ["1", { id: "1", title: "Noli Me Tangere" }],
69
+ ]);
70
+
71
+ app.route({
72
+ method: "GET",
73
+ path: "/books/:id",
74
+ operationId: "getBookById",
75
+ tags: ["Books"],
76
+ request: { params: z.object({ id: z.string() }) },
77
+ responses: {
78
+ 200: { description: "Found", body: Book },
79
+ 404: { description: "Not found" },
80
+ },
81
+ handler: async ({ params }) => {
82
+ const book = books.get(params.id);
83
+ if (!book) throw new NotFoundError(`Book ${params.id} not found`);
84
+ return { status: 200, body: book };
85
+ },
86
+ });
87
+ // daloy-minimal:strip-end books
88
+
89
+ export default toFetchHandler(app);
@@ -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.1",
14
+ "@daloyjs/core": "^0.38.3",
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}`);
@@ -0,0 +1,10 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import handler from "../api/index.ts";
4
+
5
+ test("Vercel Node.js handler responds through DaloyJS", async () => {
6
+ // Vercel Node.js Functions invoke the default export's `fetch` method.
7
+ const response = await handler.fetch(new Request("https://example.test/healthz"));
8
+ assert.equal(response.status, 200);
9
+ assert.equal((await response.json()).runtime, "vercel");
10
+ });
@@ -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
  }
@@ -0,0 +1,7 @@
1
+ {
2
+ "$schema": "https://openapi.vercel.sh/vercel.json",
3
+ "rewrites": [{ "source": "/(.*)", "destination": "/api" }],
4
+ "functions": {
5
+ "api/index.ts": { "memory": 1024, "maxDuration": 30 }
6
+ }
7
+ }
@@ -1,70 +0,0 @@
1
- import { z } from "zod";
2
- import { App, NotFoundError, requestId, secureHeaders } from "@daloyjs/core";
3
- import { toWebHandler } from "@daloyjs/core/vercel";
4
-
5
- // This template defaults to Vercel's Edge runtime for compatibility with the
6
- // existing `vercel-edge` starter. For Vercel's recommended Node.js runtime,
7
- // remove this config and export `toFetchHandler(app)` from @daloyjs/core/vercel.
8
- export const config = { runtime: "edge" };
9
-
10
- const app = new App({
11
- bodyLimitBytes: 256 * 1024,
12
- requestTimeoutMs: 5_000,
13
- production: process.env.NODE_ENV === "production",
14
- // daloy-minimal:strip-start docs
15
- // Auto-mounted docs (when `docs: true`):
16
- // GET /openapi.json — OpenAPI 3.1 spec (JSON)
17
- // GET /openapi.yaml — OpenAPI 3.1 spec (YAML, served inline as text/yaml)
18
- // GET /docs — Scalar API reference UI that loads the spec
19
- openapi: {
20
- info: { title: "My Daloy Edge API", version: "0.0.1" },
21
- },
22
- docs: true,
23
- // daloy-minimal:strip-end docs
24
- });
25
-
26
- app.use(requestId());
27
- app.use(secureHeaders());
28
-
29
- app.route({
30
- method: "GET",
31
- path: "/healthz",
32
- operationId: "healthz",
33
- tags: ["Ops"],
34
- responses: {
35
- 200: {
36
- description: "Service is healthy",
37
- body: z.object({ ok: z.literal(true), runtime: z.literal("vercel-edge") }),
38
- },
39
- },
40
- handler: async () => ({
41
- status: 200,
42
- body: { ok: true as const, runtime: "vercel-edge" as const },
43
- }),
44
- });
45
-
46
- // daloy-minimal:strip-start books
47
- const Book = z.object({ id: z.string(), title: z.string() });
48
- const books = new Map<string, z.infer<typeof Book>>([
49
- ["1", { id: "1", title: "Noli Me Tangere" }],
50
- ]);
51
-
52
- app.route({
53
- method: "GET",
54
- path: "/books/:id",
55
- operationId: "getBookById",
56
- tags: ["Books"],
57
- request: { params: z.object({ id: z.string() }) },
58
- responses: {
59
- 200: { description: "Found", body: Book },
60
- 404: { description: "Not found" },
61
- },
62
- handler: async ({ params }) => {
63
- const book = books.get(params.id);
64
- if (!book) throw new NotFoundError(`Book ${params.id} not found`);
65
- return { status: 200, body: book };
66
- },
67
- });
68
- // daloy-minimal:strip-end books
69
-
70
- export default toWebHandler(app);
@@ -1,9 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import test from "node:test";
3
- import handler from "../api/[...path].ts";
4
-
5
- test("Vercel Edge handler responds through DaloyJS", async () => {
6
- const response = await handler(new Request("https://example.test/healthz"));
7
- assert.equal(response.status, 200);
8
- assert.equal((await response.json()).runtime, "vercel-edge");
9
- });
@@ -1,4 +0,0 @@
1
- {
2
- "version": 2,
3
- "cleanUrls": true
4
- }
File without changes
File without changes
File without changes
File without changes
File without changes