import { Meta } from '@storybook/addon-docs/blocks'; # Learning path A paced, hands-on route through this codebase for a developer who is a **strong programmer but new to frontend functional programming**. The [Overview](?path=/docs/foundations-overview--docs) is the _map_ — every idea, cross-linked. This is the _route_: what to read first, what to **do** to make it stick, and how to know you understood it. Work through it over roughly three days. Each lesson has the same shape: - **Goal** — one sentence: what you'll be able to do. - **~time** — a rough budget so a day stays a day. - a few paragraphs that **teach the idea** (self-contained — you can read straight through), then - **Do** — a hands-on exercise. Most reuse the repo's invocable skills (`/new-feature`, `/form-machine`, …), which scaffold real code the house way. - **Check yourself** — a question; if you can answer it, move on. - **Go deeper** — the deep-dive page and the long-form source in `docs/reference/`. The one idea underneath everything: **make illegal states unrepresentable.** Every rule below is a way to stop the compiler letting you build a state that can't actually happen. --- ## Day 1 — Orient: the shape of the codebase ### 1.1 Why this exists — state that can lie · ~15 min **Goal:** name the failure mode this whole architecture is designed to prevent. Most UI bugs are not wrong algorithms — they're **impossible states that the types allowed anyway**. `isLoading` true _and_ `error` set _and_ `data` present: three booleans give eight combinations, but only four are real. The extra four are bugs waiting to be rendered. The reflex this codebase trains: when you reach for a second or third boolean to track one thing, model a **discriminated union** instead, so the illegal combinations can't be typed. The second big idea is structural. The folder layout is not filing — **the folder structure _is_ the architecture**. Where a file lives declares what it's allowed to depend on, and that rule is enforced by lint, not hoped for. You'll meet the same "compose small honest pieces, forbid the illegal combinations" principle at three scales today and tomorrow: in the domain model, in the component tree, and in state. **Do:** open `src/app/` and read the top of `CLAUDE.md` ("The decisions"). Just get the lay of the land — six contexts, five layers. **Check yourself:** three booleans model how many states, and how many are real for a "fetch"? Why is that gap the enemy? **Go deeper:** `docs/reference/fp-tea-atomic-design.md` Part 1. ### 1.2 Domain-driven design: contexts then layers · ~25 min **Goal:** predict which imports are legal before the linter tells you. Code is organised first by **bounded context** — a business capability with its own language: `shared`, `auth`, `registratie`, `herregistratie`, `brief`, `showcase`. Inside each context are five **layers**, and dependencies only ever point **inward**: | Layer | Job | Angular? | | ----------------- | ----------------------------------------- | -------------------------------- | | `domain/` | business rules + data types | **No — pure TS**, has `.spec.ts` | | `application/` | coordinate state/tasks (stores, commands) | yes (signals) | | `infrastructure/` | where data comes from (HTTP) | yes | | `contracts/` | wire DTOs (the FE⇄BE seam) | no | | `ui/` | how it looks | yes | `ui → application → domain`, never the reverse; `ui` never touches `infrastructure` directly. Cross-context is one-directional too: `herregistratie → registratie → shared`, `auth → shared`, `brief → shared`. A context downstream may lean on one upstream; the upstream never learns the downstream exists. This keeps the domain pure and testable and stops the dependency graph rotting into a ball of mud. **Do:** open `eslint.config.mjs` and find the import-boundary rules. Then pick any file in `herregistratie/` and trace one import back into `registratie` or `shared`. **Check yourself:** why may `herregistratie` import from `registratie`, but `registratie` may **not** import from `herregistratie`? What breaks if you invert it? **Go deeper:** [Domain-driven design](?path=/docs/foundations-domain-driven-design--docs); `docs/reference/architecture/ARCHITECTURE.md` §1. ### 1.3 Atomic design: composition is the default · ~20 min **Goal:** decide, for a new screen, whether to add a building block or just compose. The design system is a ladder: **Atoms → Molecules → Organisms → Templates**, each level built only from the level below. Atoms (`button`, `form-field`) are thin typed wrappers over CIBG Huisstijl CSS classes; molecules compose atoms; organisms compose molecules; templates lay out organisms; a context's `ui/` page composes templates. A new page should be **composition of existing blocks** — adding a block is the exception, not the reflex. Notice this is the same shape as 1.2: small honest pieces, each only allowed to reach one level down, illegal combinations forbidden by structure. That's not a coincidence — you'll see why tomorrow. **Do:** trace a real composition chain in Storybook: **Atoms → Button**, then find where it's used up through `form-field → document-upload → page-shell`. Watch each level only reach one level down. **Check yourself:** you need a new "application summary" screen. What's the first question you ask before writing a component? **Go deeper:** [Atomic design](?path=/docs/foundations-atomic-design--docs). Adding a block (only when composition truly can't do it): the `/ui-component` skill. --- ## Day 2 — The functional core ### 2.1 FP fundamentals · ~25 min **Goal:** read code as "functional core, imperative shell" and spot which is which. Four tools do the heavy lifting. **Pure functions:** output depends only on input, no side effects — trivially testable, no mocks. **Immutability:** you compute new values, you don't mutate old ones, so nothing changes under you. **Unidirectional flow:** data moves one way (state → view → message → new state), never a tangle of two-way bindings. **Sum and product types:** a _product_ is "A and B" (a record); a _sum_ is "A **or** B" (a discriminated union) — sums are how you make illegal states unrepresentable. Put together: the **functional core** is pure logic (all of `domain/`, the reducers, the parsers) that knows nothing about Angular or HTTP; the **imperative shell** (components, adapters) does the messy I/O and hands data in and out of the core. Bugs hide in the shell; the core stays provable. **Do:** open any `domain/` file next to its `.spec.ts` and confirm the spec uses no Angular `TestBed` — it calls the function directly. That's the core being pure. **Check yourself:** which of these is a sum type and why — "a form field's value" vs. "a form's submission state (idle / submitting / failed / done)"? **Go deeper:** [FP in the UI](?path=/docs/foundations-fp-in-the-ui--docs); `docs/reference/fp-tea-atomic-design.md` Part 2. ### 2.2 State machines — The Elm Architecture · ~30 min **Goal:** model a form as `Model → Msg → reduce`, with effects kept out of the reducer. Every form and wizard here is one state machine: a **Model** (a tagged union — the current state), a **Msg** union (everything that can happen), and a **pure** `reduce(model, msg): model`. The template never mutates state; it **dispatches a message**, `reduce` returns the next model, the view re-renders. All wiring goes through one idiom, `createStore(initial, reduce)` — you never hand-roll `signal(model)` + a local dispatch. The rule that keeps `reduce` pure: **side effects live in commands, not the reducer.** A command (`application/submit-*.ts`) does the HTTP, then dispatches a message describing the _outcome_. Reducer = "what the new state is"; command = "go do it, then say what happened." And **derive, don't store** anything you can compute — e.g. a wizard's visible steps are `visibleSteps(answers)`, not a stored field. A field's value lands in the Model on **every keystroke** (not on blur — blur only marks the field "touched"); a separate 600 ms debounce off the model snapshot autosaves the draft to the backend, an effect that lives _outside_ the reducer. See `docs/reference/architecture/ARCHITECTURE.md` §2g. **Do:** run `/form-machine` for a toy single field (say a "nickname" field with a max length). Read the generated Model / Msg / reduce and its spec. **Check yourself:** why can't `reduce` make the HTTP call itself? What goes wrong if it does? **Go deeper:** [State machines (TEA)](?path=/docs/foundations-state-machines-tea--docs); `docs/reference/fp-tea-atomic-design.md` Parts 3–4. ### 2.3 RemoteData & async · ~20 min **Goal:** replace loading/error/empty booleans with one four-state value. `RemoteData` is a sum type with exactly four cases: `Loading | Empty | Failure{error} | Success{value}`. That's the four _real_ states from lesson 1.1, and no others — you literally cannot construct "loading and error." Combine sources with `map` / `map2` / `andThen` (precedence: Failure > Loading > Empty > Success), and render it with the `` molecule, which picks one of four mutually-exclusive templates by construction. The default spinner is delay-gated (~250 ms) so fast connections don't flash. **Do:** open a data page in the running app with `?scenario=slow`, then `?scenario=empty`, then `?scenario=error` (the dev-only scenario toggle). Watch `` switch templates without any `*ngIf` soup. **Check yourself:** a page combines two independent fetches with `map2`. One is still loading, the other has failed — what does the combined value show, and why that precedence? **Go deeper:** [RemoteData & Async](?path=/docs/foundations-remotedata-async--docs); `docs/reference/architecture/ARCHITECTURE.md` §2. ### 2.4 Parse, don't validate · ~20 min **Goal:** turn untrusted input into a domain type once, then trust it forever. Raw input (`unknown`, a string, a wire DTO) becomes a **branded value object** only by passing through a **parser** that returns `Result` — `parsePostcode`, `parseUren`, `parseBigNummer`. Once you hold a `Postcode`, its shape is guaranteed by the type system; you **never re-check it**. This happens in two places: value objects (form fields) and boundary `parse*` adapters in `infrastructure/` (the FE⇄BE seam, where untrusted JSON becomes domain types). "Validate" scatters `if`-checks everywhere and forgets one; "parse" concentrates the check at the door and lets the compiler enforce the rest. **Why "brand"?** TypeScript is _structurally_ typed, so a bare `type Postcode = string` would accept any string and lose all proof of validation. Intersecting a phantom marker — `string & { readonly __brand: 'Postcode' }` — makes the type **nominal**: no plain string satisfies it, so the only way to hold a `Postcode` is to go through the parser that stamps the brand. The brand is compile-time proof the value was validated (it exists only in the types, never at runtime). The DDD name for the concept is a _value object_; "brand" is just the TypeScript trick that makes it enforceable. **Do:** run `/value-object` for a small field (e.g. a Dutch phone number). Read the parser and its spec — note it returns `Result`, not a boolean, and note the branded type. **Check yourself:** you're three functions deep and you hold a `Postcode`. Should you re-validate its format? Why not? **Go deeper:** [Parse, don't validate](?path=/docs/foundations-parse-dont-validate--docs); `docs/reference/architecture/ARCHITECTURE.md` §3. ### Day 2 closer — one principle, two scales You've now seen it twice: **small honest pieces, each only allowed to reach one level down, with illegal combinations forbidden by structure.** Atomic design applies it to _components_ (atoms compose upward); The Elm Architecture applies it to _state_ (pure `reduce` composes messages into models). They are the same principle at two scales — that is the thesis of this codebase. Read `docs/reference/fp-tea-atomic-design.md` Part 5; it's the "aha" that ties Day 1 and Day 2 together. --- ## Day 3 — Quality & shipping ### 3.1 Testing strategy — what to test, by layer · ~20 min **Goal:** know where a test goes and what kind it is, given any change. Test grain follows the layer. **Domain and pure logic must have a spec** — reducers, combinators, `visibleSteps`, parsers, boundary `parse*` adapters — tested **directly, no TestBed**, because they're pure. **UI is exercised via Storybook stories** (co-located `*.stories.ts`, a11y addon on), not heavy component tests. Backend rules have their own `dotnet test`. The GREEN gate before you push: `npm run lint && npm test && npm run build` (plus `cd backend && dotnet test`). **Do:** run `/test-strategy` and read where it says each layer's test belongs. Then run `npm test` and watch the pure specs fly (no browser, no mocks). **Check yourself:** you add a new parser and a new page. Which gets a `.spec.ts`, and which gets a Storybook story instead? **Go deeper:** [Testing strategy](?path=/docs/foundations-testing-strategy--docs); `CLAUDE.md` §5. ### 3.2 BDD — one behaviour per test · ~15 min **Goal:** write test names that read as a specification in the domain's language. `describe` names the subject; each `it` states **one observable behaviour** in present tense — no `should`, no Given/When/Then ceremony. One behaviour per test means one _behaviour_, not one `expect`: assertions pinning down the same behaviour stay together (a `Result`'s `.ok` then its `.value`); assertions about different behaviours split apart (the ok branch **and** the err branch). If a title needs "and"/"then"/"/" to join two things, that's the smell — split it. And speak the **ubiquitous language**: a _behandelaar_ drafts, a _beoordelaar_ approves — the same words as the bounded contexts. **Do:** read `registratie/domain/registratie-wizard.machine.spec.ts` — one transition per test, each named as a behaviour. (You saw this style get enforced when the specs were recently split.) **Check yourself:** `it('submits and then shows the reference')` — what's wrong with this name? **Go deeper:** [BDD](?path=/docs/foundations-bdd--docs). ### 3.3 Accessibility — four layered tools · ~15 min **Goal:** know which a11y bug each tool catches, and what only a human catches. Four layers, each a different bug class: **axe on every story** (CI-gated, catches contrast/roles/labels), **template a11y lint** (catches missing alt/labels at author time), **Storybook play tests** (catches keyboard/focus interaction), and a **manual WCAG checklist** for what automation can't — tab order across a page, focus traps, 200% zoom, screen-reader narration. a11y is a build gate here, not a nice-to-have. **Do:** open any story and check the **Accessibility** tab (axe results). Then skim `docs/reference/wcag-checklist.md` — note the honest empty "Screen reader" column: some things only a human pass finds. **Check yourself:** axe passes on a form. Name one real a11y bug it still can't catch. **Go deeper:** [Accessibility](?path=/docs/foundations-accessibility--docs). ### 3.4 Internationalization — the locale seam · ~15 min **Goal:** wrap user-facing copy so a second language is a translation file, not a code change. Every user-visible string is wrapped in Angular's first-party `$localize` with a stable custom id — `` $localize`:@@context.key:Tekst` ``. Source locale is `nl`; English is a translation file, not edited code — that's the seam. Shared/English components must **not** hardcode Dutch: they expose copy as `input()`s with localizable defaults, and the Dutch domain caller supplies the text. **Do:** grep for `$localize` in a `ui/` component; note the `@@`-prefixed stable ids. Find one `shared/ui` component that takes copy as an `input()` rather than hardcoding it. **Check yourself:** why must a shared English atom take its label as an `input()` instead of writing the Dutch word directly? **Go deeper:** [Internationalization](?path=/docs/foundations-internationalization--docs). ### 3.5 The design-system track (parallel) · ~15 min **Goal:** style via semantic tokens and the CIBG Huisstijl, never hand-written colours. This strand is largely independent of the FP/state spine — learn it whenever. The app speaks a semantic `--rhc-*` token vocabulary; `src/styles.scss` is a **token bridge** that maps those onto the vendored **CIBG Huisstijl** (a customized Bootstrap 5.2) `--bs-*` values. Rule: reach for a **CIBG class first, then a token** — no hand-written hex. Where CIBG lacks a class (e.g. `alert`), the atom is hand-rolled from tokens and recorded in the **CIBG gap register** so the divergence stays honest. **Do:** open [Design tokens](?path=/docs/foundations-design-tokens--docs) and read the live swatches; then skim the [CIBG gap register](?path=/docs/foundations-cibg-gap-register--docs). **Check yourself:** you need a warning colour. Where does it come from, and where does it **not**? **Go deeper:** `docs/reference/architecture/0003-cibg-huisstijl.md` (ADR-0003). --- ## Capstone — add a feature end-to-end **Goal:** ship one small vertical slice the house way, and name which layer owns each rule. Two framing ideas first. **BFF-lite + decision DTOs (ADR-0001):** each screen gets one screen-shaped endpoint returning a **decision-enriched** DTO — the backend computes the business rules, and **the FE renders decisions, it does not recompute them.** Per rule you pick a _decision flag_ (server sends the boolean) or a _config value_ (server sends the threshold, FE applies it for instant feedback, server re-validates as authority). The FE keeps only **format** validation, never as authority. Then the house pipeline, always in this order: **domain** (types + pure rules + spec, no Angular) → **infrastructure** (adapter with a `parse*` boundary, or a command returning `Result`) → **application** (a store if state is shared; union + pure `reduce`) → **ui** last (compose `shared/ui` atoms, wrap async in ``, dispatch messages). **Do:** build a tiny slice — e.g. a one-field "update phone number" action — using the skills in pipeline order: 1. `/value-object` — the field's parser + branded type (domain). 2. `/bff-endpoint` — a screen-shaped read with a decision DTO + `parse*` boundary. 3. `/form-machine` — the form's Model/Msg/reduce. 4. `/mutation-command` — the write, returning `Result`, keeping the reducer pure. 5. `/ui-component` **only if** no existing block composes — otherwise just compose. `/new-feature` walks the whole pipeline if you'd rather do it in one guided pass. **Check yourself:** for your slice, name for each business rule whether it's a _decision flag_ or a _config value_, and which layer owns it. If a rule lives in two layers, which one is the **authority**? **Go deeper:** `docs/reference/architecture/0001-bff-lite-decision-dtos.md`; `docs/reference/fp-tea-atomic-design.md` Part 7 (the copy-paste recipes); `docs/reference/architecture/ARCHITECTURE.md` §4 (the recipe) and §6a (the full FE⇄BE request lifecycle, read + write, with file links). For how contexts scale to a second app and actor-based authorization, ADR-0002 (the advanced read). --- You've done the route. From here the [Overview](?path=/docs/foundations-overview--docs) map is your reference, the deep-dive pages hold the detail, and the skills scaffold each new piece the house way.