create-warlock 5.34.1 → 5.34.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/llms-full.txt +21 -28
  3. package/llms.txt +1 -1
  4. package/package.json +2 -2
  5. package/skills/{create-a-warlock-project/SKILL.md → SKILL.md} +20 -27
  6. package/templates/toolchains/oxc/.oxlintrc.json +3 -1
  7. package/templates/warlock/.oxlintrc.json +3 -1
  8. package/templates/warlock/AGENTS.md +10 -10
  9. package/templates/warlock/skills/api-design/SKILL.md +110 -50
  10. package/templates/warlock/skills/code-standards/SKILL.md +48 -39
  11. package/templates/warlock/skills/data-and-persistence/SKILL.md +72 -57
  12. package/templates/warlock/skills/git-workflow/SKILL.md +9 -9
  13. package/templates/warlock/skills/module-boundaries/SKILL.md +2 -2
  14. package/templates/warlock/skills/observability-and-resilience/SKILL.md +8 -6
  15. package/templates/warlock/skills/security-baseline/SKILL.md +23 -22
  16. package/templates/warlock/skills/testing-strategy/SKILL.md +7 -7
  17. package/templates/warlock/skills/web-code-standards/SKILL.md +157 -60
  18. package/templates/warlock/src/app/auth/models/otp/otp.model.ts +9 -3
  19. package/templates/warlock/src/app/auth/services/otp.service.ts +18 -6
  20. package/templates/warlock/src/app/contact/controllers/contact.controller.ts +6 -0
  21. package/templates/warlock/src/app/locale/controllers/locale.controller.ts +6 -0
  22. package/templates/warlock/src/app/posts/controllers/create-new-post.controller.ts +7 -0
  23. package/templates/warlock/src/app/posts/controllers/update-post.controller.ts +14 -9
  24. package/templates/warlock/src/app/posts/models/post/migrations/09-01-2026_02-07-51-post.migration.ts +1 -0
  25. package/templates/warlock/src/app/posts/models/post/post.model.ts +6 -0
  26. package/templates/warlock/src/app/posts/resources/post.resource.ts +1 -1
  27. package/templates/warlock/src/app/posts/schema/update-post.schema.ts +1 -0
  28. package/templates/warlock/src/app/shared/utils/format-http-error-response.ts +41 -0
  29. package/templates/warlock/src/app/users/commands/hello-world.command.ts +2 -1
  30. package/templates/warlock/src/app/users/controllers/create-new-user.controller.ts +12 -1
  31. package/templates/warlock/src/app/users/controllers/list-users.controller.ts +24 -1
  32. package/templates/warlock/src/app/users/models/user/migrations/11-12-2025_23-58-03-user.migration.ts +1 -0
  33. package/templates/warlock/src/app/users/models/user/user.model.ts +5 -0
  34. package/templates/warlock/src/app/users/repositories/users.repository.ts +2 -2
  35. package/templates/warlock/src/app/users/resources/user.resource.ts +1 -1
  36. package/templates/warlock/src/app/users/schema/list-users.schema.ts +1 -1
  37. package/templates/warlock/src/config/http.ts +6 -0
  38. package/templates/warlock/src/web/home/components/contact-field.tsx +18 -0
  39. package/templates/warlock/src/web/home/components/contact-section.tsx +5 -29
  40. package/templates/warlock/src/web/home/components/contact-submit-button.tsx +16 -0
  41. package/templates/warlock/src/web/home/hooks/use-contact-field.ts +9 -0
  42. package/templates/warlock/src/web/home/hooks/use-contact-form.ts +5 -8
  43. package/templates/warlock/src/web/home/hooks/use-contact-locale.ts +34 -0
  44. package/templates/warlock/src/web/home/hooks/use-contact-submit-button.ts +6 -0
  45. package/templates/warlock/src/web/home/index.page.tsx +1 -1
  46. package/templates/warlock/src/web/home/types/contact-controls.type.ts +13 -0
  47. package/templates/warlock/src/web/shared/utils/set-form-errors.ts +3 -1
  48. package/templates/warlock/src/web/home/components/contact-form-controls.tsx +0 -42
package/CHANGELOG.md CHANGED
@@ -4,6 +4,22 @@ All notable changes to `create-warlock` are documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). `@warlock.js/*` packages are released in lockstep — every package shares the same version number, so a version below may list only the changes that affected this package.
6
6
 
7
+ ## 5.34.3 - 2026-10-20
8
+
9
+ ### Fixed
10
+
11
+ - Keep the public UUID integrity assertion independent of formatter line breaks while checking the complete schema default callback.
12
+ - Align project skills with real handler signatures, resource source-key tuples, public UUIDs, response schemas, idempotency headers, and the canonical JSON error envelope. Keep discovery descriptions within 450 characters and resolve cross-skill references by name.
13
+ - Extract contact locale and form-control behavior into feature hooks and separate components with dedicated props types; preserve form interactions and consume normalized validation details.
14
+
15
+ ### Changed
16
+
17
+ - New app models expose a separate public UUID; sample post updates and user filters resolve public IDs, and user lists return the documented pagination envelope.
18
+ - Enable non-type-aware Oxlint rules for explicit any, braces, member accessibility, and console usage in both scaffold toolchain copies.
19
+
20
+ - Strengthen the web code standards skill with single-responsibility components, Mongez React Form validation, accessible custom controls, and explicit UI kit selection; consolidate setup-file guidance and correct typed-route advice.
21
+ - The agent skill now exports as `create-warlock`; package discovery descriptions stay within 450 characters.
22
+
7
23
  ## 5.34.1 - 2026-10-19
8
24
 
9
25
  ### Changed
package/llms-full.txt CHANGED
@@ -4,11 +4,11 @@
4
4
 
5
5
  > Generated artifact. Concatenates every SKILL.md and reference file under `create-warlock/skills/`. Re-run `node scripts/generate-llms.mjs` after any change.
6
6
 
7
- ## create-a-warlock-project `create-warlock/create-a-warlock-project/SKILL.md`
7
+ ## create-warlock `create-warlock/skills/SKILL.md`
8
8
 
9
9
  ---
10
- name: create-a-warlock-project
11
- description: "Create a Warlock project with the canonical scaffold, package selection, and project conventions."
10
+ name: create-warlock
11
+ description: 'Scaffold Warlock apps with `npm create warlock@latest` or `npx create-warlock`; the CLI entry exports `parseFlags`, `packageVersion` and default `createApp`. Use for "create a project", "start a backend or SSR app", choose a database, AI adapters, features or linting, and automate setup with `--yes` and `--pm=npm`. Not this package → @warlock.js/core for extending an existing app; @warlock.js/web for page APIs.'
12
12
  ---
13
13
 
14
14
  # create-warlock — scaffold a new project
@@ -53,7 +53,7 @@ that is just the project name, the one required answer with no default:
53
53
 
54
54
  ```
55
55
  A project name is required and no terminal is available to ask for one. Pass it as
56
- the first argument or --name=<name>. Non-interactive flags: --yes, --pm=<npm|yarn|pnpm>,
56
+ the first argument or --name=<name>. Non-interactive flags: --yes, --pm=npm,
57
57
  --db=<driver>|--no-db, --features=<list>, --ai=<list>, --lint=<oxc>, --git|--no-git, --jwt|--no-jwt.
58
58
  ```
59
59
 
@@ -72,30 +72,23 @@ Features: none
72
72
 
73
73
  ## Flags
74
74
 
75
- | Flag | Takes value | Default | Purpose |
76
- | ------------------------------ | ----------- | ------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
77
- | `<positional>` / `--name` | yes | — (required) | Project name + target directory. |
78
- | `--stack` | yes | `api` | `api` or `web` — the structural choice; `web` seeds the `web` feature by default. |
79
- | `--db` | yes | `mongodb` | Database driver key (`mongodb`, `postgres`, or `none`). |
80
- | `--no-db` | no | — | Opt out of a database — shorthand for `--db=none`. |
81
- | `--pm` | yes | inferred (`npm_config_user_agent` / system) | Package manager — must be one of `npm` / `yarn` / `pnpm` / `bun`; anything else is rejected before scaffolding starts. |
82
- | `--features` | yes (CSV) | none, or `["web"]` when `--stack=web` and `--features` is not given | Comma-separated optional feature keys. An explicit `--features` always wins over the `--stack` default. |
83
- | `--lint` | yes | `oxc` | `oxc` (Oxlint/Oxfmt); removed or invalid values fail before writes. |
84
- | `--ai` | yes (CSV) | none | Comma-separated AI provider keys (auto-pulls `@warlock.js/ai`). |
85
- | `--agents` | yes (CSV) | `claude` | Comma-separated `agent-kit` targets — see below. |
86
- | `--git` / `--no-git` | no | off | Initialize a Git repo (`git init` + `main` branch + initial commit). |
87
- | `--jwt` / `--no-jwt` | no | off | Generate JWT secrets via the project's `jwt` script. |
88
- | `--yes` / `-y` | no | off | Fully headless mode. |
89
- | `--interactive`, `--customize` | no | off | Restore the full long-form wizard. |
90
-
91
- With `--pm=pnpm`, the scaffold reads `pnpm --version` and records install-script
92
- approvals (esbuild, and Sharp when the `image` feature is selected) where that
93
- version reads them: `allowBuilds` in `pnpm-workspace.yaml` on pnpm 10.26+, 11
94
- and 12; that file plus `onlyBuiltDependencies` on 10.5–10.25; and, before 10.5,
95
- no workspace file (those versions reject one without `packages:`) but
96
- `pnpm.onlyBuiltDependencies` in `package.json`. An explicit existing denial is
97
- retained. If selected feature dependencies do
98
- not install, JWT generation is skipped and reported rather than retried through
75
+ | Flag | Takes value | Default | Purpose |
76
+ | ------------------------------ | ----------- | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
77
+ | `<positional>` / `--name` | yes | — (required) | Project name + target directory. |
78
+ | `--stack` | yes | `api` | `api` or `web` — the structural choice; `web` seeds the `web` feature by default. |
79
+ | `--db` | yes | `mongodb` | Database driver key (`mongodb`, `postgres`, or `none`). |
80
+ | `--no-db` | no | — | Opt out of a database — shorthand for `--db=none`. |
81
+ | `--pm` | yes | inferred (`npm_config_user_agent` / system) | Package manager — use `npm` for the commands in this guide. |
82
+ | `--features` | yes (CSV) | none, or `["web"]` when `--stack=web` and `--features` is not given | Comma-separated optional feature keys. An explicit `--features` always wins over the `--stack` default. |
83
+ | `--lint` | yes | `oxc` | `oxc` (Oxlint/Oxfmt); removed or invalid values fail before writes. |
84
+ | `--ai` | yes (CSV) | none | Comma-separated AI provider keys (auto-pulls `@warlock.js/ai`). |
85
+ | `--agents` | yes (CSV) | `claude` | Comma-separated `agent-kit` targets — see below. |
86
+ | `--git` / `--no-git` | no | off | Initialize a Git repo (`git init` + `main` branch + initial commit). |
87
+ | `--jwt` / `--no-jwt` | no | off | Generate JWT secrets via the project's `jwt` script. |
88
+ | `--yes` / `-y` | no | off | Fully headless mode. |
89
+ | `--interactive`, `--customize` | no | off | Restore the full long-form wizard. |
90
+
91
+ If selected feature dependencies do not install, JWT generation is skipped and reported rather than retried through
99
92
  the package manager.
100
93
 
101
94
  Value flags accept either `--db=postgres` or `--db postgres`. Unknown `--pm` / `--db` / `--features` / `--ai` / `--agents` / `--lint` values fail fast before any write or install.
package/llms.txt CHANGED
@@ -6,4 +6,4 @@
6
6
 
7
7
  ## Skills
8
8
 
9
- - [create-a-warlock-project](create-warlock/create-a-warlock-project/SKILL.md): Create a Warlock project with the canonical scaffold, package selection, and project conventions.
9
+ - [create-warlock](create-warlock/skills/SKILL.md): Scaffold Warlock apps with `npm create warlock@latest` or `npx create-warlock`; the CLI entry exports `parseFlags`, `packageVersion` and default `createApp`. Use for "create a project", "start a backend or SSR app", choose a database, AI adapters, features or linting, and automate setup with `--yes` and `--pm=npm`. Not this package → @warlock.js/core for extending an existing app; @warlock.js/web for page APIs.
package/package.json CHANGED
@@ -13,7 +13,7 @@
13
13
  "dependencies": {
14
14
  "@clack/prompts": "^0.8.2",
15
15
  "@mongez/copper": "^2.1.3",
16
- "@warlock.js/fs": "5.34.1",
16
+ "@warlock.js/fs": "5.34.3",
17
17
  "@mongez/reinforcements": "^4.0.2",
18
18
  "cross-spawn": "^7.0.3",
19
19
  "rimraf": "^6.0.1",
@@ -22,7 +22,7 @@
22
22
  "engines": {
23
23
  "node": ">=22.15"
24
24
  },
25
- "version": "5.34.1",
25
+ "version": "5.34.3",
26
26
  "type": "module",
27
27
  "main": "./esm/index.mjs",
28
28
  "module": "./esm/index.mjs",
@@ -1,6 +1,6 @@
1
1
  ---
2
- name: create-a-warlock-project
3
- description: "Create a Warlock project with the canonical scaffold, package selection, and project conventions."
2
+ name: create-warlock
3
+ description: 'Scaffold Warlock apps with `npm create warlock@latest` or `npx create-warlock`; the CLI entry exports `parseFlags`, `packageVersion` and default `createApp`. Use for "create a project", "start a backend or SSR app", choose a database, AI adapters, features or linting, and automate setup with `--yes` and `--pm=npm`. Not this package → @warlock.js/core for extending an existing app; @warlock.js/web for page APIs.'
4
4
  ---
5
5
 
6
6
  # create-warlock — scaffold a new project
@@ -45,7 +45,7 @@ that is just the project name, the one required answer with no default:
45
45
 
46
46
  ```
47
47
  A project name is required and no terminal is available to ask for one. Pass it as
48
- the first argument or --name=<name>. Non-interactive flags: --yes, --pm=<npm|yarn|pnpm>,
48
+ the first argument or --name=<name>. Non-interactive flags: --yes, --pm=npm,
49
49
  --db=<driver>|--no-db, --features=<list>, --ai=<list>, --lint=<oxc>, --git|--no-git, --jwt|--no-jwt.
50
50
  ```
51
51
 
@@ -64,30 +64,23 @@ Features: none
64
64
 
65
65
  ## Flags
66
66
 
67
- | Flag | Takes value | Default | Purpose |
68
- | ------------------------------ | ----------- | ------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
69
- | `<positional>` / `--name` | yes | — (required) | Project name + target directory. |
70
- | `--stack` | yes | `api` | `api` or `web` — the structural choice; `web` seeds the `web` feature by default. |
71
- | `--db` | yes | `mongodb` | Database driver key (`mongodb`, `postgres`, or `none`). |
72
- | `--no-db` | no | — | Opt out of a database — shorthand for `--db=none`. |
73
- | `--pm` | yes | inferred (`npm_config_user_agent` / system) | Package manager — must be one of `npm` / `yarn` / `pnpm` / `bun`; anything else is rejected before scaffolding starts. |
74
- | `--features` | yes (CSV) | none, or `["web"]` when `--stack=web` and `--features` is not given | Comma-separated optional feature keys. An explicit `--features` always wins over the `--stack` default. |
75
- | `--lint` | yes | `oxc` | `oxc` (Oxlint/Oxfmt); removed or invalid values fail before writes. |
76
- | `--ai` | yes (CSV) | none | Comma-separated AI provider keys (auto-pulls `@warlock.js/ai`). |
77
- | `--agents` | yes (CSV) | `claude` | Comma-separated `agent-kit` targets — see below. |
78
- | `--git` / `--no-git` | no | off | Initialize a Git repo (`git init` + `main` branch + initial commit). |
79
- | `--jwt` / `--no-jwt` | no | off | Generate JWT secrets via the project's `jwt` script. |
80
- | `--yes` / `-y` | no | off | Fully headless mode. |
81
- | `--interactive`, `--customize` | no | off | Restore the full long-form wizard. |
82
-
83
- With `--pm=pnpm`, the scaffold reads `pnpm --version` and records install-script
84
- approvals (esbuild, and Sharp when the `image` feature is selected) where that
85
- version reads them: `allowBuilds` in `pnpm-workspace.yaml` on pnpm 10.26+, 11
86
- and 12; that file plus `onlyBuiltDependencies` on 10.5–10.25; and, before 10.5,
87
- no workspace file (those versions reject one without `packages:`) but
88
- `pnpm.onlyBuiltDependencies` in `package.json`. An explicit existing denial is
89
- retained. If selected feature dependencies do
90
- not install, JWT generation is skipped and reported rather than retried through
67
+ | Flag | Takes value | Default | Purpose |
68
+ | ------------------------------ | ----------- | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
69
+ | `<positional>` / `--name` | yes | — (required) | Project name + target directory. |
70
+ | `--stack` | yes | `api` | `api` or `web` — the structural choice; `web` seeds the `web` feature by default. |
71
+ | `--db` | yes | `mongodb` | Database driver key (`mongodb`, `postgres`, or `none`). |
72
+ | `--no-db` | no | — | Opt out of a database — shorthand for `--db=none`. |
73
+ | `--pm` | yes | inferred (`npm_config_user_agent` / system) | Package manager — use `npm` for the commands in this guide. |
74
+ | `--features` | yes (CSV) | none, or `["web"]` when `--stack=web` and `--features` is not given | Comma-separated optional feature keys. An explicit `--features` always wins over the `--stack` default. |
75
+ | `--lint` | yes | `oxc` | `oxc` (Oxlint/Oxfmt); removed or invalid values fail before writes. |
76
+ | `--ai` | yes (CSV) | none | Comma-separated AI provider keys (auto-pulls `@warlock.js/ai`). |
77
+ | `--agents` | yes (CSV) | `claude` | Comma-separated `agent-kit` targets — see below. |
78
+ | `--git` / `--no-git` | no | off | Initialize a Git repo (`git init` + `main` branch + initial commit). |
79
+ | `--jwt` / `--no-jwt` | no | off | Generate JWT secrets via the project's `jwt` script. |
80
+ | `--yes` / `-y` | no | off | Fully headless mode. |
81
+ | `--interactive`, `--customize` | no | off | Restore the full long-form wizard. |
82
+
83
+ If selected feature dependencies do not install, JWT generation is skipped and reported rather than retried through
91
84
  the package manager.
92
85
 
93
86
  Value flags accept either `--db=postgres` or `--db postgres`. Unknown `--pm` / `--db` / `--features` / `--ai` / `--agents` / `--lint` values fail fast before any write or install.
@@ -4,7 +4,9 @@
4
4
  "env": { "node": true, "es2021": true },
5
5
  "categories": { "correctness": "error" },
6
6
  "rules": {
7
- "typescript/no-explicit-any": "off",
7
+ "typescript/no-explicit-any": "error",
8
+ "curly": "error",
9
+ "no-console": "error",
8
10
  "no-unused-vars": [
9
11
  "warn",
10
12
  {
@@ -4,7 +4,9 @@
4
4
  "env": { "node": true, "es2021": true },
5
5
  "categories": { "correctness": "error" },
6
6
  "rules": {
7
- "typescript/no-explicit-any": "off",
7
+ "typescript/no-explicit-any": "error",
8
+ "curly": "error",
9
+ "no-console": "error",
8
10
  "no-unused-vars": [
9
11
  "warn",
10
12
  {
@@ -5,7 +5,7 @@
5
5
  > `.gemini/GEMINI.md`, `.github/copilot-instructions.md`, `CONVENTIONS.md`, …)
6
6
  > are **derived** from this file by
7
7
  > [agent-kit](https://github.com/hassanzohdy/agent-kit). Edit **this** file, then
8
- > run `yarn skills:sync` (or `npx agent-kit sync`) to regenerate them — never edit
8
+ > run `npm run skills:sync` (or `npx agent-kit sync`) to regenerate them — never edit
9
9
  > the derived files directly.
10
10
 
11
11
  ## Project overview
@@ -51,7 +51,7 @@ The rule is: **skill first, source second, web last.**
51
51
  `.cursor/skills/`, GitHub Copilot → `.github/skills/`, Codex → `.codex/skills/`,
52
52
  and the equivalent directory for other agents).
53
53
 
54
- If a skill you expect is missing, run `yarn skills:sync` (it also runs
54
+ If a skill you expect is missing, run `npm run skills:sync` (it also runs
55
55
  automatically on `postinstall`).
56
56
 
57
57
  ## Available project skills (`skills/`)
@@ -75,19 +75,19 @@ automatically on `postinstall`).
75
75
 
76
76
  ## Commands
77
77
 
78
- - `yarn dev` — start the dev server (`warlock dev`)
79
- - `yarn build` — type-check then build for production (`tsc && warlock build`)
80
- - `yarn start` — start the production server (`warlock start`)
81
- - `yarn migrate` / `yarn migrate.fresh` / `yarn migrate.list` — run migrations
82
- - `yarn seed` — run database seeders
83
- - `yarn lint` / `yarn format` / `yarn tsc` — lint and format with the selected toolchain, and type-check (`tsc --noEmit`)
78
+ - `npm run dev` — start the dev server (`warlock dev`)
79
+ - `npm run build` — type-check then build for production (`tsc && warlock build`)
80
+ - `npm run start` — start the production server (`warlock start`)
81
+ - `npm run migrate` / `npm run migrate.fresh` / `npm run migrate.list` — run migrations
82
+ - `npm run seed` — run database seeders
83
+ - `npm run lint` / `npm run format` / `npm run tsc` — lint and format with the selected toolchain, and type-check (`tsc --noEmit`)
84
84
  - `npx warlock check` — verify a change: typecheck, lint, the affected tests and the framework's conventions in one report (`--changed` for only what changed); run it after every change and loop until it exits 0
85
- - `yarn skills:sync` — re-sync skills from `node_modules` and regenerate the
85
+ - `npm run skills:sync` — re-sync skills from `node_modules` and regenerate the
86
86
  derived per-agent files from this `AGENTS.md`
87
87
 
88
88
  ## Code style
89
89
 
90
- Follow the **code-standards** skill (`skills/code-standards/SKILL.md`) for all
90
+ Follow the `code-standards` skill for all
91
91
  TypeScript — read it before writing or reviewing any `.ts` / `.tsx`. In short:
92
92
  `interface` for contracts and `type` for data shapes, no `any`, access modifiers
93
93
  on every class member, single-responsibility files with the documented naming
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: api-design
3
- description: 'Project-wide HTTP API conventions for Warlock.js apps — list endpoints return `{ <pluralResourceName>, pagination }`, single-item endpoints return `{ <resourceCamelName>: <object> }`, delete returns `204 No Content` (or `{ message }`); framework response helpers map 1:1 to status codes (`response.success` / `successCreate` / `noContent` / `accepted` / `badRequest` / `unauthorized` / `forbidden` / `notFound` / `conflict` / `unprocessableEntity` / `tooManyRequests` / `serverError`); kebab-case plural URL nouns (`/ai-models`); snake_case fields inside resources; offset pagination by default, cursor for time-ordered firehose; list query params validated by a schema (caps `limit`, whitelists `orderBy`, strips unknown keys — never raw `request.all()`); typed controllers via `GuardedRequestHandler<Schema>` (guarded) / `RequestHandler<Request<Schema>>` (public) with the schema in the `schema/` folder + `request.validated()` — the per-endpoint `requests/` folder is retired; not-found is thrown from the service (`ResourceNotFoundError`) so controllers stay clean; resources are output-only (`defineResource`); RESTful routes via `router.route().list().show().create().update().destroy()`; versioning guidance for the rare app that needs it. Triggers: writing a controller, route handler, or HTTP endpoint; shaping a response body; choosing a status code; adding pagination to a list endpoint; designing a `.resource.ts`; defining a route; deciding what `request.validated()` vs `request.input(...)` returns; where the schema file lives; how to return not-found; whether to version an API; user asks "what status code", "how do we paginate", "what does the response look like", "list vs single envelope", "snake_case or camelCase in responses", "how do we structure a controller", "where does the schema go", "should I version my API". Skip: input validation rules (load `skills/security-baseline/SKILL.md` § 1); pagination performance / N+1 (load `skills/observability-and-resilience/SKILL.md`); data column naming inside models (load `skills/data-and-persistence/SKILL.md`); framework router internals — load `@warlock.js/core` route skills; CSS / UI work.'
3
+ description: "HTTP envelopes, camelCase resources, opaque public IDs, status codes, pagination, responseSchema, routes and idempotency headers. Use when designing or reviewing an API endpoint. Use security-baseline instead for input trust, authentication and authorization."
4
4
  ---
5
5
 
6
6
  # API design
@@ -16,7 +16,7 @@ The project-wide contract between server and client. The frontend should be able
16
16
 
17
17
  ## 1. Response envelope
18
18
 
19
- The project uses **three envelope shapes**, one per response kind. Each kind has its own controller pattern. Do not invent a fourth.
19
+ The project uses **three envelope shapes**, one per response kind. Each success kind has its own controller pattern. All errors use § 1.4.
20
20
 
21
21
  ### 1.1 List endpoints — `{ <pluralResourceName>, pagination }`
22
22
 
@@ -47,10 +47,10 @@ Read, create, and update endpoints return the resource under its camelCase singu
47
47
  ```typescript
48
48
  // ✅ get-faq.service.ts — the service throws
49
49
  import { ResourceNotFoundError } from "@warlock.js/core";
50
- import { faqsRepository } from "../repositories/faqs.repository";
50
+ import { Faq } from "../models/faq/faq.model";
51
51
 
52
- export async function getFaqService(id: number | string) {
53
- const faq = await faqsRepository.getCached(id);
52
+ export async function getFaqService(id: string) {
53
+ const faq = await Faq.findOne({ publicId: id });
54
54
 
55
55
  if (!faq) {
56
56
  throw new ResourceNotFoundError("Faq resource not found!");
@@ -60,13 +60,20 @@ export async function getFaqService(id: number | string) {
60
60
  }
61
61
 
62
62
  // ✅ get-faq.controller.ts — stays clean, no null-check
63
- export const getFaqController: RequestHandler = async (request, response) => {
64
- const faq = await getFaqService(request.input("id"));
63
+ import { v } from "@warlock.js/seal";
64
+ import type { Request, RequestHandler } from "@warlock.js/core";
65
+ export const getFaqController: RequestHandler<Request<{ id: string }>> = async ({
66
+ request,
67
+ response,
68
+ }) => {
69
+ const faq = await getFaqService(request.validated().id);
65
70
 
66
71
  return response.success({
67
72
  faq,
68
73
  });
69
74
  };
75
+
76
+ getFaqController.validation = { params: v.object({ id: v.string().uuid() }) };
70
77
  ```
71
78
 
72
79
  The framework maps `ResourceNotFoundError` to a `404` response automatically — the controller doesn't translate it.
@@ -79,11 +86,16 @@ The framework maps `ResourceNotFoundError` to a `404` response automatically —
79
86
 
80
87
  ```typescript
81
88
  // ✅ default — nothing to say, say nothing
82
- export const deleteFaqController: RequestHandler = async (request, response) => {
83
- await deleteFaqService(request.input("id"));
89
+ export const deleteFaqController: RequestHandler<Request<{ id: string }>> = async ({
90
+ request,
91
+ response,
92
+ }) => {
93
+ await deleteFaqService(request.validated().id);
84
94
 
85
95
  return response.noContent();
86
96
  };
97
+
98
+ deleteFaqController.validation = { params: v.object({ id: v.string().uuid() }) };
87
99
  ```
88
100
 
89
101
  - Default to `204 No Content` — a successful delete has no body worth sending.
@@ -92,21 +104,28 @@ export const deleteFaqController: RequestHandler = async (request, response) =>
92
104
 
93
105
  ### 1.4 Error responses
94
106
 
95
- Use the framework's status helpers — never hand-roll an error body.
107
+ Every JSON error response has this shape, with optional `details` for safe field errors or domain context:
108
+
109
+ ```json
110
+ {
111
+ "error": { "code": "notFound", "message": "Resource not found." },
112
+ "requestId": "request-correlation-id"
113
+ }
114
+ ```
115
+
116
+ Use the matching status helper or throw a framework error from the service. The starter's `http.errorResponse` formatter normalizes helper bodies and framework-generated JSON errors, including unmatched-route 404, validation 422, rate-limit 429, and 500. It removes stacks and masks all 5xx messages. `requestId` is the real Warlock `request.id`, read from the framework's correlation header at the transport boundary; a rejection before Warlock creates a Request uses Fastify's request ID and echoes that same value in the header. Do not generate a new ID inside a controller or use a user ID. See the `observability-and-resilience` skill for trace propagation.
96
117
 
97
118
  ```typescript
98
- // ✅
99
- return response.notFound(); // { error: "notFound" }, 404
100
- return response.unauthorized(); // { error: "unauthorized" }, 401
101
- return response.conflict({ error: "Order already cancelled" });
102
- return response.badRequest({ error: "Validation failed", otherInfo: "whatever" });
103
-
104
- // ❌
105
- return response.send({ error: "..." }, 404); // bypassing the helper
106
- return response.success({ error: "..." }); // 200 with an error body — never
119
+ return response.notFound(); // formatter supplies { error: { code, message }, requestId }
120
+ return response.conflict({
121
+ error: { code: "orderCancelled", message: "Order already cancelled." },
122
+ });
123
+ return response.badRequest({ error: { code: "invalidRequest", message: "Invalid request." } });
107
124
  ```
108
125
 
109
- Never `200 OK` with an `error` field — the status code carries the outcome.
126
+ `http.errorMapper` translates thrown non-`HttpError` domain errors to `{ status, message?, payload? }`; it does **not** format response bodies. It skips `HttpError` subclasses, returned validation/helper responses, and Fastify's unmatched-route and global rate-limit responses. The starter config uses it to keep unknown thrown errors opaque; `http.errorResponse` handles the final JSON shape for all these paths. HTML error pages, files, and already-started streams retain their own contracts; neither hook rewrites their bodies.
127
+
128
+ Never send a `200 OK` with an `error` field. Never put secrets, stack traces, raw request bodies, or internal database IDs in `details`.
110
129
 
111
130
  ---
112
131
 
@@ -132,7 +151,7 @@ One canonical mapping. Use the framework helpers; the helper's name **is** the s
132
151
 
133
152
  **The 401 vs 403 distinction:** 401 means "I don't know who you are." 403 means "I know who you are, and you can't do this." Mixing them leaks information.
134
153
 
135
- **404 vs 403 on private resources:** if revealing existence is itself a leak ("this private workspace exists, you just can't see it"), return 404. The framework default `{ error: "notFound" }` is fine — don't expose internal reasons.
154
+ **404 vs 403 on private resources:** if revealing existence is itself a leak ("this private workspace exists, you just can't see it"), return 404. The canonical error with code `notFound` is sufficient — don't expose internal reasons.
136
155
 
137
156
  ---
138
157
 
@@ -184,7 +203,7 @@ Use when the dataset is large, append-mostly, and time-ordered: chat messages, e
184
203
 
185
204
  Filters go in query params named after the filterable field (`?status=active&type=basic`). The query schema (§ 3.5) validates each value's type; the repository's `filterBy` rules then map accepted fields to WHERE clauses.
186
205
 
187
- Sort: `?orderBy=created_at&orderDir=desc`. Default sort lives on the repository's `defaultOptions`.
206
+ Sort: `?orderBy=createdAt&orderDir=desc`. Default sort lives on the repository's `defaultOptions`.
188
207
 
189
208
  ### 3.5 Validate the query — schema first, strip unknown
190
209
 
@@ -192,7 +211,7 @@ List endpoints validate their query params with a `@warlock.js/seal` schema, exa
192
211
 
193
212
  The schema earns its place three ways:
194
213
 
195
- - **Kills operator injection.** Declaring `status: v.enum(OrderStatus)` rejects a smuggled `{ $ne: null }` at the boundary — `filterBy` alone may pass it straight through (see `skills/security-baseline/SKILL.md` § 4.2).
214
+ - **Kills operator injection.** Declaring `status: v.enum(OrderStatus)` rejects a smuggled `{ $ne: null }` at the boundary — `filterBy` alone may pass it straight through (see the `security-baseline` skill § 4.2).
196
215
  - **Makes the limit cap real.** `limit: v.int().min(1).max(100)` turns the § 3.3 ceiling into enforcement — `?limit=9999999` becomes a `422`, not a table scan.
197
216
  - **Whitelists `orderBy`.** Restricting sort to known indexed columns closes the slow-query vector.
198
217
 
@@ -205,7 +224,7 @@ export const listOrdersSchema = v.object({
205
224
  status: v.enum(OrderStatus).optional(),
206
225
  page: v.int().min(1).default(1),
207
226
  limit: v.int().min(1).max(100).default(20),
208
- orderBy: v.literal("created_at", "total_cents").optional(),
227
+ orderBy: v.literal("createdAt", "totalCents").optional(),
209
228
  });
210
229
 
211
230
  export type ListOrdersSchema = Infer<typeof listOrdersSchema>;
@@ -214,7 +233,7 @@ export type ListOrdersSchema = Infer<typeof listOrdersSchema>;
214
233
  const { data, pagination } = await listOrdersService(request.validated());
215
234
  ```
216
235
 
217
- **Strip unknown keys — don't disallow them.** Seal strips extra keys by default, which is exactly right for query strings: they routinely carry incidental params (`?utm_source=...`, cache-busters `?_=1718`). Throwing a `422` on those is hostile. Reserve disallow-unknown for request *bodies*, where an unexpected key usually signals a client bug or a renamed field.
236
+ **Strip unknown keys — don't disallow them.** Seal strips extra keys by default, which is exactly right for query strings: they routinely carry incidental params (`?utm_source=...`, cache-busters `?_=1718`). Throwing a `422` on those is hostile. Reserve disallow-unknown for request _bodies_, where an unexpected key usually signals a client bug or a renamed field.
218
237
 
219
238
  ---
220
239
 
@@ -224,7 +243,8 @@ const { data, pagination } = await listOrdersService(request.validated());
224
243
 
225
244
  - **Plural, kebab-case nouns:** `/ai-models`, `/ai-api-keys`, `/contacts`.
226
245
  - **Nested resources only one level deep:** `/orders/:id/items`. If you need two levels (`/orders/:id/items/:itemId/refunds`), the inner resource probably deserves to be top-level.
227
- - **Actions use verb suffixes on the resource path:** `POST /orders/:id/cancel`, not `POST /cancel-order`. The verb is the second-to-last segment.
246
+ - **Actions use verb suffixes on the resource path:** `POST /orders/:id/cancel`, not `POST /cancel-order`. The verb is the final segment.
247
+ - **Public path IDs are opaque UUIDs.** `/orders/:id` resolves the model's `publicId`; resource `id` is that same value. Cascade's default numeric `id` and its stringifying `uuid` getter are internal identities, not public UUID generators. See the `data-and-persistence` skill.
228
248
  - **Query params are filters, never identity.** Identity goes in the path (`/orders/:id`), filters in the query string (`?status=paid`).
229
249
 
230
250
  ### 4.2 Route registration — the RESTful builder
@@ -259,7 +279,7 @@ For endpoints that don't fit the RESTful five (e.g. `POST /orders/:id/cancel`),
259
279
 
260
280
  ## 5. Resources (`.resource.ts`)
261
281
 
262
- A resource is the **wire format** for a model. Output-only. No reconciliation, no hydration, no computed side effects — those live in services or model accessors (see project memory `feedback_resources_output_only`).
282
+ A resource is the **wire format** for a model. Output-only. No reconciliation, no hydration, no computed side effects — those live in services or model accessors.
263
283
 
264
284
  ### 5.1 Definition
265
285
 
@@ -269,32 +289,42 @@ import { defineResource } from "@warlock.js/core";
269
289
 
270
290
  export const AiModelResource = defineResource({
271
291
  schema: {
272
- id: "string",
292
+ id: ["publicId", "string"],
273
293
  provider: "string",
274
294
  name: "string",
275
295
  description: "string",
276
- provider_model_id: "string",
277
- is_free: "boolean",
278
- input_price: "number",
279
- output_price: "number",
296
+ providerModelId: "string",
297
+ isFree: "boolean",
298
+ inputPrice: "number",
299
+ outputPrice: "number",
280
300
  capabilities: "object",
281
301
  features: "array",
282
- created_by: "string",
283
- updated_by: "string",
302
+ createdBy: "string",
303
+ updatedBy: "string",
284
304
  },
285
305
  });
286
306
  ```
287
307
 
288
- ### 5.2 Field naming — snake_case
308
+ ### 5.2 Field naming — camelCase everywhere
289
309
 
290
- Field names inside the resource schema use **snake_case**, matching the framework's wire convention: `provider_model_id`, `is_free`, `created_by`. The model layer above uses camelCase; the resource is where the convention shifts to wire format.
310
+ Envelope keys **and resource fields** are camelCase: `aiModel`, `providerModelId`, `isFree`, `createdBy`. `defineResource` preserves each schema key as the output key; it does not automatically convert naming styles. A string cast reads the same source key. A tuple `[sourceKey, castType]` reads a differently named stored column:
291
311
 
292
- Top-level envelope keys (the wrapper around the resource: `aiModel`, `aiModels`, `pagination`, `message`) remain camelCase.
312
+ ```typescript
313
+ export const OrderResource = defineResource({
314
+ schema: {
315
+ id: ["publicId", "string"],
316
+ totalCents: ["total_cents", "int"],
317
+ createdAt: ["created_at", "date"],
318
+ },
319
+ });
320
+ ```
321
+
322
+ This supports legacy snake_case storage without leaking snake_case into JSON. New starter domain columns use camelCase; driver-managed timestamps can still be snake_case (PostgreSQL). Select the tuple's source key to match the actual stored field. See the `@warlock.js/core` skill's `define-resource` topic.
293
323
 
294
324
  ### 5.3 Rules
295
325
 
296
326
  - A resource never reaches into another model — if you need composed data, the service composes the result and passes one final object to the resource.
297
- - A resource never runs business logic — if a value is computed (e.g. `total_spent_cents`), compute it in the service and pass the already-computed field.
327
+ - A resource never runs business logic — if a value is computed (e.g. `totalSpentCents`), compute it in the service and pass the already-computed field.
298
328
  - Sensitive fields are omitted at the resource level, not redacted at the log layer.
299
329
 
300
330
  ---
@@ -305,7 +335,7 @@ Top-level envelope keys (the wrapper around the resource: `aiModel`, `aiModels`,
305
335
 
306
336
  A write endpoint is **two files**: the schema in the module's `schema/` folder, and the controller. The old per-endpoint `requests/` folder is retired — the controller is typed directly via a generic request-handler type, so no separate `Request<Schema>` wrapper file is needed.
307
337
 
308
- List endpoints follow the **same** two-file shape — a `list-<noun>.schema.ts` validates the query string (see § 3.5). Only single-identifier reads (`get-<noun>` by id) skip the schema and use `request.input("id")`.
338
+ List endpoints follow the **same** two-file shape — a `list-<noun>.schema.ts` validates the query string (see § 3.5). Validate public IDs as strings (UUIDs for starter models), including single-identifier reads. A service accepts the validated public ID and looks up `publicId`, never the sequential primary key.
309
339
 
310
340
  ```
311
341
  schema/create-lead.schema.ts ← v.object({...}) + Infer<>
@@ -334,13 +364,13 @@ import { type GuardedRequestHandler } from "app/auth/requests/guarded.request";
334
364
  import { type CreateLeadSchema, createLeadSchema } from "../schema/create-lead.schema";
335
365
  import { createLeadService } from "../services/create-lead.service";
336
366
 
337
- export const createLeadController: GuardedRequestHandler<CreateLeadSchema> = async (
367
+ export const createLeadController: GuardedRequestHandler<CreateLeadSchema> = async ({
338
368
  request,
339
369
  response,
340
- ) => {
370
+ }) => {
341
371
  const lead = await createLeadService(request.validated());
342
372
 
343
- return response.success({
373
+ return response.successCreate({
344
374
  lead,
345
375
  });
346
376
  };
@@ -352,24 +382,51 @@ createLeadController.validation = {
352
382
 
353
383
  ### 6.2 Picking the handler type
354
384
 
355
- | Route kind | Controller type | What it adds |
356
- | -------------------------------- | ------------------------------------- | ------------------------------------- |
357
- | Guarded (auth required) + body | `GuardedRequestHandler<Schema>` | typed `request.validated()` + `request.locals.user` |
358
- | Public + body | `RequestHandler<Request<Schema>>` | typed `request.validated()` |
359
- | Read / list (no body) | `RequestHandler` | nothing extra |
385
+ | Route kind | Controller type | What it adds |
386
+ | ------------------------------ | --------------------------------- | --------------------------------------------------- |
387
+ | Guarded (auth required) + body | `GuardedRequestHandler<Schema>` | typed `request.validated()` + `request.locals.user` |
388
+ | Public + body | `RequestHandler<Request<Schema>>` | typed `request.validated()` |
389
+ | Read / list (no body) | `RequestHandler` | nothing extra |
360
390
 
361
391
  `GuardedRequestHandler<Schema>` (defined in `app/auth/requests/guarded.request.ts`) is `RequestHandler<GuardedRequest<Schema>>` — it both types the validated payload and guarantees `request.locals.user` is present.
362
392
 
363
393
  ### 6.3 Accessing inputs
364
394
 
365
395
  - `request.validated()` — typed access to the schema-validated body. Use this in every write endpoint. The type comes from the handler's generic, so no cast is needed.
366
- - `request.input(name)` — single named input (path / query / body fallback). Use for read endpoints that take a single identifier.
396
+ - `request.input(name)` — raw single named input (path / query / body fallback). Validate it before passing an identifier to a service; a TypeScript annotation alone is not validation.
367
397
  - `request.all()` — raw, unvalidated payload. **Avoid it.** List endpoints take a query schema and use `request.validated()` just like writes (see § 3.5); `request.all()` bypasses the boundary type-guard and should never appear on a public route.
368
398
 
369
399
  ### 6.4 Validation failures
370
400
 
371
401
  The framework returns `422 Unprocessable Entity` with field-level errors automatically when `controller.validation.schema` rejects the input. Do not handle validation errors inside the controller — let the framework boundary do it.
372
402
 
403
+ ### 6.5 Declare every controller's responseSchema
404
+
405
+ Attach `responseSchema` beside `validation` on every controller, including list/read/delete handlers. It declares each documented status body; it is not runtime response validation. Export resources from their own `.resource.ts` files so route generation can resolve them.
406
+
407
+ ```typescript
408
+ createLeadController.responseSchema = {
409
+ 201: { body: { lead: LeadResource } },
410
+ 422: { body: { error: { code: "string", message: "string" }, requestId: "string" } },
411
+ };
412
+ ```
413
+
414
+ Use `response.successCreate({ lead })` for that 201. List schemas describe the plural resource array plus `pagination`; 204 has no body. `npx warlock generate.typings` (also run by `npx warlock dev`) generates named route declarations. The web `ApiResponse` type and `useSubmitForm({ route })` infer these declared status bodies. `npx warlock generate.openapi` uses the same declarations for OpenAPI. See the `@warlock.js/core` skill's `create-controller` and `generate-openapi` topics and the `@warlock.js/web` skill's `submit-a-form` topic.
415
+
416
+ ### 6.6 Side effects require an Idempotency-Key header
417
+
418
+ For payments, email sends, webhook dispatch, and other retry-sensitive writes, require `Idempotency-Key` at the HTTP boundary. The caller creates one key per user intent and reuses it for retries with the same body:
419
+
420
+ ```http
421
+ POST /orders
422
+ Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
423
+ Content-Type: application/json
424
+
425
+ {"cartId":"6ba7b810-9dad-41d1-80b4-00c04fd430c8"}
426
+ ```
427
+
428
+ Attach `middleware.idempotency()` **after authentication**. It replays a cached status/body for the same caller, key and body and marks `Idempotent-Replay: true`; a changed body with the same key returns 422, an in-flight request returns 409. The middleware passes through a missing key, so add an app middleware that validates and requires `request.header("idempotency-key")` before it. Configure a shared cache driver for multiple processes and define a retention TTL. Do not cache a retry-sensitive operation only in component state. The service must also make downstream side effects idempotent: a response cache alone cannot make a remote charge and a local write atomic. See the `@warlock.js/core` skill's `use-middleware` topic.
429
+
373
430
  ---
374
431
 
375
432
  ## 7. Caching
@@ -453,9 +510,12 @@ Before merging any controller / route / resource change:
453
510
  - [ ] Routes registered via `router.route(...).list().show().create().update().destroy()` (or explicit `router.<verb>(...)` for custom actions)
454
511
  - [ ] Auth-required routes wrapped in `guarded(() => { ... })`
455
512
  - [ ] Write endpoint: schema in `schema/` folder, controller typed `GuardedRequestHandler<Schema>` / `RequestHandler<Request<Schema>>`, `controller.validation = { schema }`, no separate `requests/` file
456
- - [ ] Read endpoint uses `request.input(...)` for identity, not `request.all()`
513
+ - [ ] Read endpoint validates its opaque public ID before service lookup
457
514
  - [ ] Resource is output-only — no logic, no reaching into other models
458
- - [ ] Resource field names are snake_case
515
+ - [ ] Envelope and resource keys are camelCase; legacy stored columns use source-key tuples
516
+ - [ ] Every controller declares `responseSchema`; generated client/OpenAPI types match its statuses
517
+ - [ ] Errors use `{ error: { code, message }, details?, requestId }`, including framework failures
518
+ - [ ] Side-effecting endpoints require `Idempotency-Key`, scoped after auth and reused on retries
459
519
  - [ ] `Cache-Control` set explicitly on `GET` endpoints
460
520
  - [ ] Versioning: additive-only by default; explicit `/vN` prefix only when a real breaking change forces it
461
521
  - [ ] Deprecated fields announced via `Deprecation` / `Sunset` headers, not silently changed