docs(skills): extract house recipes as Claude Code skills for SSP templating
8 template-generic skills in .claude/skills/ (new-feature, new-context, value-object, form-machine, bff-endpoint, mutation-command, ui-component, new-ssp), condensed from CLAUDE.md/ARCHITECTURE/fp-tea/ADRs and pointing at this repo's worked examples. CLAUDE.md gains a pointer. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
57
.claude/skills/bff-endpoint/SKILL.md
Normal file
57
.claude/skills/bff-endpoint/SKILL.md
Normal file
@@ -0,0 +1,57 @@
|
||||
---
|
||||
name: bff-endpoint
|
||||
description: Add a screen-shaped read — backend endpoint returning a decision-enriched DTO, regenerated typed client, contracts DTO, adapter with parse boundary, application store, <app-async> in the page. Use for any new data a page needs.
|
||||
---
|
||||
|
||||
# BFF-lite read endpoint (decision DTO)
|
||||
|
||||
One screen = one endpoint returning everything that screen needs, **decisions
|
||||
included**. The FE renders the server's decisions; it never recomputes business
|
||||
rules (ADR-0001). Per rule choose: **decision flag** (server computes the boolean)
|
||||
or **config value** (server sends the threshold, FE applies it for instant feedback,
|
||||
server re-validates as authority).
|
||||
|
||||
## Steps
|
||||
|
||||
1. **Backend** (`backend/src/BigRegister.Api/`): minimal-API endpoint under `/api/v1`,
|
||||
wire DTO in `Contracts/`, rule in `Domain/` with a test in
|
||||
`backend/tests/BigRegister.Tests/`. Rejections are ProblemDetails (RFC 7807, 422).
|
||||
2. **Regenerate the client**: `npm run gen:api` — commits `backend/swagger.json` +
|
||||
`src/app/shared/infrastructure/api-client.ts` (CI has a drift check; never edit the generated file).
|
||||
3. **DTO** in `<context>/contracts/<name>.dto.ts` — plain interfaces/string-literal
|
||||
unions. Contracts import **nothing** (lint-enforced): no Angular, no aliases, no relative imports.
|
||||
4. **Adapter** in `<context>/infrastructure/<name>.adapter.ts` — the only place HTTP lives:
|
||||
|
||||
```ts
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class DashboardViewAdapter {
|
||||
private client = inject(ApiClient);
|
||||
viewResource() { return resource({ loader: () => this.client.dashboardView() }); }
|
||||
}
|
||||
// The trust boundary: validate the untrusted shape AND map DTO → domain.
|
||||
export function parseDashboardView(json: unknown): Result<string, DashboardView> { … }
|
||||
```
|
||||
|
||||
The mapping may read as an identity copy today — the point is the **types** differ,
|
||||
so the compiler forces a real mapping the moment the wire diverges.
|
||||
5. **Application store** exposes the resource as `RemoteData` (`fromResource`,
|
||||
combine sources with `map`/`map2`/`andThen` from `@shared/application/remote-data`).
|
||||
UI never imports infrastructure (lint-enforced).
|
||||
6. **Page** renders through `<app-async>` (`shared/ui/async`) — one of
|
||||
loading/empty/error/loaded by construction. Check all four states with the
|
||||
dev-only `?scenario=slow|loading|empty|error` toggle.
|
||||
|
||||
## Worked example (trace one name through all layers)
|
||||
|
||||
`dashboard-view`: `backend/src/BigRegister.Api` endpoint →
|
||||
`src/app/registratie/contracts/dashboard-view.dto.ts` →
|
||||
`src/app/registratie/infrastructure/dashboard-view.adapter.ts` (+ spec on `parseDashboardView`) →
|
||||
`src/app/registratie/application/big-profile.store.ts` → dashboard page.
|
||||
|
||||
## Verify
|
||||
|
||||
```bash
|
||||
cd backend && dotnet test && cd ..
|
||||
npm run gen:api && git diff --exit-code src/app/shared/infrastructure/api-client.ts
|
||||
npm test && npm run lint && npm run build
|
||||
```
|
||||
Reference in New Issue
Block a user