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.
- package/README.md +13 -8
- package/bin/create-daloy.mjs +24 -4
- package/package.json +1 -1
- package/sbom.cdx.json +9 -9
- package/sbom.spdx.json +5 -5
- package/templates/bun-basic/_vscode/mcp.json +8 -0
- package/templates/bun-basic/package.json +1 -1
- package/templates/bun-basic/src/build-app.ts +22 -1
- package/templates/bun-basic/src/index.ts +4 -1
- package/templates/cloudflare-worker/AGENTS.md +1 -1
- package/templates/cloudflare-worker/_agents/skills/daloyjs-best-practices/SKILL.md +6 -5
- package/templates/cloudflare-worker/_vscode/mcp.json +8 -0
- package/templates/cloudflare-worker/package.json +1 -1
- package/templates/cloudflare-worker/src/index.ts +16 -0
- package/templates/deno-basic/_vscode/mcp.json +8 -0
- package/templates/deno-basic/deno.json +4 -4
- package/templates/deno-basic/src/build-app.ts +22 -1
- package/templates/node-basic/_vscode/mcp.json +8 -0
- package/templates/node-basic/package.json +1 -1
- package/templates/node-basic/src/build-app.ts +22 -1
- package/templates/node-basic/tsconfig.build.json +2 -1
- package/templates/{vercel-edge → vercel}/AGENTS.md +11 -10
- package/templates/{vercel-edge → vercel}/README.md +29 -15
- package/templates/{vercel-edge → vercel}/_Dockerfile +2 -2
- package/templates/{vercel-edge → vercel}/_agents/skills/daloyjs-best-practices/SKILL.md +65 -49
- package/templates/vercel/_vscode/mcp.json +8 -0
- package/templates/vercel/api/index.ts +89 -0
- package/templates/{vercel-edge → vercel}/package.json +2 -2
- package/templates/vercel/src/dev.ts +16 -0
- package/templates/vercel/tests/app.test.ts +10 -0
- package/templates/{vercel-edge → vercel}/tsconfig.json +1 -1
- package/templates/vercel/vercel.json +7 -0
- package/templates/vercel-edge/api/[...path].ts +0 -70
- package/templates/vercel-edge/tests/app.test.ts +0 -9
- package/templates/vercel-edge/vercel.json +0 -4
- /package/templates/{vercel-edge → vercel}/CLAUDE.md +0 -0
- /package/templates/{vercel-edge → vercel}/_dockerignore +0 -0
- /package/templates/{vercel-edge → vercel}/_env.example +0 -0
- /package/templates/{vercel-edge → vercel}/_gitignore +0 -0
- /package/templates/{vercel-edge → vercel}/_npmrc +0 -0
- /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
|
|
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
|
|
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
|
|
91
|
+
### `vercel`
|
|
92
92
|
|
|
93
|
-
A Vercel
|
|
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
|
|
97
|
-
-
|
|
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
|
|
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.
|
package/bin/create-daloy.mjs
CHANGED
|
@@ -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
|
|
27
|
-
title: "Vercel
|
|
28
|
-
description: "Catch-all Vercel
|
|
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
|
|
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
package/sbom.cdx.json
CHANGED
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"bomFormat": "CycloneDX",
|
|
3
3
|
"specVersion": "1.5",
|
|
4
|
-
"serialNumber": "urn:uuid:
|
|
4
|
+
"serialNumber": "urn:uuid:2a24975c-f4c4-542d-80c9-be6056ddc994",
|
|
5
5
|
"version": 1,
|
|
6
6
|
"metadata": {
|
|
7
|
-
"timestamp": "2026-06-
|
|
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.
|
|
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.
|
|
18
|
+
"bom-ref": "pkg:npm/create-daloy@0.38.3",
|
|
19
19
|
"name": "create-daloy",
|
|
20
|
-
"version": "0.38.
|
|
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.
|
|
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.
|
|
45
|
+
"tagId": "swidtag-create-daloy-0.38.3",
|
|
46
46
|
"name": "create-daloy",
|
|
47
|
-
"version": "0.38.
|
|
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.
|
|
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.
|
|
6
|
-
"documentNamespace": "https://github.com/daloyjs/daloy/sbom/create-daloy-0.38.
|
|
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-
|
|
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.
|
|
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.
|
|
30
|
+
"referenceLocator": "pkg:npm/create-daloy@0.38.3"
|
|
31
31
|
}
|
|
32
32
|
]
|
|
33
33
|
}
|
|
@@ -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
|
|
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
|
-
|
|
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.
|
|
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
|
|
73
|
+
## OpenAPI & docs routes
|
|
74
74
|
|
|
75
|
-
This Worker starter
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
|
@@ -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.
|
|
12
|
-
"@daloyjs/core/banner": "jsr:@daloyjs/daloy@^0.38.
|
|
13
|
-
"@daloyjs/core/deno": "jsr:@daloyjs/daloy@^0.38.
|
|
14
|
-
"@daloyjs/core/openapi": "jsr:@daloyjs/daloy@^0.38.
|
|
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
|
|
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
|
|
@@ -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
|
|
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
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# AGENTS.md
|
|
2
2
|
|
|
3
|
-
A [DaloyJS](https://daloyjs.dev) REST API deployed to **Vercel
|
|
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
|
|
6
|
+
- Runtime: Vercel Node.js Functions on Fluid Compute (Web Standard `Request`/`Response`).
|
|
7
7
|
|
|
8
8
|
## Commands
|
|
9
9
|
|
|
10
|
-
- `pnpm dev` — local
|
|
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/
|
|
19
|
-
- `vercel.json` — Vercel
|
|
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/
|
|
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.
|
|
40
|
-
7.
|
|
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 `
|
|
52
|
-
- Keep `api/
|
|
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
|
|
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
|
|
30
|
-
To brand Scalar, change `docs: true` in `api/
|
|
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/
|
|
40
|
+
The API entry lives at `api/index.ts` and uses `@daloyjs/core/vercel`:
|
|
41
41
|
|
|
42
42
|
```ts
|
|
43
|
-
|
|
44
|
-
|
|
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
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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 {
|
|
56
|
+
import { toWebHandler } from "@daloyjs/core/vercel";
|
|
54
57
|
|
|
55
|
-
export
|
|
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
|
-
|
|
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/
|
|
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
|
|
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
|
|
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
|
-
#
|
|
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
|
|
6
|
-
middleware, or error handling; regenerating the OpenAPI spec or the
|
|
7
|
-
Hey API client; keeping the
|
|
8
|
-
|
|
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
|
|
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
|
|
17
|
-
truth** for how to add routes, write tests,
|
|
18
|
-
the quality gates. Read this in full before
|
|
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
|
|
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
|
|
35
|
+
DaloyJS is a **contract-first** framework. On Vercel, additionally:
|
|
35
36
|
|
|
36
|
-
1. **
|
|
37
|
-
|
|
38
|
-
|
|
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,
|
|
47
|
-
|
|
48
|
-
6. **One
|
|
49
|
-
|
|
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/
|
|
54
|
-
routes/middleware, and exports `default
|
|
55
|
-
|
|
56
|
-
|
|
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
|
|
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
|
|
86
|
-
|
|
87
|
-
|
|
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/
|
|
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
|
|
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/
|
|
171
|
+
import handler from "../api/index.ts";
|
|
161
172
|
|
|
162
|
-
// Either import the underlying app, or test via the
|
|
163
|
-
//
|
|
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
|
|
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
|
|
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
|
-
-
|
|
191
|
-
adding heavy dependencies. Inspect bundle size during
|
|
192
|
-
|
|
193
|
-
|
|
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`
|
|
199
|
-
|
|
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
|
-
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
-
|
|
218
|
-
|
|
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,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": "
|
|
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.
|
|
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
|
+
});
|
|
@@ -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
|
-
});
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|