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.
- package/CHANGELOG.md +16 -0
- package/llms-full.txt +21 -28
- package/llms.txt +1 -1
- package/package.json +2 -2
- package/skills/{create-a-warlock-project/SKILL.md → SKILL.md} +20 -27
- package/templates/toolchains/oxc/.oxlintrc.json +3 -1
- package/templates/warlock/.oxlintrc.json +3 -1
- package/templates/warlock/AGENTS.md +10 -10
- package/templates/warlock/skills/api-design/SKILL.md +110 -50
- package/templates/warlock/skills/code-standards/SKILL.md +48 -39
- package/templates/warlock/skills/data-and-persistence/SKILL.md +72 -57
- package/templates/warlock/skills/git-workflow/SKILL.md +9 -9
- package/templates/warlock/skills/module-boundaries/SKILL.md +2 -2
- package/templates/warlock/skills/observability-and-resilience/SKILL.md +8 -6
- package/templates/warlock/skills/security-baseline/SKILL.md +23 -22
- package/templates/warlock/skills/testing-strategy/SKILL.md +7 -7
- package/templates/warlock/skills/web-code-standards/SKILL.md +157 -60
- package/templates/warlock/src/app/auth/models/otp/otp.model.ts +9 -3
- package/templates/warlock/src/app/auth/services/otp.service.ts +18 -6
- package/templates/warlock/src/app/contact/controllers/contact.controller.ts +6 -0
- package/templates/warlock/src/app/locale/controllers/locale.controller.ts +6 -0
- package/templates/warlock/src/app/posts/controllers/create-new-post.controller.ts +7 -0
- package/templates/warlock/src/app/posts/controllers/update-post.controller.ts +14 -9
- package/templates/warlock/src/app/posts/models/post/migrations/09-01-2026_02-07-51-post.migration.ts +1 -0
- package/templates/warlock/src/app/posts/models/post/post.model.ts +6 -0
- package/templates/warlock/src/app/posts/resources/post.resource.ts +1 -1
- package/templates/warlock/src/app/posts/schema/update-post.schema.ts +1 -0
- package/templates/warlock/src/app/shared/utils/format-http-error-response.ts +41 -0
- package/templates/warlock/src/app/users/commands/hello-world.command.ts +2 -1
- package/templates/warlock/src/app/users/controllers/create-new-user.controller.ts +12 -1
- package/templates/warlock/src/app/users/controllers/list-users.controller.ts +24 -1
- package/templates/warlock/src/app/users/models/user/migrations/11-12-2025_23-58-03-user.migration.ts +1 -0
- package/templates/warlock/src/app/users/models/user/user.model.ts +5 -0
- package/templates/warlock/src/app/users/repositories/users.repository.ts +2 -2
- package/templates/warlock/src/app/users/resources/user.resource.ts +1 -1
- package/templates/warlock/src/app/users/schema/list-users.schema.ts +1 -1
- package/templates/warlock/src/config/http.ts +6 -0
- package/templates/warlock/src/web/home/components/contact-field.tsx +18 -0
- package/templates/warlock/src/web/home/components/contact-section.tsx +5 -29
- package/templates/warlock/src/web/home/components/contact-submit-button.tsx +16 -0
- package/templates/warlock/src/web/home/hooks/use-contact-field.ts +9 -0
- package/templates/warlock/src/web/home/hooks/use-contact-form.ts +5 -8
- package/templates/warlock/src/web/home/hooks/use-contact-locale.ts +34 -0
- package/templates/warlock/src/web/home/hooks/use-contact-submit-button.ts +6 -0
- package/templates/warlock/src/web/home/index.page.tsx +1 -1
- package/templates/warlock/src/web/home/types/contact-controls.type.ts +13 -0
- package/templates/warlock/src/web/shared/utils/set-form-errors.ts +3 -1
- 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-
|
|
7
|
+
## create-warlock `create-warlock/skills/SKILL.md`
|
|
8
8
|
|
|
9
9
|
---
|
|
10
|
-
name: create-
|
|
11
|
-
description:
|
|
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
|
|
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 —
|
|
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
|
-
|
|
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-
|
|
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.
|
|
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.
|
|
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-
|
|
3
|
-
description:
|
|
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
|
|
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 —
|
|
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
|
-
|
|
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": "
|
|
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": "
|
|
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 `
|
|
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 `
|
|
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
|
-
- `
|
|
79
|
-
- `
|
|
80
|
-
- `
|
|
81
|
-
- `
|
|
82
|
-
- `
|
|
83
|
-
- `
|
|
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
|
-
- `
|
|
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
|
|
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:
|
|
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.
|
|
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 {
|
|
50
|
+
import { Faq } from "../models/faq/faq.model";
|
|
51
51
|
|
|
52
|
-
export async function getFaqService(id:
|
|
53
|
-
const faq = await
|
|
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
|
-
|
|
64
|
-
|
|
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 (
|
|
83
|
-
|
|
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
|
-
|
|
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.
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
return response.badRequest({ error:
|
|
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
|
-
|
|
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
|
|
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=
|
|
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 `
|
|
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("
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
296
|
+
providerModelId: "string",
|
|
297
|
+
isFree: "boolean",
|
|
298
|
+
inputPrice: "number",
|
|
299
|
+
outputPrice: "number",
|
|
280
300
|
capabilities: "object",
|
|
281
301
|
features: "array",
|
|
282
|
-
|
|
283
|
-
|
|
302
|
+
createdBy: "string",
|
|
303
|
+
updatedBy: "string",
|
|
284
304
|
},
|
|
285
305
|
});
|
|
286
306
|
```
|
|
287
307
|
|
|
288
|
-
### 5.2 Field naming —
|
|
308
|
+
### 5.2 Field naming — camelCase everywhere
|
|
289
309
|
|
|
290
|
-
|
|
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
|
-
|
|
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. `
|
|
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).
|
|
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.
|
|
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
|
|
356
|
-
|
|
|
357
|
-
| Guarded (auth required) + body
|
|
358
|
-
| Public + body
|
|
359
|
-
| Read / list (no body)
|
|
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).
|
|
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
|
|
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
|
-
- [ ]
|
|
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
|