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>
2.8 KiB
name, description
| name | description |
|---|---|
| bff-endpoint | 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
-
Backend (
backend/src/BigRegister.Api/): minimal-API endpoint under/api/v1, wire DTO inContracts/, rule inDomain/with a test inbackend/tests/BigRegister.Tests/. Rejections are ProblemDetails (RFC 7807, 422). -
Regenerate the client:
npm run gen:api— commitsbackend/swagger.json+src/app/shared/infrastructure/api-client.ts(CI has a drift check; never edit the generated file). -
DTO in
<context>/contracts/<name>.dto.ts— plain interfaces/string-literal unions. Contracts import nothing (lint-enforced): no Angular, no aliases, no relative imports. -
Adapter in
<context>/infrastructure/<name>.adapter.ts— the only place HTTP lives:@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.
-
Application store exposes the resource as
RemoteData(fromResource, combine sources withmap/map2/andThenfrom@shared/application/remote-data). UI never imports infrastructure (lint-enforced). -
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|errortoggle.
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
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