create-daloy 0.1.17 → 0.1.19
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 +14 -5
- package/bin/create-daloy.mjs +567 -91
- 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/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,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 -->
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
# SKILL.md — DaloyJS best practices (Node)
|
|
2
|
+
|
|
3
|
+
Operational guidance and best practices for AI coding agents working in this
|
|
4
|
+
DaloyJS Node.js project. This is the project's **single source of truth** for
|
|
5
|
+
how to add routes, write tests, ship secure defaults, and run the quality
|
|
6
|
+
gates. Read this in full before making non-trivial changes.
|
|
7
|
+
|
|
8
|
+
## When to use this skill
|
|
9
|
+
|
|
10
|
+
Use this skill when you need to:
|
|
11
|
+
|
|
12
|
+
- Add, modify, or remove HTTP routes in this project.
|
|
13
|
+
- Regenerate the OpenAPI spec or the typed Hey API SDK in `generated/`.
|
|
14
|
+
- Wire up new middleware, validation, or error handling.
|
|
15
|
+
- Add or update tests, run typecheck, or build the project.
|
|
16
|
+
- Harden the API (auth, CORS, rate limits, secrets, dependency hygiene).
|
|
17
|
+
|
|
18
|
+
Do **not** use this skill for tasks unrelated to the API itself (infra-only
|
|
19
|
+
changes, unrelated docs sites, etc.).
|
|
20
|
+
|
|
21
|
+
## Core principles
|
|
22
|
+
|
|
23
|
+
DaloyJS is a **contract-first** framework. Internalize these rules — every
|
|
24
|
+
recommendation below follows from them:
|
|
25
|
+
|
|
26
|
+
1. **The route definition is the contract.** Method, path, request schemas,
|
|
27
|
+
and response schemas live in one place (`app.route({...})`). The OpenAPI
|
|
28
|
+
spec, the typed client, and the runtime validation are all derived from
|
|
29
|
+
it. Never duplicate that information by hand-writing fetch calls, types,
|
|
30
|
+
or `openapi.json` entries.
|
|
31
|
+
2. **Zod schemas validate at every boundary.** Body, params, query, and
|
|
32
|
+
headers go through Zod. If a field is not in the schema, it is not part
|
|
33
|
+
of the contract.
|
|
34
|
+
3. **Preserve literal types.** Return `status: 200 as const` and use
|
|
35
|
+
`z.literal(...)` / `as const` on discriminator fields. The typed client
|
|
36
|
+
needs narrow types to do useful response narrowing.
|
|
37
|
+
4. **`buildApp()` is pure.** Construction never opens sockets or reads
|
|
38
|
+
stateful resources. The HTTP listener lives in a separate file. This
|
|
39
|
+
lets codegen, tests, and tooling import `buildApp()` without side
|
|
40
|
+
effects.
|
|
41
|
+
5. **Secure by default.** `requestId()`, `secureHeaders()`, and
|
|
42
|
+
`rateLimit()` are registered before route definitions. Do not remove
|
|
43
|
+
them unless the user explicitly asks.
|
|
44
|
+
|
|
45
|
+
## Project shape
|
|
46
|
+
|
|
47
|
+
- `src/build-app.ts` — exports `buildApp()`. All routes and middleware are
|
|
48
|
+
registered here. **Pure function, no side effects.**
|
|
49
|
+
- `src/index.ts` — calls `buildApp()` and starts the Node HTTP listener via
|
|
50
|
+
`@daloyjs/core/node`. This is the only file allowed to open a port.
|
|
51
|
+
- `scripts/dump-openapi.ts` — imports `buildApp()` and writes
|
|
52
|
+
`generated/openapi.json`. Imports nothing that boots a server.
|
|
53
|
+
- `openapi-ts.config.ts` — Hey API config; reads `generated/openapi.json`
|
|
54
|
+
and writes `generated/client/`.
|
|
55
|
+
- `tests/` — Node test runner files (`*.test.ts`).
|
|
56
|
+
- `generated/` — **machine-written**. Never edit by hand; rerun `pnpm gen`.
|
|
57
|
+
|
|
58
|
+
## Commands cheat-sheet
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
pnpm dev # watch-mode dev server on http://localhost:3000
|
|
62
|
+
pnpm typecheck # tsc --noEmit
|
|
63
|
+
pnpm test # Node built-in test runner
|
|
64
|
+
pnpm gen # gen:openapi + gen:client
|
|
65
|
+
pnpm gen:openapi # write generated/openapi.json
|
|
66
|
+
pnpm gen:client # write generated/client/
|
|
67
|
+
pnpm build # emit dist/
|
|
68
|
+
pnpm audit # supply-chain audit
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Always run `pnpm typecheck` and `pnpm test` before declaring a task done.
|
|
72
|
+
If a change touches route shapes, also run `pnpm gen` so the client stays
|
|
73
|
+
in sync.
|
|
74
|
+
|
|
75
|
+
## Workflow: add a new route
|
|
76
|
+
|
|
77
|
+
Follow these steps in order. Skipping any of them is a common source of
|
|
78
|
+
bugs (drifted client SDK, missing test, broken codegen).
|
|
79
|
+
|
|
80
|
+
1. **Open `src/build-app.ts`.** Routes are registered on the `app`
|
|
81
|
+
instance returned by `new App({...})`.
|
|
82
|
+
2. **Design the schemas first.** Define request body / params / query /
|
|
83
|
+
headers and a response body per status code. Reuse schemas — co-locate
|
|
84
|
+
them in `src/build-app.ts` or extract a `src/schemas/*.ts` module if
|
|
85
|
+
they grow. Prefer `z.object({...}).strict()` for inputs so unknown
|
|
86
|
+
keys are rejected at the boundary.
|
|
87
|
+
3. **Call `app.route({...})`.** Required keys: `method`, `path`,
|
|
88
|
+
`operationId`, `tags`, `responses`, `handler`. Add `request` when the
|
|
89
|
+
route accepts input.
|
|
90
|
+
4. **Return `{ status, body, headers? }` from the handler.** Always use
|
|
91
|
+
`status: 200 as const` (or whatever code) so the typed client can
|
|
92
|
+
narrow. For literal discriminators in `body`, use `as const` or
|
|
93
|
+
`z.literal(...)` in the schema.
|
|
94
|
+
5. **Throw typed errors, do not return raw error responses.** Use
|
|
95
|
+
`NotFoundError`, `BadRequestError`, `UnauthorizedError`,
|
|
96
|
+
`ForbiddenError`, `ConflictError`, etc. from `@daloyjs/core`. The
|
|
97
|
+
framework maps them to RFC 7807 problem responses.
|
|
98
|
+
6. **Add a test in `tests/<route>.test.ts`.** Use `app.request(...)` for
|
|
99
|
+
in-process tests — no port needed (see "Testing best practices").
|
|
100
|
+
7. **Regenerate the contract.** Run `pnpm gen`. Inspect
|
|
101
|
+
`generated/openapi.json` to confirm the operation shows up with the
|
|
102
|
+
expected schemas and status codes.
|
|
103
|
+
8. **Run the quality gates.** `pnpm typecheck && pnpm test`.
|
|
104
|
+
|
|
105
|
+
### Example: a typed route
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
import { z } from "zod";
|
|
109
|
+
import { NotFoundError } from "@daloyjs/core";
|
|
110
|
+
|
|
111
|
+
const Book = z.object({ id: z.string(), title: z.string() }).strict();
|
|
112
|
+
const BookParams = z.object({ id: z.string().min(1) }).strict();
|
|
113
|
+
|
|
114
|
+
app.route({
|
|
115
|
+
method: "GET",
|
|
116
|
+
path: "/books/:id",
|
|
117
|
+
operationId: "getBookById",
|
|
118
|
+
tags: ["Books"],
|
|
119
|
+
request: { params: BookParams },
|
|
120
|
+
responses: {
|
|
121
|
+
200: { description: "Found", body: Book },
|
|
122
|
+
404: { description: "Not found" },
|
|
123
|
+
},
|
|
124
|
+
handler: async ({ params }) => {
|
|
125
|
+
const book = await store.find(params.id);
|
|
126
|
+
if (!book) throw new NotFoundError(`Book ${params.id} not found`);
|
|
127
|
+
return { status: 200 as const, body: book };
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Validation & schema conventions
|
|
133
|
+
|
|
134
|
+
- **Inputs**: use `.strict()` on top-level object schemas to reject unknown
|
|
135
|
+
keys at the API boundary. This blocks mass-assignment-style attacks and
|
|
136
|
+
catches typos in clients early.
|
|
137
|
+
- **IDs**: prefer `z.string().min(1)` over `z.string()`. Use
|
|
138
|
+
`z.string().uuid()` / `z.string().regex(...)` when the shape is known.
|
|
139
|
+
- **Numbers from query strings**: use `z.coerce.number().int().min(...)`
|
|
140
|
+
because query params arrive as strings.
|
|
141
|
+
- **Optional vs nullable**: `.optional()` for "may be absent",
|
|
142
|
+
`.nullable()` for "explicitly null". They are not interchangeable in
|
|
143
|
+
OpenAPI.
|
|
144
|
+
- **Pagination**: standardize on `{ items, nextCursor }` cursor pagination
|
|
145
|
+
unless the user asks otherwise. Offset pagination invites large skips.
|
|
146
|
+
- **Discriminated unions** (e.g. for response variants): use
|
|
147
|
+
`z.discriminatedUnion("kind", [...])` and tag each branch with
|
|
148
|
+
`z.literal("...")` so codegen produces a narrow TypeScript union.
|
|
149
|
+
- **Never** call `JSON.parse` or `req.body` directly in a handler. Let the
|
|
150
|
+
framework validate via the schema and read the typed object passed to
|
|
151
|
+
the handler.
|
|
152
|
+
|
|
153
|
+
## Error handling
|
|
154
|
+
|
|
155
|
+
- Throw typed errors from `@daloyjs/core` — they carry status codes and
|
|
156
|
+
serialize to RFC 7807 problem responses (`application/problem+json`).
|
|
157
|
+
- Add a `responses[code]` entry for every error you throw, so the OpenAPI
|
|
158
|
+
spec and the typed client know it can happen.
|
|
159
|
+
- Do not swallow errors in handlers. If you need to log and rethrow, use
|
|
160
|
+
`ctx.log.error(err, "context")` and rethrow.
|
|
161
|
+
- For unexpected errors, let them bubble. The framework's error middleware
|
|
162
|
+
will convert them into a generic 500 problem response and log them with
|
|
163
|
+
the request ID for correlation.
|
|
164
|
+
|
|
165
|
+
## Middleware
|
|
166
|
+
|
|
167
|
+
Register middleware **before** route definitions inside `buildApp()`.
|
|
168
|
+
Order matters — earlier middleware wraps later middleware and routes.
|
|
169
|
+
|
|
170
|
+
Keep these as the secure baseline:
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
app.use(requestId()); // x-request-id for log correlation
|
|
174
|
+
app.use(secureHeaders()); // strict security headers
|
|
175
|
+
app.use(rateLimit({ windowMs: 60_000, max: 120 }));
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Add CORS only when the API is consumed by browsers from a different
|
|
179
|
+
origin, and always pin `origin` to an allowlist — never `*` for an API
|
|
180
|
+
that accepts credentials.
|
|
181
|
+
|
|
182
|
+
Custom middleware should be small, well-typed, and call `await next()`
|
|
183
|
+
exactly once. Wrap it in `try { await next() } finally { ... }` if it
|
|
184
|
+
needs to run code after the handler.
|
|
185
|
+
|
|
186
|
+
## Testing best practices
|
|
187
|
+
|
|
188
|
+
Tests live under `tests/` and run with `node --test` (Node's built-in
|
|
189
|
+
runner via `tsx`). Use **in-process** requests through `app.request()` —
|
|
190
|
+
no HTTP server, no port flakiness, no teardown.
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
import { test } from "node:test";
|
|
194
|
+
import assert from "node:assert/strict";
|
|
195
|
+
import { buildApp } from "../src/build-app.ts";
|
|
196
|
+
|
|
197
|
+
test("GET /healthz returns ok", async () => {
|
|
198
|
+
const app = buildApp();
|
|
199
|
+
const res = await app.request("/healthz");
|
|
200
|
+
assert.equal(res.status, 200);
|
|
201
|
+
const body = await res.json();
|
|
202
|
+
assert.equal(body.ok, true);
|
|
203
|
+
assert.ok(typeof body.uptime === "number");
|
|
204
|
+
});
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Cover both **happy paths** and **unhappy paths** for every route:
|
|
208
|
+
|
|
209
|
+
- Happy path: valid input → expected `status` and `body`.
|
|
210
|
+
- Validation failure: missing/invalid fields → `400` with problem details.
|
|
211
|
+
- Auth failure (when applicable): unauthenticated → `401`; wrong scope →
|
|
212
|
+
`403`.
|
|
213
|
+
- Not found: unknown id → `404`.
|
|
214
|
+
- Conflict: duplicate create → `409`.
|
|
215
|
+
- Rate limit: hammer the route → `429` after the configured threshold.
|
|
216
|
+
|
|
217
|
+
For routes that touch external services, write a thin in-memory fake
|
|
218
|
+
inside the test and inject it via the factory pattern (`buildApp({ store })`).
|
|
219
|
+
Do not mock global `fetch` unless there is no alternative.
|
|
220
|
+
|
|
221
|
+
Aim for **100% line and function coverage** on routes you add. If a
|
|
222
|
+
branch is impractical to test (e.g. defensive `never` arms), refactor it
|
|
223
|
+
out rather than adding ignore comments — agent coverage tools may not
|
|
224
|
+
honor them.
|
|
225
|
+
|
|
226
|
+
## Security best practices
|
|
227
|
+
|
|
228
|
+
- Keep `secureHeaders()`, `requestId()`, and `rateLimit()` enabled. They
|
|
229
|
+
ship the OWASP-recommended baseline.
|
|
230
|
+
- Never log secrets. Filter `authorization`, `cookie`, and any header /
|
|
231
|
+
body field that may contain tokens before logging.
|
|
232
|
+
- Read secrets from `process.env`, validated through a Zod schema at
|
|
233
|
+
boot. Fail fast on missing config rather than at request time.
|
|
234
|
+
- For auth, prefer a small JWT or session middleware over rolling your
|
|
235
|
+
own. Verify signatures against an allowlist of keys, never trust the
|
|
236
|
+
`alg` header from the token, and always check `exp` / `nbf`.
|
|
237
|
+
- Validate redirects against an allowlist. Open redirects are an OWASP
|
|
238
|
+
Top-10 issue.
|
|
239
|
+
- Limit body sizes via `bodyLimitBytes` on `new App({...})` — large
|
|
240
|
+
payloads are a cheap denial-of-service vector.
|
|
241
|
+
- Set `requestTimeoutMs` so slow clients cannot tie up workers.
|
|
242
|
+
- For database access, use parameterized queries / a query builder. Never
|
|
243
|
+
interpolate user input into SQL strings.
|
|
244
|
+
- Review `pnpm audit` output before releases. Avoid lowering the
|
|
245
|
+
install cooldown without reason; new package versions can be malicious.
|
|
246
|
+
|
|
247
|
+
## Logging & observability
|
|
248
|
+
|
|
249
|
+
- The default logger emits structured JSON in production and pretty logs
|
|
250
|
+
in development. Use it via the handler context: `await handler(ctx)` →
|
|
251
|
+
`ctx.log.info({ userId }, "message")`.
|
|
252
|
+
- Always include the request id in log lines automatically emitted by
|
|
253
|
+
the framework. When you add your own logs, the request id is on
|
|
254
|
+
`ctx.requestId` and on the bound child logger.
|
|
255
|
+
- For tracing, the `tracing()` middleware (from `@daloyjs/core`) emits
|
|
256
|
+
OpenTelemetry-compatible spans. Enable it once the user wires up an
|
|
257
|
+
exporter.
|
|
258
|
+
|
|
259
|
+
## Configuration & secrets
|
|
260
|
+
|
|
261
|
+
- Centralize config parsing in one module (e.g. `src/config.ts`) that
|
|
262
|
+
validates `process.env` via a Zod schema and exports a typed `config`
|
|
263
|
+
object.
|
|
264
|
+
- `.env.example` documents required variables; `.env` is gitignored.
|
|
265
|
+
- Treat config as immutable at runtime — read it once at startup.
|
|
266
|
+
|
|
267
|
+
## Pitfalls and guardrails
|
|
268
|
+
|
|
269
|
+
- Never import `@daloyjs/core/node` (or any adapter that boots a server)
|
|
270
|
+
from `src/build-app.ts` or any script under `scripts/`. That would
|
|
271
|
+
start an HTTP listener as a side effect of codegen.
|
|
272
|
+
- Do not edit files under `generated/` by hand — they are overwritten by
|
|
273
|
+
`pnpm gen`.
|
|
274
|
+
- Do not weaken response literal types (`as const`); the typed client
|
|
275
|
+
depends on them.
|
|
276
|
+
- Do not return errors as `{ status: 4xx, body: {...} }`. Throw a typed
|
|
277
|
+
error so the framework can format the problem response consistently.
|
|
278
|
+
- Do not add runtime dependencies without checking the hardened `.npmrc` (installs wait 24h after publish by default).
|
|
279
|
+
- Do not bypass safety checks like `--no-verify` on commit or
|
|
280
|
+
`--ignore-scripts=false` on install without a clear reason.
|
|
281
|
+
- Avoid global mutable state in `buildApp()`. If you need shared state,
|
|
282
|
+
pass it in as a parameter (`buildApp({ store })`).
|
|
283
|
+
|
|
284
|
+
## Process expectations
|
|
285
|
+
|
|
286
|
+
- Every new feature must include happy-path **and** unhappy-path tests.
|
|
287
|
+
- Bug fixes include a regression test.
|
|
288
|
+
- Quality gates (`pnpm typecheck`, `pnpm test`) must pass before
|
|
289
|
+
declaring the task complete.
|
|
290
|
+
- When route shapes change, also run `pnpm gen` and commit the updated
|
|
291
|
+
`generated/openapi.json` + client.
|
|
292
|
+
- Keep `README.md`, this `SKILL.md`, and `AGENTS.md` consistent with the
|
|
293
|
+
code. If you add a workflow, document it here.
|
|
294
|
+
|
|
295
|
+
## More
|
|
296
|
+
|
|
297
|
+
- Framework docs: <https://daloyjs.dev/docs>
|
|
298
|
+
- Issues: <https://github.com/daloyjs/daloy/issues>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# AGENTS.md
|
|
2
2
|
|
|
3
|
-
A [DaloyJS](https://daloyjs.dev) REST API deployed to **Vercel Edge**. Contract-first
|
|
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.
|
|
4
4
|
|
|
5
5
|
- Package manager: pnpm (use `pnpm` unless the project's `package.json` was rewritten for npm/yarn/bun).
|
|
6
6
|
- Runtime: Vercel Edge (Web Standard `Request`/`Response`).
|
|
@@ -8,13 +8,33 @@ A [DaloyJS](https://daloyjs.dev) REST API deployed to **Vercel Edge**. Contract-
|
|
|
8
8
|
## Commands
|
|
9
9
|
|
|
10
10
|
- `pnpm dev` — local Vercel dev server on http://localhost:3000
|
|
11
|
-
- `pnpm typecheck`
|
|
12
|
-
- `pnpm test`
|
|
11
|
+
- `pnpm typecheck` — `tsc --noEmit`
|
|
12
|
+
- `pnpm test` — run test suite
|
|
13
13
|
- `pnpm deploy` — deploy to Vercel
|
|
14
|
+
- `pnpm audit` — supply-chain audit
|
|
14
15
|
|
|
15
|
-
##
|
|
16
|
+
## Project shape
|
|
16
17
|
|
|
17
|
-
-
|
|
18
|
-
-
|
|
18
|
+
- `api/[...path].ts` — Edge entrypoint. Builds the `App`, registers routes/middleware, and exports `default toEdgeHandler(app)` plus `export const config = { runtime: "edge" }`. **Keep it a catch-all** so DaloyJS owns routing.
|
|
19
|
+
- `vercel.json` — Vercel build/runtime configuration.
|
|
20
|
+
- `tests/` — test files.
|
|
19
21
|
|
|
20
|
-
|
|
22
|
+
## Core rules
|
|
23
|
+
|
|
24
|
+
1. The route definition is the contract. Method, path, request schemas, and response schemas live in one place — `app.route({...})`.
|
|
25
|
+
2. Validate every input with Zod. Use `.strict()` on top-level object schemas to reject unknown keys at the boundary.
|
|
26
|
+
3. Preserve literal types in responses: `status: 200 as const`, `z.literal(...)` on discriminator fields.
|
|
27
|
+
4. Throw typed errors (`NotFoundError`, `BadRequestError`, etc.) from `@daloyjs/core`.
|
|
28
|
+
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).
|
|
29
|
+
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.
|
|
30
|
+
7. The catch-all `api/[...path].ts` must remain a catch-all so DaloyJS handles routing.
|
|
31
|
+
8. Every new route ships with a test that covers a happy path and at least one unhappy path.
|
|
32
|
+
|
|
33
|
+
## Process expectations
|
|
34
|
+
|
|
35
|
+
- Quality gates must pass before declaring work done: `pnpm typecheck` and `pnpm test`.
|
|
36
|
+
- Bug fixes include a regression test.
|
|
37
|
+
- For deploys, ensure the user has run `vercel login`; do not authenticate on their behalf.
|
|
38
|
+
- Never bypass safety checks without a clear reason.
|
|
39
|
+
|
|
40
|
+
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).
|
|
@@ -41,3 +41,14 @@ export default toEdgeHandler(app);
|
|
|
41
41
|
```
|
|
42
42
|
|
|
43
43
|
That catch-all API route lets DaloyJS own routing while Vercel handles the Edge runtime.
|
|
44
|
+
|
|
45
|
+
## What's included
|
|
46
|
+
|
|
47
|
+
- `@daloyjs/core/vercel` with starter security middleware: `secureHeaders` and `requestId`.
|
|
48
|
+
- Smaller edge-friendly body and timeout limits in the generated app.
|
|
49
|
+
<!-- daloy-minimal:strip-start books -->
|
|
50
|
+
- A health route and a contract-first `/books/:id` route with Zod validation.
|
|
51
|
+
<!-- daloy-minimal:strip-end books -->
|
|
52
|
+
<!-- daloy-minimal:strip-start docs -->
|
|
53
|
+
- Swagger UI at `/docs` and a live OpenAPI 3.1 document at `/openapi.json`.
|
|
54
|
+
<!-- daloy-minimal:strip-end docs -->
|