create-daloy 0.1.18 → 0.1.20
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 +8 -5
- package/bin/create-daloy.mjs +14 -1
- package/package.json +1 -1
- package/templates/bun-basic/AGENTS.md +29 -8
- package/templates/bun-basic/README.md +1 -1
- package/templates/bun-basic/_agents/skills/daloyjs-best-practices/SKILL.md +233 -0
- package/templates/cloudflare-worker/AGENTS.md +29 -7
- package/templates/cloudflare-worker/README.md +6 -0
- package/templates/cloudflare-worker/_agents/skills/daloyjs-best-practices/SKILL.md +236 -0
- package/templates/deno-basic/AGENTS.md +29 -8
- package/templates/deno-basic/README.md +1 -1
- package/templates/deno-basic/_agents/skills/daloyjs-best-practices/SKILL.md +225 -0
- package/templates/node-basic/AGENTS.md +27 -6
- package/templates/node-basic/README.md +1 -1
- package/templates/node-basic/_agents/skills/daloyjs-best-practices/SKILL.md +298 -0
- package/templates/node-basic/package.json +1 -1
- package/templates/node-basic/tsconfig.build.json +10 -0
- package/templates/node-basic/tsconfig.json +3 -6
- package/templates/vercel-edge/AGENTS.md +27 -7
- package/templates/vercel-edge/README.md +11 -0
- package/templates/vercel-edge/_agents/skills/daloyjs-best-practices/SKILL.md +204 -0
- package/templates/bun-basic/SKILL.md +0 -68
- package/templates/cloudflare-worker/SKILL.md +0 -68
- package/templates/deno-basic/SKILL.md +0 -71
- package/templates/node-basic/SKILL.md +0 -70
- package/templates/vercel-edge/SKILL.md +0 -64
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
# SKILL.md — DaloyJS best practices (Cloudflare Workers)
|
|
2
|
+
|
|
3
|
+
Operational guidance and best practices for AI coding agents working in this
|
|
4
|
+
DaloyJS **Cloudflare Workers** project. This is the project's **single
|
|
5
|
+
source of truth** for how to add routes, write tests, ship secure defaults,
|
|
6
|
+
and run the quality gates. Read this in full before making non-trivial
|
|
7
|
+
changes.
|
|
8
|
+
|
|
9
|
+
## When to use this skill
|
|
10
|
+
|
|
11
|
+
Use this skill when you need to:
|
|
12
|
+
|
|
13
|
+
- Add, modify, or remove HTTP routes in this Worker.
|
|
14
|
+
- Adjust middleware, validation, or error handling.
|
|
15
|
+
- Change Worker bindings (KV, D1, R2, Queues, env vars) in `wrangler.toml`.
|
|
16
|
+
- Run tests/typecheck or deploy the Worker.
|
|
17
|
+
- Harden the API (auth, CORS, rate limits, secrets, dependency hygiene).
|
|
18
|
+
|
|
19
|
+
Do **not** use this skill for tasks unrelated to the API itself.
|
|
20
|
+
|
|
21
|
+
## Core principles
|
|
22
|
+
|
|
23
|
+
DaloyJS is a **contract-first** framework. On Workers, additionally:
|
|
24
|
+
|
|
25
|
+
1. **Stay on the Workers runtime.** Only Web Standards APIs and
|
|
26
|
+
Cloudflare-specific bindings. No `node:` modules unless
|
|
27
|
+
`nodejs_compat` is enabled in `wrangler.toml` and the user explicitly
|
|
28
|
+
opts in.
|
|
29
|
+
2. **The route definition is the contract.** Method, path, request
|
|
30
|
+
schemas, and response schemas live in one place (`app.route({...})`).
|
|
31
|
+
3. **Zod schemas validate at every boundary.**
|
|
32
|
+
4. **Preserve literal types.** Return `status: 200 as const`.
|
|
33
|
+
5. **Secure by default.** `requestId()`, `secureHeaders()`, and
|
|
34
|
+
`rateLimit()` are registered. Note: the in-memory rate limiter resets
|
|
35
|
+
per isolate — for production traffic, prefer Cloudflare's native
|
|
36
|
+
rate-limit binding.
|
|
37
|
+
6. **Bindings flow through `env`.** Read KV/D1/R2/secrets from the
|
|
38
|
+
`env` argument to `fetch`, never from globals.
|
|
39
|
+
|
|
40
|
+
## Project shape
|
|
41
|
+
|
|
42
|
+
- `src/index.ts` — the Worker entrypoint. Builds the `App`, registers
|
|
43
|
+
routes/middleware, and exports `default { fetch: toFetchHandler(app) }`
|
|
44
|
+
from `@daloyjs/core/cloudflare`.
|
|
45
|
+
- `wrangler.toml` — Worker config (name, compatibility date, bindings,
|
|
46
|
+
routes).
|
|
47
|
+
- `tests/` — test files using Workers-compatible test runners (e.g.
|
|
48
|
+
`vitest` + `@cloudflare/vitest-pool-workers`) or in-process
|
|
49
|
+
`app.request(...)` for pure logic.
|
|
50
|
+
|
|
51
|
+
## Commands cheat-sheet
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pnpm dev # wrangler dev on http://localhost:8787
|
|
55
|
+
pnpm typecheck # tsc --noEmit
|
|
56
|
+
pnpm test # run test suite
|
|
57
|
+
pnpm deploy # wrangler deploy
|
|
58
|
+
pnpm audit # supply-chain audit
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Always run `pnpm typecheck` and `pnpm test` before declaring a task done.
|
|
62
|
+
|
|
63
|
+
## Workflow: add a new route
|
|
64
|
+
|
|
65
|
+
1. **Open `src/index.ts`.**
|
|
66
|
+
2. **Design schemas first.** Use `z.object({...}).strict()` for inputs.
|
|
67
|
+
3. **Call `app.route({...})`** with `method`, `path`, `operationId`,
|
|
68
|
+
`tags`, `responses`, `handler` (plus `request` when accepting input).
|
|
69
|
+
4. **Return `{ status, body, headers? }`** with `status: 200 as const`.
|
|
70
|
+
5. **Throw typed errors** (`NotFoundError`, `BadRequestError`, etc.).
|
|
71
|
+
6. **Add a test** under `tests/`. Use `app.request(...)` for pure logic;
|
|
72
|
+
use `unstable_dev` (Wrangler) or `@cloudflare/vitest-pool-workers`
|
|
73
|
+
when you need bindings.
|
|
74
|
+
7. **Run the quality gates**: `pnpm typecheck && pnpm test`.
|
|
75
|
+
|
|
76
|
+
### Example: a typed route with bindings
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
import { z } from "zod";
|
|
80
|
+
import { App, NotFoundError, rateLimit, requestId, secureHeaders } from "@daloyjs/core";
|
|
81
|
+
import { toFetchHandler } from "@daloyjs/core/cloudflare";
|
|
82
|
+
|
|
83
|
+
interface Env {
|
|
84
|
+
BOOKS: KVNamespace;
|
|
85
|
+
JWT_SECRET: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const Book = z.object({ id: z.string(), title: z.string() }).strict();
|
|
89
|
+
|
|
90
|
+
function buildApp(env: Env) {
|
|
91
|
+
const app = new App({ bodyLimitBytes: 1024 * 1024, requestTimeoutMs: 5_000 });
|
|
92
|
+
app.use(requestId());
|
|
93
|
+
app.use(secureHeaders());
|
|
94
|
+
app.use(rateLimit({ windowMs: 60_000, max: 120 }));
|
|
95
|
+
|
|
96
|
+
app.route({
|
|
97
|
+
method: "GET",
|
|
98
|
+
path: "/books/:id",
|
|
99
|
+
operationId: "getBookById",
|
|
100
|
+
tags: ["Books"],
|
|
101
|
+
request: { params: z.object({ id: z.string().min(1) }).strict() },
|
|
102
|
+
responses: {
|
|
103
|
+
200: { description: "Found", body: Book },
|
|
104
|
+
404: { description: "Not found" },
|
|
105
|
+
},
|
|
106
|
+
handler: async ({ params }) => {
|
|
107
|
+
const raw = await env.BOOKS.get(params.id, "json");
|
|
108
|
+
if (!raw) throw new NotFoundError(`Book ${params.id} not found`);
|
|
109
|
+
return { status: 200 as const, body: Book.parse(raw) };
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
return app;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export default {
|
|
117
|
+
fetch: (req: Request, env: Env, ctx: ExecutionContext) =>
|
|
118
|
+
toFetchHandler(buildApp(env))(req, env, ctx),
|
|
119
|
+
};
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Validation & schema conventions
|
|
123
|
+
|
|
124
|
+
- **Inputs**: use `.strict()` on top-level object schemas.
|
|
125
|
+
- **IDs**: prefer `z.string().min(1)`; use `z.string().uuid()` when
|
|
126
|
+
applicable.
|
|
127
|
+
- **Numbers from query strings**: `z.coerce.number().int().min(...)`.
|
|
128
|
+
- **Optional vs nullable**: differ in OpenAPI output.
|
|
129
|
+
- **Pagination**: standardize on `{ items, nextCursor }` cursor
|
|
130
|
+
pagination.
|
|
131
|
+
- **Discriminated unions**: `z.discriminatedUnion("kind", [...])`.
|
|
132
|
+
|
|
133
|
+
## Error handling
|
|
134
|
+
|
|
135
|
+
- Throw typed errors from `@daloyjs/core` — they serialize to RFC 7807
|
|
136
|
+
problem responses.
|
|
137
|
+
- Add a `responses[code]` entry for every error you throw.
|
|
138
|
+
- For unexpected errors, let them bubble. The framework's error
|
|
139
|
+
middleware converts them to a 500 problem response.
|
|
140
|
+
|
|
141
|
+
## Middleware
|
|
142
|
+
|
|
143
|
+
Register middleware **before** route definitions. Order matters.
|
|
144
|
+
|
|
145
|
+
Keep the secure baseline (`requestId`, `secureHeaders`, `rateLimit`).
|
|
146
|
+
Add CORS only when needed, with an explicit `origin` allowlist.
|
|
147
|
+
|
|
148
|
+
## Working with bindings
|
|
149
|
+
|
|
150
|
+
1. Add the binding (`[[kv_namespaces]]`, `[[d1_databases]]`, `[vars]`,
|
|
151
|
+
etc.) to `wrangler.toml`.
|
|
152
|
+
2. Type the binding in the `Env` interface inside `src/index.ts`.
|
|
153
|
+
3. Pass `env` into `buildApp(env)` so handlers receive bindings via
|
|
154
|
+
closure or factory argument. **Never read bindings via globals.**
|
|
155
|
+
4. Store secrets via `wrangler secret put` — they appear on `env` but
|
|
156
|
+
are not committed to `wrangler.toml`.
|
|
157
|
+
|
|
158
|
+
## Testing best practices
|
|
159
|
+
|
|
160
|
+
Two patterns:
|
|
161
|
+
|
|
162
|
+
- **In-process** with `app.request(...)` for pure logic that does not
|
|
163
|
+
need bindings.
|
|
164
|
+
- **Workers-aware** runners (`@cloudflare/vitest-pool-workers` or
|
|
165
|
+
Wrangler `unstable_dev`) when KV/D1/etc. are involved.
|
|
166
|
+
|
|
167
|
+
Cover **happy paths and unhappy paths** for every route: valid input,
|
|
168
|
+
validation failures (400), auth failures (401/403), not-found (404),
|
|
169
|
+
conflict (409), rate limiting (429). For external services, inject an
|
|
170
|
+
in-memory fake into `buildApp(env)` during tests.
|
|
171
|
+
|
|
172
|
+
Aim for **100% line and function coverage** on the routes you add.
|
|
173
|
+
|
|
174
|
+
## Security best practices
|
|
175
|
+
|
|
176
|
+
- Keep `secureHeaders()`, `requestId()`, and `rateLimit()` enabled. For
|
|
177
|
+
high-traffic routes, attach Cloudflare's native rate-limit binding so
|
|
178
|
+
limits are shared across isolates.
|
|
179
|
+
- Never log secrets — filter `authorization`, `cookie`, etc.
|
|
180
|
+
- Read secrets via `wrangler secret put`, never via plain `[vars]` in
|
|
181
|
+
`wrangler.toml`.
|
|
182
|
+
- For auth, verify JWT signatures with the Web Crypto API
|
|
183
|
+
(`crypto.subtle`). Never trust the `alg` header from the token.
|
|
184
|
+
- Validate redirects against an allowlist.
|
|
185
|
+
- Set `bodyLimitBytes` and `requestTimeoutMs` on `new App({...})` to
|
|
186
|
+
mitigate DoS.
|
|
187
|
+
- Workers have CPU and bundle-size limits; be cautious about adding
|
|
188
|
+
heavy dependencies. Run `wrangler deploy --dry-run --outdir=dist` to
|
|
189
|
+
inspect bundle size.
|
|
190
|
+
- Use `ctx.waitUntil(...)` for fire-and-forget work so the response
|
|
191
|
+
returns promptly.
|
|
192
|
+
- Pin a `compatibility_date` in `wrangler.toml` and only bump it
|
|
193
|
+
deliberately. New compat flags can change runtime semantics.
|
|
194
|
+
|
|
195
|
+
## Logging & observability
|
|
196
|
+
|
|
197
|
+
- Use `ctx.log` — it carries the request id.
|
|
198
|
+
- `console.log` in Workers shows up in `wrangler tail`. Prefer
|
|
199
|
+
structured logs through the framework logger.
|
|
200
|
+
- For tracing, the `tracing()` middleware emits OpenTelemetry-compatible
|
|
201
|
+
spans; wire up a Workers-friendly exporter when needed.
|
|
202
|
+
|
|
203
|
+
## Configuration & secrets
|
|
204
|
+
|
|
205
|
+
- Centralize env shape in an `Env` interface.
|
|
206
|
+
- Validate env via Zod once per request (cheap with Workers) or on first
|
|
207
|
+
access via a memoized helper.
|
|
208
|
+
- Treat env as immutable during a request.
|
|
209
|
+
|
|
210
|
+
## Pitfalls and guardrails
|
|
211
|
+
|
|
212
|
+
- Use `toFetchHandler(app)` from `@daloyjs/core/cloudflare` — never
|
|
213
|
+
hand-roll a `fetch(req, env, ctx)` adapter.
|
|
214
|
+
- Do not import `@daloyjs/core/node`, `@daloyjs/core/bun`, etc. — only
|
|
215
|
+
`@daloyjs/core` and `@daloyjs/core/cloudflare`.
|
|
216
|
+
- Avoid Node-only APIs (`Buffer`, `fs`, `process` beyond
|
|
217
|
+
`process.env`) unless `nodejs_compat` is enabled and required.
|
|
218
|
+
- Do not weaken response literal types (`as const`).
|
|
219
|
+
- Do not return errors as `{ status: 4xx, body }`. Throw a typed error.
|
|
220
|
+
- Do not add runtime dependencies without checking the hardened `.npmrc` (installs wait 24h after publish by default).
|
|
221
|
+
- Long-running work belongs in `ctx.waitUntil(...)`, not blocking the
|
|
222
|
+
response.
|
|
223
|
+
|
|
224
|
+
## Process expectations
|
|
225
|
+
|
|
226
|
+
- Every new feature ships with happy-path and unhappy-path tests.
|
|
227
|
+
- Bug fixes include a regression test.
|
|
228
|
+
- `pnpm typecheck` and `pnpm test` must pass before completion.
|
|
229
|
+
- For deploys, ask the user to run `wrangler login` first if needed —
|
|
230
|
+
do not attempt to authenticate on their behalf.
|
|
231
|
+
- Keep `README.md`, this `SKILL.md`, and `AGENTS.md` consistent.
|
|
232
|
+
|
|
233
|
+
## More
|
|
234
|
+
|
|
235
|
+
- Framework docs: <https://daloyjs.dev/docs>
|
|
236
|
+
- Issues: <https://github.com/daloyjs/daloy/issues>
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# AGENTS.md
|
|
2
2
|
|
|
3
|
-
A [DaloyJS](https://daloyjs.dev) REST API for the [Deno](https://deno.com) runtime. Contract-first
|
|
3
|
+
A [DaloyJS](https://daloyjs.dev) REST API for the [Deno](https://deno.com) runtime. **Contract-first**: routes are defined with Zod schemas and OpenAPI 3.1 is generated from them.
|
|
4
4
|
|
|
5
|
-
- Runtime: Deno (no Node package manager). Dependencies are loaded via `npm:` specifiers in `deno.json`.
|
|
5
|
+
- Runtime: Deno (no Node package manager). Dependencies are loaded via `npm:` and `jsr:` specifiers in `deno.json`.
|
|
6
6
|
|
|
7
7
|
## Commands
|
|
8
8
|
|
|
@@ -11,12 +11,33 @@ A [DaloyJS](https://daloyjs.dev) REST API for the [Deno](https://deno.com) runti
|
|
|
11
11
|
- `deno task test`
|
|
12
12
|
- `deno task gen:openapi` — write `generated/openapi.json`
|
|
13
13
|
|
|
14
|
-
The typed Hey API SDK is generated outside Deno (Hey API has no Deno entrypoint yet). Run `npx @hey-api/openapi-ts
|
|
14
|
+
The typed Hey API SDK is generated outside Deno (Hey API has no Deno entrypoint yet). Run `npx @hey-api/openapi-ts -i generated/openapi.json -o generated/client` if you need the client.
|
|
15
15
|
|
|
16
|
-
##
|
|
16
|
+
## Project shape
|
|
17
17
|
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
- Codegen reads from `buildApp()` only — never import `src/main.ts` from scripts.
|
|
18
|
+
- `src/build-app.ts` — `buildApp()` factory. Routes, schemas, and middleware live here. **Pure, no side effects.**
|
|
19
|
+
- `src/main.ts` — calls `buildApp()` and starts the listener via `@daloyjs/core/deno`. The only file that opens a port.
|
|
20
|
+
- `scripts/dump-openapi.ts` — imports `buildApp()` and writes `generated/openapi.json`. Codegen reads from `buildApp()` only — never import `src/main.ts` from scripts.
|
|
21
|
+
- `deno.json` — tasks, import map, and `npm:` specifiers. There is no `package.json` in this project.
|
|
22
|
+
- `generated/` — machine-written. Do not edit by hand.
|
|
23
|
+
- `tests/` — Deno test files.
|
|
21
24
|
|
|
22
|
-
|
|
25
|
+
## Core rules
|
|
26
|
+
|
|
27
|
+
1. The route definition is the contract. Method, path, request schemas, and response schemas live in one place — `app.route({...})`.
|
|
28
|
+
2. Validate every input with Zod. Use `.strict()` on top-level object schemas to reject unknown keys at the boundary.
|
|
29
|
+
3. Preserve literal types in responses: `status: 200 as const`, `z.literal(...)` on discriminator fields.
|
|
30
|
+
4. Throw typed errors (`NotFoundError`, `BadRequestError`, etc.) from `@daloyjs/core`.
|
|
31
|
+
5. Keep `requestId()`, `secureHeaders()`, and `rateLimit()` enabled.
|
|
32
|
+
6. Deno permissions are part of the contract — keep `--allow-net --allow-env --allow-read` narrow; never use `--allow-all`.
|
|
33
|
+
7. Every new route ships with a test that covers a happy path and at least one unhappy path.
|
|
34
|
+
8. After any route change: `deno task gen:openapi && deno task typecheck && deno task test`.
|
|
35
|
+
|
|
36
|
+
## Process expectations
|
|
37
|
+
|
|
38
|
+
- Quality gates must pass before declaring work done: `deno task typecheck` and `deno task test`.
|
|
39
|
+
- Regenerate the OpenAPI spec whenever route shapes change.
|
|
40
|
+
- Bug fixes include a regression test.
|
|
41
|
+
- Use `deno task ...`, not `npm`/`pnpm`. There is no `package.json` here.
|
|
42
|
+
|
|
43
|
+
For the full workflow — adding routes step-by-step, schema conventions, testing patterns, security guidance, and deployment notes — read [.agents/skills/daloyjs-best-practices/SKILL.md](.agents/skills/daloyjs-best-practices/SKILL.md).
|
|
@@ -44,7 +44,7 @@ deno task test
|
|
|
44
44
|
## What's included
|
|
45
45
|
|
|
46
46
|
- `@daloyjs/core` (loaded via `npm:` specifiers in `deno.json`).
|
|
47
|
-
- `secureHeaders`, `requestId`, and `rateLimit
|
|
47
|
+
- Starter security middleware: `secureHeaders`, `requestId`, and `rateLimit`.
|
|
48
48
|
<!-- daloy-minimal:strip-start books -->
|
|
49
49
|
- A health route and contract-first `/books/:id` route with Zod validation.
|
|
50
50
|
<!-- daloy-minimal:strip-end books -->
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# SKILL.md — DaloyJS best practices (Deno)
|
|
2
|
+
|
|
3
|
+
Operational guidance and best practices for AI coding agents working in this
|
|
4
|
+
DaloyJS [Deno](https://deno.com) project. This is the project's **single
|
|
5
|
+
source of truth** for how to add routes, write tests, ship secure defaults,
|
|
6
|
+
and run the quality gates. Read this in full before making non-trivial
|
|
7
|
+
changes.
|
|
8
|
+
|
|
9
|
+
## When to use this skill
|
|
10
|
+
|
|
11
|
+
Use this skill when you need to:
|
|
12
|
+
|
|
13
|
+
- Add, modify, or remove HTTP routes in this project.
|
|
14
|
+
- Regenerate the OpenAPI spec.
|
|
15
|
+
- Wire up new middleware, validation, or error handling.
|
|
16
|
+
- Add or update tests, run typecheck, or build the project.
|
|
17
|
+
- Harden the API (auth, CORS, rate limits, permissions, secrets).
|
|
18
|
+
|
|
19
|
+
Do **not** use this skill for tasks unrelated to the API itself.
|
|
20
|
+
|
|
21
|
+
## Core principles
|
|
22
|
+
|
|
23
|
+
DaloyJS is a **contract-first** framework. Internalize these rules:
|
|
24
|
+
|
|
25
|
+
1. **The route definition is the contract.** Method, path, request schemas,
|
|
26
|
+
and response schemas live in one place (`app.route({...})`).
|
|
27
|
+
2. **Zod schemas validate at every boundary.**
|
|
28
|
+
3. **Preserve literal types.** Return `status: 200 as const`; use
|
|
29
|
+
`z.literal(...)` / `as const` on discriminator fields.
|
|
30
|
+
4. **`buildApp()` is pure.** Construction never opens sockets. The HTTP
|
|
31
|
+
listener lives in `src/main.ts` via `@daloyjs/core/deno`.
|
|
32
|
+
5. **Secure by default.** `requestId()`, `secureHeaders()`, and
|
|
33
|
+
`rateLimit()` are registered before route definitions.
|
|
34
|
+
6. **Deno permissions are part of the contract.** Tasks declare exactly
|
|
35
|
+
the permissions they need (`--allow-net`, `--allow-env`, `--allow-read`).
|
|
36
|
+
Do not broaden them casually.
|
|
37
|
+
|
|
38
|
+
## Project shape
|
|
39
|
+
|
|
40
|
+
- `src/build-app.ts` — exports `buildApp()`. All routes and middleware
|
|
41
|
+
registered here. **Pure factory.**
|
|
42
|
+
- `src/main.ts` — calls `buildApp()` and starts the Deno HTTP listener via
|
|
43
|
+
`@daloyjs/core/deno`. The only file allowed to open a port.
|
|
44
|
+
- `scripts/dump-openapi.ts` — imports `buildApp()` and writes
|
|
45
|
+
`generated/openapi.json`.
|
|
46
|
+
- `deno.json` — tasks, import map, and `npm:` specifiers. **There is no
|
|
47
|
+
`package.json`** in this project — do not add one.
|
|
48
|
+
- `tests/` — Deno test files (`*.test.ts`).
|
|
49
|
+
- `generated/` — **machine-written**. Never edit by hand.
|
|
50
|
+
|
|
51
|
+
## Commands cheat-sheet
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
deno task dev # watch-mode server on http://localhost:3000
|
|
55
|
+
deno task typecheck # deno check
|
|
56
|
+
deno task test # deno test
|
|
57
|
+
deno task gen:openapi # write generated/openapi.json
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The typed Hey API SDK is generated outside Deno today (Hey API has no
|
|
61
|
+
Deno entrypoint yet). To produce the client, run:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
npx @hey-api/openapi-ts -i generated/openapi.json -o generated/client
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Always run `deno task typecheck` and `deno task test` before declaring a
|
|
68
|
+
task done.
|
|
69
|
+
|
|
70
|
+
## Workflow: add a new route
|
|
71
|
+
|
|
72
|
+
1. **Open `src/build-app.ts`.**
|
|
73
|
+
2. **Design schemas first.** Define request body/params/query/headers and a
|
|
74
|
+
response body per status code. Prefer `z.object({...}).strict()` for
|
|
75
|
+
inputs.
|
|
76
|
+
3. **Call `app.route({...})`** with `method`, `path`, `operationId`,
|
|
77
|
+
`tags`, `responses`, `handler` (plus `request` when accepting input).
|
|
78
|
+
4. **Return `{ status, body, headers? }` from the handler.** Always
|
|
79
|
+
`status: 200 as const`.
|
|
80
|
+
5. **Throw typed errors** (`NotFoundError`, `BadRequestError`, etc.) from
|
|
81
|
+
`@daloyjs/core`.
|
|
82
|
+
6. **Add a test in `tests/<route>.test.ts`** using `app.request(...)` for
|
|
83
|
+
in-process tests.
|
|
84
|
+
7. **Regenerate the contract**: `deno task gen:openapi`.
|
|
85
|
+
8. **Run the quality gates**: `deno task typecheck && deno task test`.
|
|
86
|
+
|
|
87
|
+
### Example: a typed route
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
import { z } from "zod";
|
|
91
|
+
import { NotFoundError } from "@daloyjs/core";
|
|
92
|
+
|
|
93
|
+
const Book = z.object({ id: z.string(), title: z.string() }).strict();
|
|
94
|
+
const BookParams = z.object({ id: z.string().min(1) }).strict();
|
|
95
|
+
|
|
96
|
+
app.route({
|
|
97
|
+
method: "GET",
|
|
98
|
+
path: "/books/:id",
|
|
99
|
+
operationId: "getBookById",
|
|
100
|
+
tags: ["Books"],
|
|
101
|
+
request: { params: BookParams },
|
|
102
|
+
responses: {
|
|
103
|
+
200: { description: "Found", body: Book },
|
|
104
|
+
404: { description: "Not found" },
|
|
105
|
+
},
|
|
106
|
+
handler: async ({ params }) => {
|
|
107
|
+
const book = await store.find(params.id);
|
|
108
|
+
if (!book) throw new NotFoundError(`Book ${params.id} not found`);
|
|
109
|
+
return { status: 200 as const, body: book };
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Validation & schema conventions
|
|
115
|
+
|
|
116
|
+
- **Inputs**: use `.strict()` on top-level object schemas.
|
|
117
|
+
- **IDs**: prefer `z.string().min(1)`; use `z.string().uuid()` or
|
|
118
|
+
`z.string().regex(...)` when shape is known.
|
|
119
|
+
- **Numbers from query strings**: `z.coerce.number().int().min(...)`.
|
|
120
|
+
- **Optional vs nullable**: `.optional()` ≠ `.nullable()` in OpenAPI.
|
|
121
|
+
- **Pagination**: standardize on `{ items, nextCursor }` cursor
|
|
122
|
+
pagination.
|
|
123
|
+
- **Discriminated unions**: `z.discriminatedUnion("kind", [...])`.
|
|
124
|
+
- **Never** parse `req.body` directly — let the framework validate.
|
|
125
|
+
|
|
126
|
+
## Error handling
|
|
127
|
+
|
|
128
|
+
- Throw typed errors from `@daloyjs/core` — they serialize to RFC 7807
|
|
129
|
+
problem responses.
|
|
130
|
+
- Add a `responses[code]` entry for every error you throw.
|
|
131
|
+
- Do not swallow errors. Log via `ctx.log.error(...)` and rethrow.
|
|
132
|
+
|
|
133
|
+
## Middleware
|
|
134
|
+
|
|
135
|
+
Register middleware **before** route definitions. Order matters.
|
|
136
|
+
|
|
137
|
+
Keep the secure baseline:
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
app.use(requestId());
|
|
141
|
+
app.use(secureHeaders());
|
|
142
|
+
app.use(rateLimit({ windowMs: 60_000, max: 120 }));
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Add CORS only when needed, with an explicit `origin` allowlist.
|
|
146
|
+
|
|
147
|
+
## Testing best practices
|
|
148
|
+
|
|
149
|
+
Tests run with `deno test`. Use **in-process** `app.request()` — no port
|
|
150
|
+
needed.
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
import { assertEquals } from "jsr:@std/assert";
|
|
154
|
+
import { buildApp } from "../src/build-app.ts";
|
|
155
|
+
|
|
156
|
+
Deno.test("GET /healthz returns ok", async () => {
|
|
157
|
+
const app = buildApp();
|
|
158
|
+
const res = await app.request("/healthz");
|
|
159
|
+
assertEquals(res.status, 200);
|
|
160
|
+
const body = await res.json();
|
|
161
|
+
assertEquals(body.ok, true);
|
|
162
|
+
});
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Cover **happy paths and unhappy paths**: valid input, validation failures
|
|
166
|
+
(400), auth failures (401/403), not-found (404), conflict (409), rate
|
|
167
|
+
limiting (429). For external services, inject an in-memory fake via
|
|
168
|
+
`buildApp({ store })`.
|
|
169
|
+
|
|
170
|
+
Aim for **100% line and function coverage** on routes you add.
|
|
171
|
+
|
|
172
|
+
## Security best practices
|
|
173
|
+
|
|
174
|
+
- Keep `secureHeaders()`, `requestId()`, and `rateLimit()` enabled.
|
|
175
|
+
- Permissions for the `dev` task are intentionally narrow: `--allow-net
|
|
176
|
+
--allow-env --allow-read`. If a change requires more permissions, add
|
|
177
|
+
them explicitly to the relevant task in `deno.json` and call it out to
|
|
178
|
+
the user — never `--allow-all`.
|
|
179
|
+
- Never log secrets — filter `authorization`, `cookie`, etc.
|
|
180
|
+
- Validate env via Zod at boot (`Deno.env.toObject()`). Fail fast on
|
|
181
|
+
missing config.
|
|
182
|
+
- For auth, verify JWT signatures against an allowlist of keys, never
|
|
183
|
+
trust the `alg` header, always check `exp` / `nbf`.
|
|
184
|
+
- Validate redirects against an allowlist.
|
|
185
|
+
- Set `bodyLimitBytes` and `requestTimeoutMs` on `new App({...})` to
|
|
186
|
+
mitigate DoS.
|
|
187
|
+
- Pin `npm:` and `jsr:` specifiers in `deno.json` to exact or
|
|
188
|
+
caret-locked versions; review changes in `deno.lock` before committing.
|
|
189
|
+
|
|
190
|
+
## Logging & observability
|
|
191
|
+
|
|
192
|
+
- Use `ctx.log` — it carries the request id.
|
|
193
|
+
- Avoid `console.log` in production code paths.
|
|
194
|
+
|
|
195
|
+
## Configuration & secrets
|
|
196
|
+
|
|
197
|
+
- Centralize config parsing (e.g. `src/config.ts`) validated by Zod.
|
|
198
|
+
- Read from `Deno.env`; do not introduce a `package.json` or dotenv
|
|
199
|
+
shim.
|
|
200
|
+
|
|
201
|
+
## Pitfalls and guardrails
|
|
202
|
+
|
|
203
|
+
- Never import `@daloyjs/core/deno` from `src/build-app.ts` or any
|
|
204
|
+
script under `scripts/`. That would boot a listener during codegen.
|
|
205
|
+
- Do not edit files under `generated/` by hand.
|
|
206
|
+
- Do not weaken response literal types (`as const`).
|
|
207
|
+
- Do not return errors as `{ status: 4xx, body }`. Throw a typed error.
|
|
208
|
+
- Use `deno task ...`, not `npm`/`pnpm`. There is no `package.json`.
|
|
209
|
+
- If you need a new dependency, add it to `imports` in `deno.json` via
|
|
210
|
+
`npm:` or `jsr:` specifiers; do not introduce a `package.json`.
|
|
211
|
+
|
|
212
|
+
## Process expectations
|
|
213
|
+
|
|
214
|
+
- Every new feature ships with happy-path and unhappy-path tests.
|
|
215
|
+
- Bug fixes include a regression test.
|
|
216
|
+
- `deno task typecheck` and `deno task test` must pass before completion.
|
|
217
|
+
- Run `deno task gen:openapi` when route shapes change; commit the
|
|
218
|
+
updated `generated/openapi.json`.
|
|
219
|
+
- Keep `README.md`, this `SKILL.md`, and `AGENTS.md` consistent with the
|
|
220
|
+
code.
|
|
221
|
+
|
|
222
|
+
## More
|
|
223
|
+
|
|
224
|
+
- Framework docs: <https://daloyjs.dev/docs>
|
|
225
|
+
- Issues: <https://github.com/daloyjs/daloy/issues>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# AGENTS.md
|
|
2
2
|
|
|
3
|
-
A [DaloyJS](https://daloyjs.dev) Node.js REST API. Contract-first
|
|
3
|
+
A [DaloyJS](https://daloyjs.dev) Node.js REST API. **Contract-first**:
|
|
4
|
+
routes are defined with Zod schemas and OpenAPI 3.1 is generated from them.
|
|
4
5
|
|
|
5
6
|
- Package manager: pnpm (use `pnpm` unless the project's `package.json` was rewritten for npm/yarn/bun).
|
|
6
7
|
- Runtime: Node.js >= 20.10.
|
|
@@ -12,11 +13,31 @@ A [DaloyJS](https://daloyjs.dev) Node.js REST API. Contract-first: routes are de
|
|
|
12
13
|
- `pnpm test` — Node built-in test runner
|
|
13
14
|
- `pnpm gen` — regenerate `generated/openapi.json` and the typed Hey API client
|
|
14
15
|
- `pnpm build` — emit `dist/`
|
|
16
|
+
- `pnpm audit` — supply-chain audit (respects the hardened `.npmrc`)
|
|
15
17
|
|
|
16
|
-
##
|
|
18
|
+
## Project shape
|
|
17
19
|
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
- Codegen reads from `buildApp()` only — never import `src/index.ts` from scripts.
|
|
20
|
+
- `src/build-app.ts` — `buildApp()` factory. Routes, schemas, and middleware live here. **Pure, no side effects.**
|
|
21
|
+
- `src/index.ts` — calls `buildApp()` and starts the listener via `@daloyjs/core/node`. The only file that opens a port.
|
|
22
|
+
- `scripts/dump-openapi.ts` — imports `buildApp()` and writes `generated/openapi.json`. Codegen reads from `buildApp()` only — never import `src/index.ts` from scripts.
|
|
23
|
+
- `generated/` — machine-written by `pnpm gen`. Do not edit by hand.
|
|
24
|
+
- `tests/` — `*.test.ts` files run with `node --test` (via `tsx`).
|
|
21
25
|
|
|
22
|
-
|
|
26
|
+
## Core rules
|
|
27
|
+
|
|
28
|
+
1. The route definition is the contract. Method, path, request schemas, and response schemas live in one place — `app.route({...})`.
|
|
29
|
+
2. Validate every input with Zod. Use `.strict()` on top-level object schemas to reject unknown keys at the boundary.
|
|
30
|
+
3. Preserve literal types in responses: `status: 200 as const`, `z.literal(...)` on discriminator fields. Codegen depends on these.
|
|
31
|
+
4. Throw typed errors (`NotFoundError`, `BadRequestError`, etc.) from `@daloyjs/core` — never return raw error responses.
|
|
32
|
+
5. Keep `requestId()`, `secureHeaders()`, and `rateLimit()` enabled. They are the project's secure defaults.
|
|
33
|
+
6. Every new route ships with a test that covers a happy path and at least one unhappy path.
|
|
34
|
+
7. After any route change: `pnpm gen && pnpm typecheck && pnpm test`.
|
|
35
|
+
|
|
36
|
+
## Process expectations
|
|
37
|
+
|
|
38
|
+
- Quality gates must pass before declaring work done: `pnpm typecheck` and `pnpm test`.
|
|
39
|
+
- Update the OpenAPI spec and typed client whenever route shapes change (`pnpm gen`).
|
|
40
|
+
- Bug fixes include a regression test.
|
|
41
|
+
- Never bypass safety checks (`--no-verify`, `--ignore-scripts=false`) without a clear reason.
|
|
42
|
+
|
|
43
|
+
For the full workflow — adding routes step-by-step, schema conventions, testing patterns, security guidance, and deployment notes — read [.agents/skills/daloyjs-best-practices/SKILL.md](.agents/skills/daloyjs-best-practices/SKILL.md).
|
|
@@ -44,7 +44,7 @@ node dist/index.js
|
|
|
44
44
|
|
|
45
45
|
## What's included
|
|
46
46
|
|
|
47
|
-
- `@daloyjs/core` with `secureHeaders`, `requestId`, and `rateLimit
|
|
47
|
+
- `@daloyjs/core` with starter security middleware: `secureHeaders`, `requestId`, and `rateLimit`.
|
|
48
48
|
<!-- daloy-minimal:strip-start books -->
|
|
49
49
|
- A health route and a contract-first `/books/:id` route with Zod validation.
|
|
50
50
|
<!-- daloy-minimal:strip-end books -->
|