Files
atomic-design-poc/.claude/skills/bff-endpoint/SKILL.md
Edwin van den Houdt cf44bda0ce
Some checks failed
CI / frontend (push) Successful in 2m16s
CI / storybook-a11y (push) Successful in 4m6s
CI / backend (push) Failing after 48s
CI / api-client-drift (push) Successful in 1m32s
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>
2026-07-03 10:10:38 +02:00

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

  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:

    @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.tssrc/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