Agile System SoftwareLearn

Workshop · Angular · Advanced

NG300 Angular, advanced

The front end as a production system. Eight sessions and forty-eight steps on architecture (feature folders, smart and presentational components, environment configuration with no secrets in the bundle), async state with RxJS and signals, a typed HTTP layer over a local API you write in TypeScript against the seat's PostgreSQL, routing at scale with lazy features, resolvers and route-level providers, testing at scale with fakes and a coverage gate, performance with OnPush, @defer and bundle budgets, and the review skills of the AI era: reading unfamiliar code, finding planted defects and verifying generated code before it ships. It ends in Orders, an order-processing front end whose validation, pricing, reports and rejection reasons pass hidden tests against data you never saw.

Open Coding Tutor at codingtutor.agilesystemsoftware.com, pick NG300 from the course button in the chat and press Tutor me. Every lesson, demo and hands-on step is also published below.

  • LevelAdvanced
  • Durationself-paced ~40–50 h
  • Size8 sessions · 48 mini steps · ~443 min of lessons
  • EnvironmentA VS Code seat in your browser with Node 22, TypeScript 6, Angular 22, Vitest with jsdom and coverage, ESLint, tsx, the pg driver and PostgreSQL 17 running inside the seat, plus the App Preview; all offline, nothing to install
  • PrerequisitesNG100 Angular I, or equivalent professional Angular experience; take the placement step 8.6 to test out.
    Courses: NG100
  • CertificateAgile System Software Certificate — Angular, advanced (NG300)

Who it is for

Developers who have built and tested an Angular 22 application with signals, services, routing and typed forms (NG100 or equivalent work) and now own front ends that talk to real APIs, grow across teams and must stay fast. You are comfortable with TypeScript generics, Promises and the command line.

Bridge. Coming from NG100: the concepts you already proved there are credited here (6 of them), and the first session re-teaches the syntax.

What you will be able to do

  • Structure a front end into features with clear boundaries, smart and presentational components and environment config without secrets
  • Model async state with RxJS operators, toSignal/toObservable and resource(), with cancellation and loading/error states
  • Build a typed HTTP layer with HttpClient, interceptors and DTO mapping over an API you control
  • Write a small TypeScript API with Node http and pg against PostgreSQL, with parameterised statements
  • Route at scale: lazy features, resolvers, guards, route-level providers and deep links
  • Test at scale with fakes, API tests, component harness-style tests and a coverage gate
  • Measure and improve performance: OnPush, signals, @defer, budgets and the production build
  • Read unfamiliar code, find defects by review, and verify machine-generated code before merging
  • Keep money in integer cents through pricing rules and reports, with exact totals

Syllabus

Every session ends in a lab the machine checks on your own code. Open a session's mini steps to read each lesson, demo and hands-on task; the badge shows how far the engine can verify it: checked · T3 a check runs over your files, proven · T4 the grader also tries inputs you did not see, tutored · T0 done on your word. Each step has its own link (for example #s1-1).

Session 1 · Architecture of a front end

Six steps on shape before features. Feature folders with a public index; smart (container) versus presentational components and the one-way data rule; the domain layer as pure TypeScript with no Angular import; environment configuration through fileReplacements and an injection token, and why no secret ever belongs in a bundle; dependency direction enforced by an ESLint boundary rule; the architecture decision record. Live in the seat: a feature moved behind an index, a presentational component that compiles with zero injections, a build that swaps environment files, a lint rule rejecting an import across a boundary, and a grep of dist for a planted "secret" string.

Lab Lay out the Orders shell: features/orders with public index, a pure domain folder, an APP_CONFIG token fed from environment files, an ESLint boundary rule, and ADR-0001 explaining the shape. Session 1 checkpoint.

6 mini steps · ~54 min
  1. 1.1Feature folders and public indexes9 minchecked · T3

    Predict

    features/orders/index.ts exports OrdersPage only. Another feature imports features/orders/internal/row.ts. What should happen?

    Lesson

    src/app/features/<name>/ with index.ts as the only public surface; src/app/core for singletons; src/app/shared for dumb reusable pieces; src/app/domain for pure TypeScript. A folder is a boundary only if something enforces it.

    Demo

    The tutor reshapes node/ng300/demo/orders (supplied) into features/orders, core, shared and domain, adds index.ts files, and shows imports going through them.

    Hands-on

    In node/ng300/orders (supplied skeleton) create features/orders/index.ts, core/, shared/ and domain/ folders with index.ts each; move the supplied OrdersPage under features/orders and export it from the index.

    Explain

    What makes a folder a real boundary?

    Check

    Feature, core, shared and domain folders with indexes — checked (T3): seat_build seat_file seat_file seat_file

    #s1-1

  2. 1.2Smart and presentational components9 minchecked · T3

    Predict

    OrderRow injects OrdersStore to call remove(). Is it presentational?

    Lesson

    Containers (pages) inject services and own state; presentational components take input() and emit output(), inject nothing, and are OnPush by default. The rule is checkable: no inject() in shared/ or in *-row, *-card files.

    Demo

    The tutor splits the demo OrdersPage into a container and an OrderRow with inputs and an output, sets OnPush on the row, and writes a 5-line row test with no TestBed providers.

    Hands-on

    Create shared/order-row.ts: OnPush, order = input.required<Order>(), select = output<number>(), no inject(). Make OrdersPage render rows from a store and handle (select). Add a row spec with no providers.

    Explain

    What does a presentational component buy a team?

    Check

    OrderRow is OnPush, input/output only, tested — checked (T3): seat_build seat_file seat_test

    #s1-2

  3. 1.3The domain layer: pure TypeScript9 minchecked · T3

    Predict

    domain/pricing.ts imports { signal } from @angular/core to cache a result. Acceptable?

    Lesson

    domain/ holds types (Order, OrderLine, Money as integer cents) and pure functions (lineTotal, orderTotal, applyDiscount). No import from @angular/*. Tests run as plain Vitest. The same code can be shared with the API in session 3.

    Demo

    The tutor writes domain/order.ts and domain/pricing.ts in the demo with cents arithmetic and three tests, runs them, and shows an ESLint no-restricted-imports rule for @angular in domain/.

    Hands-on

    Write domain/order.ts (Order, OrderLine with qty and unitCents, status union) and domain/pricing.ts (lineTotal, orderTotal, applyPercentDiscount rounding half up at the end) with pricing.spec.ts of at least four cases.

    Explain

    Why round once, at the end, in cents?

    Check

    Pure domain pricing with tests and no Angular import — checked (T3): seat_file seat_test craft

    #s1-3

  4. 1.4Environment configuration without secrets9 minchecked · T3

    Predict

    environment.prod.ts contains apiKey: "sk_live_…". After ng build, where is the key?

    Lesson

    environments/environment.ts and environment.prod.ts hold public config (apiBaseUrl, featureFlags); angular.json fileReplacements swap them per configuration; an InjectionToken APP_CONFIG provides them. Never a secret: the bundle is public.

    Demo

    The tutor adds environments and fileReplacements in the demo, provides APP_CONFIG, plants a fake "SECRET_DO_NOT_SHIP" string, builds, greps dist and finds it, then removes it.

    Hands-on

    Add environment.ts and environment.prod.ts with apiBaseUrl; wire fileReplacements for the production configuration; create core/app-config.ts with an InjectionToken<AppConfig> provided in app.config.ts. No key-like strings.

    Explain

    What may go into a front-end environment file?

    Check

    APP_CONFIG token fed from environment files, no secret-like strings — checked (T3): seat_build seat_file seat_file seat_file

    #s1-4

  5. 1.5Enforcing dependency direction with ESLint8 minchecked · T3

    Predict

    A no-restricted-imports rule bans "@angular/*" inside src/app/domain/**. A developer adds import { inject } there. Result?

    Lesson

    eslint.config.js with per-folder overrides: files ['src/app/domain/**'] → no-restricted-imports for @angular/* and rxjs; files ['src/app/shared/**'] → forbid imports from features/**; a rule against deep imports past an index. Architecture as a test.

    Demo

    The tutor adds the overrides to the demo config, writes a forbidden import in domain/, runs eslint and reads the failure, then reverts.

    Hands-on

    Extend eslint.config.js with the two folder overrides described (domain: no @angular or rxjs; shared: no features imports) and a no-restricted-imports pattern for '**/features/*/internal/**'. eslint src must pass.

    Explain

    Why encode architecture in lint rather than a document?

    Check

    Boundary lint rules present and passing — checked (T3): seat_file seat_lint

    #s1-5

  6. 1.6Lab: the architected shell and ADR-000110 minchecked · T3

    Predict

    What does an ADR record that a README usually does not?

    Lesson

    The shell complete: folders with indexes, OnPush presentational rows, a pure domain with tests, APP_CONFIG from environments, boundary lint. docs/adr/0001-shape.md records context, decision, alternatives and consequences.

    Demo

    The tutor writes ADR-0001 for the demo in ten lines, runs lint, build, test and a dist grep for secret-like strings as the session's four-tool rhythm, and shows the check reading each.

    Hands-on

    Write docs/adr/0001-shape.md with Context, Decision, Alternatives, Consequences sections. Confirm lint, build and tests pass and that dist contains no string matching sk_live or SECRET.

    Explain

    Which alternative did you reject in ADR-0001 and why?

    Check

    Shell builds, lints, tests; ADR-0001 present; no secrets in dist — checked (T3): seat_file seat_lint seat_build seat_test seat_file

    #s1-6

Session 2 · Async and state

Six steps on time. The RxJS operators that matter in 2026 (map, switchMap, debounceTime, distinctUntilChanged, catchError, takeUntilDestroyed) and the ones to stop using; toSignal and toObservable as bridges; resource() and rxResource() for request-shaped state with loading, error and value; cancellation with switchMap versus the race of mergeMap; optimistic updates and rollback; a state machine for a multi-step flow. Live in the seat: a typeahead that cancels stale requests, a resource exposing status to the template, an optimistic remove rolled back on failure, and a checkout flow whose impossible states cannot be represented.

Lab An order search with debounced, cancellable lookups via rxResource, explicit loading/error/empty states, an optimistic status change with rollback, and tests using fakeAsync and a fake API. Session 2 checkpoint.

6 mini steps · ~55 min
  1. 2.1The operators that matter9 minchecked · T3

    Predict

    search$.pipe(mergeMap(q => api.find(q))). The user types a, ab, abc; responses return abc, a, ab in that order. Final shown result?

    Lesson

    Keep: map, filter, switchMap, debounceTime, distinctUntilChanged, catchError, startWith, takeUntilDestroyed. Prefer signals for state and async pipe or toSignal for consumption. switchMap cancels; mergeMap races; concatMap queues; exhaustMap ignores.

    Demo

    The tutor builds a FakeApi with delays in the demo, wires a search with mergeMap, shows the race in the console, swaps to switchMap and shows the stale response discarded.

    Hands-on

    In features/orders create order-search.ts with a Subject<string>, a pipe of debounceTime(250), distinctUntilChanged, switchMap to a FakeOrdersApi.find(q) (supplied in samples/s2), catchError returning []. Expose results via toSignal.

    Explain

    When would exhaustMap be right?

    Check

    Debounced switchMap search with catchError and toSignal — checked (T3): seat_build seat_file

    #s2-1

  2. 2.2toSignal, toObservable and lifetimes8 minchecked · T3

    Predict

    toSignal(obs$) is called inside a method on a button click. What is wrong?

    Lesson

    toSignal(obs$, { initialValue }) subscribes once and unsubscribes with the component; toObservable(sig) emits when the signal changes; both need an injection context (field initialisers, constructors, or an explicit injector). takeUntilDestroyed for manual subscriptions.

    Demo

    The tutor moves a toSignal call into a method in the demo, shows the runtime error, fixes it with a field initialiser, then converts the query signal to an observable with toObservable to feed the pipe.

    Hands-on

    Refactor order-search.ts: query = signal(''), the pipe starts from toObservable(this.query), results = toSignal(...) as a field. Remove the Subject. The template sets query via (input).

    Explain

    Why does the injection context matter for toSignal?

    Check

    Signal-first search bridged with toObservable and toSignal — checked (T3): seat_build seat_file

    #s2-2

  3. 2.3resource() and rxResource()10 minchecked · T3

    Predict

    orders = rxResource({ params: () => query(), stream: ({ params }) => api.find(params) }). query changes while a request is in flight. What happens to it?

    Lesson

    resource({ params, loader }) and rxResource({ params, stream }) model "a value that depends on params and arrives later": value(), status(), error(), isLoading(), reload(). The template switches on status; stale loads are cancelled.

    Demo

    The tutor replaces the demo search pipe with rxResource over the fake API, renders @switch on status() with loading, error and empty branches, and reloads on a button.

    Hands-on

    Replace results with orders = rxResource({ params: () => this.query(), stream: ({ params }) => this.api.find(params) }). Render @if (orders.isLoading()), @else if (orders.error()), @else with an @empty branch, plus a Retry button calling reload().

    Explain

    Which states can your template show now, and which could it not before?

    Check

    rxResource with loading, error, empty and reload — checked (T3): seat_build seat_file seat_file

    #s2-3

  4. 2.4Optimistic updates and rollback9 minchecked · T3

    Predict

    You set status to "shipped" locally, call the API, it fails. What must the UI do?

    Lesson

    Optimistic: snapshot the previous value, apply the change to the signal, fire the request; on error restore the snapshot and surface a message. Wrap it in one store method so every caller gets the same discipline.

    Demo

    The tutor adds setStatus(id, status) to the demo OrdersStore with snapshot and rollback, makes the fake API fail every third call, and shows the row flipping back with a toast-like message.

    Hands-on

    Create features/orders/orders-store.ts with orders signal and setStatus(id, status): optimistic update, await api.updateStatus, rollback on failure, lastError signal. Wire a status select on OrderRow via output.

    Explain

    What makes rollback correct even if two updates overlap?

    Check

    Optimistic setStatus with rollback and lastError — checked (T3): seat_build seat_file

    #s2-4

  5. 2.5A state machine for a flow9 minchecked · T3

    Predict

    type Checkout = { step: 'cart' } | { step: 'address'; address: Address } | { step: 'done'; orderId: number }. Can step be 'done' with no orderId?

    Lesson

    Model multi-step flows as a discriminated union in a signal and transitions as pure functions next(state, event): state. Impossible combinations cannot be built; @switch on step renders the right view; transitions are unit-tested without a DOM.

    Demo

    The tutor writes domain/checkout.ts in the demo: the union, the events, next(), eight tests on transitions including an invalid event that leaves state unchanged; wires @switch in a CheckoutPage.

    Hands-on

    Write domain/checkout.ts with the Checkout union (cart, address, review, done), an Event union, and next(state, event). Add checkout.spec.ts with ≥6 transition tests. Render with @switch in features/checkout/checkout-page.ts.

    Explain

    What bug class disappears with this model?

    Check

    Checkout state machine as a union with tested transitions — checked (T3): seat_file seat_test seat_file

    #s2-5

  6. 2.6Lab: cancellable search with explicit states, tested10 minchecked · T3

    Predict

    A fakeAsync test types "a" then "ab" within 100 ms with a 250 ms debounce. How many fake API calls after tick(250)?

    Lesson

    The search page complete: debounced query, rxResource with cancellation, four rendered states, optimistic status with rollback. Tests: fakeAsync for debounce, a controllable fake API (resolve/reject on demand) for loading/error/rollback.

    Demo

    The tutor writes a ControllableFakeApi in the demo spec, tests loading then value, error then retry, and rollback on a rejected status update; runs ng test and coverage.

    Hands-on

    Write order-search.spec.ts with tests "debounces to one request", "shows loading then results", "shows error and retries", and orders-store.spec.ts "rolls back a failed status change". Lint and build clean.

    Explain

    Why a controllable fake rather than a fixed delay?

    Check

    Search and store tests pass with fakes — checked (T3): seat_test seat_lint seat_build

    #s2-6

Session 3 · Talking to an API

Six steps across the wire. A local API in TypeScript with Node's http module and the pg driver against the seat's PostgreSQL, run with tsx and sharing the domain types; HttpClient with provideHttpClient and typed requests; DTOs mapped to domain types at one boundary; functional interceptors for base URL, correlation ids and error normalisation; retry with backoff for idempotent reads only; the proxy configuration for ng serve. Live in the seat: a parameterised SQL query refusing an injection string, a DTO with snake_case mapped to camelCase, an interceptor stamping every request, and a 500 turned into a typed ApiError the UI can show.

Lab OrdersApi over the local API: typed list/get/updateStatus with DTO mapping, three interceptors, retry on reads only, API tests with Vitest against the running server, and a fake used by component tests. Session 3 checkpoint.

6 mini steps · ~55 min
  1. 3.1A local API with Node http and pg10 minchecked · T3

    Predict

    client.query(SELECT * FROM orders WHERE customer = '${name}') versus client.query('SELECT * FROM orders WHERE customer = $1', [name]). Which is safe?

    Lesson

    server/api.ts: http.createServer with a tiny router (method + path), JSON bodies, pg.Pool from the seat's connection settings, parameterised queries only, and the domain types imported from src/app/domain. Run with tsx server/api.ts.

    Demo

    The tutor writes the demo API with GET /orders, GET /orders/:id and PATCH /orders/:id/status against a supplied schema (samples/s3/schema.sql), runs it with tsx, and curls it from the seat terminal; tries an injection string and shows it treated as data.

    Hands-on

    Create server/api.ts in orders with GET /orders, GET /orders/:id, PATCH /orders/:id/status using pg.Pool and $1 parameters, loading samples/s3/schema.sql once. Start it with tsx and call it from the terminal.

    Explain

    Why share domain types between the API and the front end?

    Check

    API routes with parameterised pg queries — checked (T3): seat_file seat_build craft

    #s3-1

  2. 3.2HttpClient and typed requests9 minchecked · T3

    Predict

    http.get<Order[]>(url) — does the generic validate the response at runtime?

    Lesson

    provideHttpClient() in app.config.ts; a core/orders-api.ts service with inject(HttpClient) and inject(APP_CONFIG); get<T>, patch<T>; convert to Promise or signal at the edge with firstValueFrom or rxResource. Types on the wire are a hope until mapped.

    Demo

    The tutor adds OrdersApi to the demo with list(), get(id), updateStatus(id, status) typed against DTOs, wires the base URL from APP_CONFIG, and shows the network tab in the App Preview.

    Hands-on

    Add provideHttpClient() and core/orders-api.ts with list(): Observable<OrderDto[]>, get(id), updateStatus(id, status) using APP_CONFIG.apiBaseUrl. Replace the FakeOrdersApi in the search with it.

    Explain

    Where does the base URL come from and why there?

    Check

    OrdersApi over HttpClient with APP_CONFIG base URL — checked (T3): seat_build seat_file seat_file

    #s3-2

  3. 3.3DTOs mapped at one boundary9 minchecked · T3

    Predict

    The API returns unit_cents as a string "349" (numeric column). The mapper does unitCents: Number(dto.unit_cents). Why map rather than cast?

    Lesson

    core/dto.ts defines OrderDto (wire shape, snake_case, strings where the database says so); core/mappers.ts has toOrder(dto): Order with conversion and validation, tested with fixtures. Nothing outside core sees a DTO.

    Demo

    The tutor writes OrderDto and toOrder in the demo, a fixture from a real response, two mapper tests including a malformed row that throws a typed error, and the lint rule forbidding dto imports outside core.

    Hands-on

    Write core/dto.ts (OrderDto, OrderLineDto) and core/mappers.ts (toOrder, toOrderLine) with Number() conversion and a status guard; mappers.spec.ts with 3 tests. OrdersApi returns Observable<Order[]> by mapping.

    Explain

    What breaks if a DTO leaks into a component?

    Check

    DTOs mapped to domain with tested mappers — checked (T3): seat_file seat_file seat_test seat_file

    #s3-3

  4. 3.4Functional interceptors9 minchecked · T3

    Predict

    withInterceptors([baseUrl, correlationId, errorMap]). A request fails with 500. Which interceptor sees the error first?

    Lesson

    provideHttpClient(withInterceptors([...])) with HttpInterceptorFn: prefix relative URLs with the base; add an X-Correlation-Id header; map HttpErrorResponse into a typed ApiError { status, code, message, correlationId } with catchError. Order matters.

    Demo

    The tutor writes the three interceptors in the demo, shows the header in the network tab, forces a 500 from the API, and reads the ApiError the component receives.

    Hands-on

    Write core/interceptors.ts with baseUrlInterceptor, correlationIdInterceptor (crypto.randomUUID()) and apiErrorInterceptor producing ApiError (core/api-error.ts). Register with withInterceptors. Remove the base URL from OrdersApi.

    Explain

    Why normalise errors in an interceptor?

    Check

    Three functional interceptors registered — checked (T3): seat_build seat_file seat_file seat_file

    #s3-4

  5. 3.5Retry, idempotency and timeouts8 minchecked · T3

    Predict

    retry({ count: 2 }) is applied to PATCH /orders/7/status. The first attempt succeeded server-side but the response was lost. What happens?

    Lesson

    retry({ count: 2, delay: backoff }) only on GET in OrdersApi; a timeout(8000) on every request; writes carry an Idempotency-Key header the API can honour later. Make the policy explicit in one place, not per call site.

    Demo

    The tutor adds retry with exponential delay to list() and get() in the demo, a timeout to all, and an Idempotency-Key header to updateStatus; kills the API mid-request and shows the retries and the final ApiError.

    Hands-on

    In OrdersApi add retry({ count: 2, delay: (e, n) => timer(200 * 2 ** n) }) to list and get only, timeout(8000) everywhere, and an Idempotency-Key header (randomUUID) on updateStatus.

    Explain

    State the retry policy in one sentence.

    Check

    Retry on reads only, timeouts, idempotency key on writes — checked (T3): seat_build seat_file craft

    #s3-5

  6. 3.6Lab: the HTTP layer, tested two ways10 minchecked · T3

    Predict

    Component tests use FakeOrdersApi; API tests hit the real server. What does neither prove?

    Lesson

    Two suites: HttpTestingController tests for OrdersApi (headers, URL, mapping, error normalisation) and server/api.spec.ts running against the API over fetch with a seeded database. A contract test runs the same assertions against FakeOrdersApi and the real one.

    Demo

    The tutor writes the demo's HttpTestingController test for the correlation header and the ApiError mapping, then an API test that patches a status and reads it back, then the shared contract test.

    Hands-on

    Write core/orders-api.spec.ts (HttpTestingController: "adds a correlation id header", "maps a 500 to ApiError", "maps DTOs to orders") and server/api.spec.ts ("lists seeded orders", "patches a status"). Both suites green.

    Explain

    What did HttpTestingController let you assert that a fake did not?

    Check

    HTTP layer and API tests pass — checked (T3): seat_test seat_test seat_lint seat_build

    #s3-6

Session 4 · Routing at scale

Six steps on URLs as the application's public API. Lazy feature routes with loadChildren and a routes file per feature; resolvers that load data before the view with proper error handling; guards for auth-like rules and unsaved changes; route-level providers scoping a store to a feature; deep links with query params as signals; title strategy and scroll restoration. Live in the seat: a feature chunk appearing in the build, a resolver redirecting on 404, a canDeactivate prompt on a dirty form, a store that dies with its feature, and a URL that restores a filtered, sorted view on reload.

Lab Orders routed at scale: lazy orders and checkout features, a resolver for the detail with a not-found redirect, an unsaved-changes guard, a feature-scoped store, query-param state for the list, titles; tested. Session 4 checkpoint.

6 mini steps · ~55 min
  1. 4.1Lazy features with loadChildren9 minchecked · T3

    Predict

    { path: 'orders', loadChildren: () => import('./features/orders/orders.routes').then(m => m.ORDERS_ROUTES) }. How many chunks for a feature with three routed components?

    Lesson

    Each feature owns its routes file (orders.routes.ts exporting ORDERS_ROUTES) and app.routes.ts lazy-loads it with loadChildren. Children can be eager within the feature or lazy again. The build table shows the chunk per feature.

    Demo

    The tutor moves the demo's order routes into features/orders/orders.routes.ts, wires loadChildren, rebuilds and points at the new chunk; does the same for checkout.

    Hands-on

    Create features/orders/orders.routes.ts (list, :id, :id/edit) and features/checkout/checkout.routes.ts; app.routes.ts lazy-loads both with loadChildren. Build and note the chunks in docs/BUILD-NOTES.md.

    Explain

    Where is the right split point for lazy loading?

    Check

    Two lazy features via loadChildren — checked (T3): seat_build seat_file seat_file seat_file

    #s4-1

  2. 4.2Resolvers that load before the view9 minchecked · T3

    Predict

    A ResolveFn calls api.get(id) which errors with 404. The resolver does nothing special. What does the user see?

    Lesson

    resolve: { order: orderResolver } where orderResolver: ResolveFn<Order> = (route) => api.get(id).pipe(catchError(() => of(new RedirectCommand(router.parseUrl('/not-found'))))). With withComponentInputBinding the component gets order = input.required<Order>().

    Demo

    The tutor writes order.resolver.ts in the demo, binds the resolved order to the detail's input, forces a 404 and shows the redirect; then shows the silent cancel when the catchError is removed.

    Hands-on

    Write features/orders/order.resolver.ts returning Order or a RedirectCommand to /not-found on error; use it in the :id route; OrderDetail takes order = input.required<Order>(). Remove the component's own loading code.

    Explain

    Why is a silent cancel worse than an error page?

    Check

    Resolver with RedirectCommand on error, bound to an input — checked (T3): seat_build seat_file seat_file seat_file

    #s4-2

  3. 4.3Guards: access and unsaved changes9 minchecked · T3

    Predict

    canDeactivate returns false when the edit form is dirty. The user presses the browser Back button. Result?

    Lesson

    canActivate for access rules (a role from a SessionService; redirect with UrlTree), canDeactivate for dirty forms (component exposes hasUnsavedChanges(); the guard asks it), canMatch to hide routes. Guards are plain functions: test them with TestBed.runInInjectionContext.

    Demo

    The tutor adds a roleGuard reading a fake SessionService and an unsavedChangesGuard in the demo, dirties the edit form, navigates away and shows the block; then tests both guards as functions.

    Hands-on

    Write core/session.service.ts (role signal), core/role.guard.ts (canActivate requiring 'staff', else /not-authorised), and features/orders/unsaved-changes.guard.ts (canDeactivate asking hasUnsavedChanges()). Apply both. Guard specs with 2 tests each.

    Explain

    Why keep the dirty check in the component and the decision in the guard?

    Check

    Role and unsaved-changes guards, applied and tested — checked (T3): seat_build seat_file seat_file seat_file seat_test

    #s4-3

  4. 4.4Route-level providers: a store per feature9 minchecked · T3

    Predict

    ORDERS_ROUTES = [{ path: '', providers: [OrdersStore], children: [...] }]. The user navigates from /orders to /checkout and back. Is OrdersStore the same instance?

    Lesson

    providers on a route create an environment injector scoped to that route tree. A feature store declared there (not providedIn root) exists only while the feature is active, is garbage-collected after, and cannot be injected elsewhere by mistake.

    Demo

    The tutor moves OrdersStore from root to the orders route providers in the demo, logs its constructor and destroy, navigates away and back, and shows a fresh instance each time; then shows another feature failing to inject it.

    Hands-on

    Remove providedIn from OrdersStore and declare it in ORDERS_ROUTES' top-level providers. Add a DestroyRef log in the store. Confirm checkout cannot inject it (compile is fine, runtime NullInjectorError in a test).

    Explain

    When should state be app-wide and when feature-scoped?

    Check

    OrdersStore scoped to the orders route — checked (T3): seat_build seat_file seat_file seat_file

    #s4-4

  5. 4.5Deep links: query params as signals9 minchecked · T3

    Predict

    /orders?status=paid&sort=total. With input binding, status = input<string>(). The user clears the filter. Which URL should the app produce?

    Lesson

    Query params bind to inputs like path params. The list reads status and sort inputs, derives the view with computed, and writes state back with router.navigate([], { queryParams, queryParamsHandling: 'merge' }), passing null to remove a param. Reload restores the view.

    Demo

    The tutor binds status and sort inputs in the demo list, drives filtering from them, writes changes back to the URL, reloads the preview and shows the same filtered view; shows the null-to-remove trick.

    Hands-on

    In OrdersPage add status = input<string>() and sort = input<string>() bound from query params; filtered/sorted computed from them; a select and a sort header that navigate with queryParamsHandling 'merge' and null for cleared values.

    Explain

    Why keep list state in the URL rather than a store?

    Check

    Status and sort as query-param inputs written back with merge — checked (T3): seat_build seat_file

    #s4-5

  6. 4.6Lab: routing at scale, titled and tested10 minchecked · T3

    Predict

    Routes have title: 'Orders' and a resolved title for the detail. What does a screen reader user gain?

    Lesson

    title on each route (static or a ResolveFn<string>), withInMemoryScrolling for scroll restoration, a not-found and a not-authorised route. Tests: RouterTestingHarness navigates to /orders?status=paid and asserts rows; a 404 id redirects; a dirty form blocks.

    Demo

    The tutor adds titles and scrolling options to the demo, then writes three harness tests covering deep link, resolver redirect and the deactivate guard; runs everything green.

    Hands-on

    Add title to every route (detail via a resolver building "Order #id"), withInMemoryScrolling, not-found and not-authorised pages. Write app.routing.spec.ts: "deep link filters by status", "unknown order redirects to not-found", "dirty edit blocks navigation".

    Explain

    Which of the three tests would have caught a missing catchError in the resolver?

    Check

    Titles, scrolling, error routes and three routing tests — checked (T3): seat_file seat_file seat_file seat_test seat_lint seat_build

    #s4-6

Session 5 · Testing at scale

Six steps that make a large suite trustworthy. The test pyramid for a front end; page objects that speak the user's language; fakes with a contract test against the real API; table-driven domain tests; API tests with a seeded database and transaction rollback; a coverage gate per folder and flakiness hunting with repeated runs. Live in the seat: a page object shrinking a 40-line test to 8, a contract test catching a drifted fake, a database test that leaves no trace, and a flaky test exposed by vitest --repeat and fixed by awaiting stability.

Lab The Orders suite at scale: page objects, a contract-tested fake API, table-driven pricing tests, seeded and rolled-back API tests, per-folder coverage gates (domain 95%, features 85%), hidden domain tests. Session 5 checkpoint.

6 mini steps · ~54 min
  1. 5.1The pyramid and what each layer proves8 minartefact · T2

    Predict

    A pricing rule changes. Which layer should catch it first and cheapest?

    Lesson

    Domain: pure unit tests, many and fast. Core: service tests with HttpTestingController. Features: component tests through the DOM with fakes. Routing: harness tests, few. API: server tests with a real database, few. Each layer proves one thing.

    Demo

    The tutor opens the demo's spec files and sorts them into the layers on a whiteboard comment in docs/TESTING.md, pointing at two tests that sit at the wrong layer and moving them down.

    Hands-on

    Write docs/TESTING.md: the five layers, which folders map to which, the rule for where a new test goes, and two existing tests you moved or would move down a layer and why.

    Explain

    What does moving a test down a layer buy?

    Check

    TESTING.md describes the layers — artefact (T2): seat_file

    #s5-1

  2. 5.2Page objects9 minchecked · T3

    Predict

    A page object exposes rows(), filterBy(status), rowText(i). The template changes a class name. How many tests change?

    Lesson

    A page object wraps a fixture: methods named for what a user does (filterBy, openRow, submit) and reads (rows, errorText). Query by role and label inside it. Tests read as scenarios; markup churn touches one file.

    Demo

    The tutor writes OrdersPageObject in the demo, rewrites two long tests with it, renames a class in the template, and shows only the page object changing.

    Hands-on

    Write features/orders/orders-page.po.ts with rows(), filterBy(status), openRow(i), errorText(); rewrite orders-page.spec.ts to use it exclusively (no querySelector in the spec).

    Explain

    What belongs in a page object and what does not?

    Check

    Page object used by the orders page spec — checked (T3): seat_file seat_file seat_test

    #s5-2

  3. 5.3Fakes with a contract test9 minchecked · T3

    Predict

    The real API starts returning 404 for unknown ids as { error: "not found" }; the fake throws. Which test catches the drift?

    Lesson

    OrdersApiContract: an abstract class the real OrdersApi and FakeOrdersApi both implement. A contract spec is a function that takes a factory and asserts behaviours (list returns orders; get unknown → ApiError 404; updateStatus round-trips), run once per implementation.

    Demo

    The tutor writes orders-api.contract.ts in the demo as a function of a factory, runs it against the fake and (when the server is up) the real API, then changes the fake's 404 behaviour and shows the contract failing.

    Hands-on

    Write core/orders-api.contract.ts exporting ordersApiContract(name, factory) with three behaviours, and two specs: fake-orders-api.spec.ts and server/orders-api.live.spec.ts calling it. Both green.

    Explain

    Why is a contract test the only honest fake?

    Check

    Contract test applied to fake and live API — checked (T3): seat_file seat_file seat_file seat_test seat_test

    #s5-3

  4. 5.4API tests with a seeded, rolled-back database9 minchecked · T3

    Predict

    Each API test runs inside BEGIN … ROLLBACK on its own connection. After 50 tests, how many rows did they leave?

    Lesson

    server/test-db.ts: open a client, BEGIN, seed from samples/s5/seed.sql, run the handler against that client, ROLLBACK in afterEach. The API's handlers take a client (or pool) parameter so tests can pass the transaction.

    Demo

    The tutor refactors the demo API handlers to accept a Queryable, writes test-db.ts with begin/seed/rollback, and runs the API tests twice to show the row count unchanged.

    Hands-on

    Refactor server/api.ts so handlers take a Queryable (pool or client). Write server/test-db.ts (begin, seed, rollback) and rewrite server/api.spec.ts to use it. Add "leaves no rows behind" asserting the count before and after.

    Explain

    Why inject the client rather than use a global pool in handlers?

    Check

    Transactional API tests leave no rows behind — checked (T3): seat_file seat_file seat_test

    #s5-4

  5. 5.5Coverage gates per folder and flakiness9 minchecked · T3

    Predict

    A test passes 9 times in 10 with vitest --repeat=10. Most likely cause?

    Lesson

    Coverage thresholds per folder in the Vitest config: domain 95% lines, core 90%, features 85%. Flakiness: run the suite with --repeat and a different --sequence.seed; a test that fails intermittently is a bug in the test or the code, never ignored.

    Demo

    The tutor sets per-folder thresholds in the demo config, runs coverage, then runs --repeat=5 and catches a flaky test that forgot to await whenStable, fixes it, repeats clean.

    Hands-on

    Add coverage thresholds (domain 95, core 90, features 85 lines) to the test config. Run the suite with --repeat=5; fix any flake. Record both results in docs/TESTING.md.

    Explain

    Why a higher gate for domain than features?

    Check

    Per-folder coverage thresholds and a repeat run recorded — checked (T3): seat_file seat_coverage seat_file

    #s5-5

  6. 5.6Lab: the suite at scale with hidden domain tests10 minproven · T4

    Predict

    The grader adds hidden pricing tests with discount percentages you never tried. What property must your code have?

    Lesson

    The suite complete: layers documented, page objects, a contract-tested fake, transactional API tests, gates. The hidden tests exercise domain/pricing with new percentages, line counts and edge cases (zero lines, 100% discount, half-cent rounding).

    Demo

    The tutor runs the whole demo suite with coverage and repeats, then the course's example hidden pricing spec, showing the half-up rounding case that trips a Math.round implementation.

    Hands-on

    Add a table-driven pricing.spec.ts (it.each with ≥8 rows including 0%, 100%, half-cent cases). All suites green, coverage gates met, then ask the tutor to run the check with hidden tests.

    Explain

    Which hidden case did you anticipate and how?

    Check

    Suite at scale passes with hidden pricing tests — proven (T4): seat_file seat_coverage seat_test seat_lint

    #s5-6

Session 6 · Performance

Six steps on speed you can measure. What change detection costs and how OnPush plus signals makes it local; @defer for below-the-fold and heavy components; bundle budgets and the build table as the review tool; virtualising long lists by hand with a windowed computed (no CDK in the seat); image and font loading; measuring with the production build and performance.mark in the App Preview. Live in the seat: a list of 5,000 rows that stops janking after OnPush and track, a chart component deferred until viewport, a budget warning traced to one import, and a hand-rolled window rendering 40 rows out of 5,000.

Lab The Orders list fast under load: OnPush everywhere, deferred report panel, a windowed list of 5,000 orders, budgets met, with before/after measurements recorded. Session 6 checkpoint.

6 mini steps · ~54 min
  1. 6.1Change detection and OnPush9 minchecked · T3

    Predict

    A Default-strategy list of 5,000 rows; one row's signal changes. How many row templates does Angular check?

    Lesson

    Default change detection checks everything on every event. OnPush checks a component only when its inputs change, an event fires inside it, or a signal it reads changes. With signals, mark every component OnPush and let the graph do the work.

    Demo

    The tutor renders 5,000 fake orders in the demo, types in the search box and shows the frame time; sets OnPush on the rows and page and shows the drop; counts checks with a console.count in a template function.

    Hands-on

    Set changeDetection: ChangeDetectionStrategy.OnPush on every component in orders. Add an ESLint rule (or a craft rule) enforcing it. Generate 5,000 orders in the fake API for the next steps.

    Explain

    What makes OnPush safe with signals where it was risky with mutable objects?

    Check

    Every component is OnPush — checked (T3): seat_build craft seat_file

    #s6-1

  2. 6.2@defer for heavy and below-the-fold components9 minchecked · T3

    Predict

    @defer (on viewport) { <app-report-panel /> } @placeholder { <p>Report</p> }. When is the panel's code downloaded?

    Lesson

    @defer (on viewport | on idle | on interaction | when cond) { … } @placeholder { … } @loading { … } @error { … } splits the deferred components into chunks and delays their work. The placeholder must have size so viewport detection works.

    Demo

    The tutor wraps the demo's ReportPanel (a heavy computed over all orders) in @defer (on viewport) with placeholder and loading blocks, rebuilds, and shows the new chunk and the later network request.

    Hands-on

    Create features/orders/report-panel.ts (totals by status over all orders) and render it under the list with @defer (on viewport), @placeholder with a sized box, @loading and @error blocks. Build and note the chunk.

    Explain

    Which trigger would you use for a modal that opens on click?

    Check

    ReportPanel deferred on viewport with placeholder, loading, error — checked (T3): seat_build seat_file

    #s6-2

  3. 6.3Bundle budgets and the build table8 minchecked · T3

    Predict

    The initial bundle jumps 180 kB after one commit that added import * as rx from "rxjs". Likely cause?

    Lesson

    Read the build table every time: initial total, each lazy chunk. Budgets: initial maximumWarning 350 kB, error 600 kB; anyComponentStyle 4 kB. Named imports only; move heavy code behind @defer or lazy routes; check for duplicate dependencies.

    Demo

    The tutor tightens the demo budgets until a warning appears, finds the cause with a namespace import, fixes it, and records before/after sizes in docs/PERF.md.

    Hands-on

    Set budgets: initial 350kB warning, 600kB error; anyComponentStyle 4kB warning. Make the build pass; convert any namespace imports to named imports. Record sizes in docs/PERF.md.

    Explain

    What is the single most useful number in the build table?

    Check

    Budgets tightened and met; no namespace imports — checked (T3): seat_file seat_build seat_file craft

    #s6-3

  4. 6.4Windowing a long list by hand10 minchecked · T3

    Predict

    5,000 rows at 32 px each in a 640 px viewport. How many rows must be in the DOM for a smooth scroll?

    Lesson

    No CDK in the seat, so write the window: a scroll container with a spacer div of rows × rowHeight, a scrollTop signal updated on (scroll), start = floor(scrollTop / rowHeight), visible = computed(() => all.slice(start - buffer, start + count + buffer)) positioned with translateY.

    Demo

    The tutor builds WindowedList in the demo over 5,000 orders, shows 40 rows in the DOM inspector while scrolling the whole list, and compares frame times with the unwindowed version.

    Hands-on

    Create shared/windowed-list.ts: inputs items and rowHeight, a scroll container, spacer, scrollTop signal, computed start/visible with a buffer of 5, translateY offset. Use it for the orders list. Row count in DOM must stay under 60.

    Explain

    What does the spacer div do?

    Check

    Hand-rolled windowed list renders a slice — checked (T3): seat_build seat_file seat_test

    #s6-4

  5. 6.5Measuring: production build and performance.mark8 minchecked · T3

    Predict

    You measure in ng serve (development) and see 400 ms to first render. Is that your production number?

    Lesson

    Measure the production build served from dist in the App Preview: performance.mark('orders:first-render') in an afterNextRender, performance.measure, read in the console. Change one thing, measure again. Write the numbers down.

    Demo

    The tutor adds marks around first render and first filter in the demo, builds for production, measures three runs each before and after OnPush and windowing, and tabulates them in PERF.md.

    Hands-on

    Add performance.mark/measure for first render (afterNextRender) and first filter in OrdersPage. Build production, measure three runs before (git stash your windowing) and after. Put the table in docs/PERF.md.

    Explain

    Why three runs rather than one?

    Check

    Marks in code and a before/after table — checked (T3): seat_file seat_file

    #s6-5

  6. 6.6Lab: the fast list, measured10 minchecked · T3

    Predict

    After all six steps, which change most reduced DOM work for 5,000 rows?

    Lesson

    The list at scale: OnPush everywhere, deferred report, windowed rows, tightened budgets, measurements recorded. The check reads the build, the craft rules, the windowed-list test and PERF.md.

    Demo

    The tutor runs the demo's full performance checklist end to end and reads the final PERF.md table aloud, noting the one change that mattered most.

    Hands-on

    Confirm: all components OnPush, @defer in place, windowed list test green, budgets met, PERF.md with before/after for first render and first filter. Lint and tests clean.

    Explain

    Name a performance change you would not make and why.

    Check

    Fast list: OnPush, defer, window, budgets, measurements — checked (T3): seat_build craft seat_test seat_file seat_lint

    #s6-6

Session 7 · Reading, reviewing and the AI era

Six steps on the skill that now matters most: judging code you did not write. Reading an unfamiliar component: entry point, data flow, invariants. A review checklist for Angular (signals misuse, subscriptions, OnPush violations, template logic, accessibility). Finding five planted defects in a supplied component and proving each with a failing test before fixing it. Verifying machine-generated code: treat it as a stranger's pull request, check its claims, run its tests, read its types. ESLint as a tireless first reviewer with a custom rule. An accessibility review with keyboard and a checklist. Live in the seat: the supplied review-me component read aloud, each defect pinned by a test, a generated service whose claimed test coverage was false, and a custom lint rule catching the recurring mistake.

Lab A written review of review-me.component.ts with all five defects found, each proven by a failing test and fixed; a generated service verified and corrected; a custom ESLint rule; an accessibility pass. Session 7 checkpoint.

6 mini steps · ~55 min
  1. 7.1Reading an unfamiliar component9 minartefact · T2

    Predict

    You open a 300-line component. Where do you start?

    Lesson

    Read in this order: the template (what renders), the contract (inputs, outputs, injected services), the state (signals and who writes them), the effects and subscriptions (what leaks). Write what you learn as notes before judging anything.

    Demo

    The tutor opens node/ng300/samples/s7/review-me.component.ts in the demo, reads it in that order aloud, and writes a half-page READING.md with the data flow and three suspicions, without fixing anything.

    Hands-on

    Copy samples/s7/review-me.component.ts (and .html) into features/review/. Write docs/REVIEW-READING.md: what it renders, its contract, each piece of state and its writers, and at least three suspicions. No code changes yet.

    Explain

    Why write the reading notes before changing code?

    Check

    Reading notes with contract, state and suspicions — artefact (T2): seat_file

    #s7-1

  2. 7.2A review checklist for Angular8 minartefact · T2

    Predict

    Which of these is a defect, not a style choice?

    Lesson

    docs/REVIEW-CHECKLIST.md: signals written inside effects; derived state stored instead of computed; subscriptions without takeUntilDestroyed; non-OnPush; mutation of arrays in signals; logic in templates; missing track; any; unlabeled controls; money as float; secrets in config; errors swallowed. Each item: how to spot it, how to prove it.

    Demo

    The tutor writes the checklist in the demo from the course's starter list and the team's own history, and marks which items a lint rule can catch and which need a human.

    Hands-on

    Write docs/REVIEW-CHECKLIST.md with at least twelve items, each with "spot it" and "prove it" columns, and a mark for lintable versus human-only.

    Explain

    Name one item only a human can review and why.

    Check

    A twelve-item checklist with spot and prove columns — artefact (T2): seat_file

    #s7-2

  3. 7.3Five planted defects, each proven by a test10 minchecked · T3

    Predict

    You believe the total is computed from a stale field. What comes first: the fix or the test?

    Lesson

    review-me.component.ts has five planted defects: a stale derived field, a signal written inside an effect, a subscription without cleanup, a mutating push on a signal array, and a float money total. For each: a failing test, then the fix, then green.

    Demo

    The tutor finds the stale total in the demo copy, writes the failing test, fixes with computed, goes green; then finds the float total and does the same, leaving three for the learner.

    Hands-on

    In features/review write review-me.component.spec.ts with five tests, one per defect, each failing before and passing after your fix. Keep the test names from samples/s7/DEFECTS-KEY.md's "test name" column.

    Explain

    Which defect was hardest to prove with a test and how did you do it?

    Check

    Five defect tests pass after fixes — checked (T3): seat_test craft

    #s7-3

  4. 7.4Verifying machine-generated code9 minchecked · T3

    Predict

    A generated service arrives with a note: "fully tested, 100% coverage". What do you do first?

    Lesson

    Treat generated code as a stranger's pull request: run it, read the tests for real assertions (not expect(true)), check types for any, look for swallowed errors and hard-coded values, verify it against the contract test. Then decide.

    Demo

    The tutor opens samples/s7/generated-discount.service.ts and its spec in the demo, finds a test with no assertion, a swallowed catch, and a percentage hard-coded; runs the contract and shows it failing.

    Hands-on

    Copy samples/s7/generated-discount.service.ts and spec into core/. Write docs/REVIEW-GENERATED.md listing every problem you found with file:line, fix the service and its spec so real assertions pass, and run the pricing table test against it.

    Explain

    What was the most convincing false signal in the generated code?

    Check

    Generated service reviewed, corrected and really tested — checked (T3): seat_file seat_file seat_file seat_test

    #s7-4

  5. 7.5A custom ESLint rule9 minchecked · T3

    Predict

    A flat-config local rule can be loaded without publishing a package?

    Lesson

    tools/eslint-rules/no-signal-write-in-effect.js: an AST rule that reports .set( or .update( calls on identifiers inside an effect(() => …) callback. Loaded as an inline plugin in eslint.config.js. The recurring review comment becomes a machine check.

    Demo

    The tutor writes the rule in the demo with the ESLint selector for CallExpression inside effect callbacks, adds a RuleTester test with valid and invalid cases, and runs eslint over the review-me component before its fix to show the hit.

    Hands-on

    Write tools/eslint-rules/no-signal-write-in-effect.js and a RuleTester spec with ≥2 valid and ≥2 invalid cases; register it in eslint.config.js as an error. eslint src must pass on your fixed code.

    Explain

    Which other checklist item would you automate next and how?

    Check

    Custom lint rule registered and tested — checked (T3): seat_file seat_file seat_file seat_lint

    #s7-5

  6. 7.6Lab: the written review and the accessibility pass10 minchecked · T3

    Predict

    A good review comment on a defect contains what?

    Lesson

    docs/REVIEW.md: for each of the five defects, what happens, why it matters, the test that proves it, the fix. Then the accessibility pass on the Orders pages with keyboard only: focus order, labels, names on icon buttons, live region for errors, contrast.

    Demo

    The tutor writes the demo REVIEW.md for the two defects they fixed in 7.3, then tabs through the demo orders page, finds a focus trap and an unlabeled button, fixes both.

    Hands-on

    Write docs/REVIEW.md with the five entries in the four-part form. Do a keyboard-only pass over Orders: add aria-label to icon buttons, aria-live="polite" to the error region, visible focus styles. All suites green.

    Explain

    What did the keyboard pass find that tests had not?

    Check

    Written review with proofs; accessibility fixes in place — checked (T3): seat_file seat_file seat_test seat_lint

    #s7-6

Session 8 · Capstone: Orders

Six steps that ship the system. The spec: orders arrive as JSON files; each is validated (customer, lines, quantities, SKUs against a catalogue, status), priced with discount rules, accepted or rejected with a reason code; the front end lists orders, shows rejections with reasons, and reports totals by status and by day. Plan and walking skeleton; the pure validation and pricing engine with a reason-code table; the import and rejection views; reports with exact cents; the API endpoints and contract; then the hidden tests over order files you never saw. Live in the seat: a six-order validation set with published reason codes, a report reconciling to the cent, and the grader's hidden file driving the whole stack.

Lab Orders complete: validated, priced, reported, routed, tested at 85%, API-backed; then the hidden order files. Course placement step. Session 8 checkpoint.

6 mini steps · ~61 min
  1. 8.1Spec to plan and skeleton9 minchecked · T3

    Predict

    The spec lists reason codes R01 (unknown SKU), R02 (qty <= 0), R03 (empty order), R04 (bad status). An order has an unknown SKU and qty 0 on another line. Which code?

    Lesson

    Read samples/s8/ORDERS-SPEC.md and REASON-CODES.md. Plan: domain/validate.ts (rules in spec order), domain/pricing.ts (exists), features/import, features/rejections, features/reports; API endpoints POST /orders/import, GET /rejections, GET /reports. Skeleton first.

    Demo

    The tutor reads the spec, writes PLAN.md with the rule table and the route/endpoint table, scaffolds the three feature folders with empty pages and routes, builds green.

    Hands-on

    Write PLAN.md (rule order table, reason codes, routes, endpoints, test list). Scaffold features/import, features/rejections, features/reports with lazy routes and empty pages. Build and lint clean.

    Explain

    Why must the rule order be fixed by the spec?

    Check

    PLAN.md and a routed skeleton for three features — checked (T3): seat_file seat_file seat_build seat_lint

    #s8-1

  2. 8.2The validation engine with reason codes10 minchecked · T3

    Predict

    validateOrder returns { ok: true; order } | { ok: false; reason: ReasonCode; detail: string }. Why not throw?

    Lesson

    domain/validate.ts: ReasonCode union, validateOrder(raw, catalogue) applying rules in spec order and returning a result union. Tests from samples/s8/validation-set.json: six orders with published codes; table-driven; then edge cases of your own.

    Demo

    The tutor writes the rules in order in the demo, runs the six-order set as an it.each, and shows one rule out of order changing a published code.

    Hands-on

    Write domain/validate.ts and validate.spec.ts: it.each over the six published orders plus ≥4 edge cases (duplicate SKU lines, negative unit price, unknown status, huge qty). No @angular import; no any.

    Explain

    Which edge case did the published set not cover, and what code did you give it?

    Check

    validateOrder with reason codes, table-tested on the published set — checked (T3): seat_file seat_file seat_test

    #s8-2

  3. 8.3Import and rejections views10 minchecked · T3

    Predict

    The import page shows "12 accepted, 3 rejected". Where should that summary be computed?

    Lesson

    features/import: a file input reads JSON, the domain validates and prices each order, the result (accepted, rejected with reasons) goes to the store and to POST /orders/import; a summary computed; features/rejections lists reason, detail and the original order with a filter by code.

    Demo

    The tutor imports the six-order set in the demo, shows the summary and the rejections page with codes, filters by R01, and shows aria-live announcing the summary.

    Hands-on

    Build ImportPage (file input, summary line in <p class="summary" aria-live="polite">, accepted/rejected lists) and RejectionsPage (table with code, detail, customer; filter by code via query param). Both OnPush, tested with a page object.

    Explain

    What makes the rejections page useful to a support person?

    Check

    Import and rejections pages wired to the domain and tested — checked (T3): seat_build seat_file seat_file seat_file seat_test

    #s8-3

  4. 8.4Reports that reconcile to the cent10 minchecked · T3

    Predict

    Totals by status sum to 1,234,567 cents; the grand total of accepted orders is 1,234,566. What does that tell you?

    Lesson

    domain/reports.ts: totalsByStatus, totalsByDay (ISO date from createdAt), grandTotal, all in cents; reconcile() asserts the groupings sum to the grand total. ReportsPage renders tables with a cents pipe and a reconciliation line. Dates grouped in the order's time zone as given.

    Demo

    The tutor writes the three aggregations in the demo, the reconcile check, a test where a dropped order breaks reconciliation, and renders the report tables deferred on viewport.

    Hands-on

    Write domain/reports.ts (totalsByStatus, totalsByDay, grandTotal, reconcile) with ≥6 tests including reconciliation on the published 20-order set (samples/s8/orders-20.json). ReportsPage with two tables and <strong class="grand">.

    Explain

    Why does reconcile belong in the domain and not just in a test?

    Check

    Reports in cents with reconciliation, tested on the published set — checked (T3): seat_file seat_file craft seat_file seat_test

    #s8-4

  5. 8.5API endpoints and the contract10 minchecked · T3

    Predict

    POST /orders/import receives a file with one invalid order. HTTP status for the response?

    Lesson

    server: POST /orders/import (validates with the shared domain, stores accepted and rejected with reason, returns both), GET /rejections?code=, GET /reports (cents). Shared domain code guarantees API and UI agree. Contract test extended; transactional API tests.

    Demo

    The tutor adds the endpoints in the demo API reusing domain/validate and reports, extends the contract with import behaviour, runs fake and live, and shows a reject stored with its code.

    Hands-on

    Implement the three endpoints in server/api.ts using the domain functions and parameterised SQL; extend ordersApiContract with "import returns accepted and rejected"; API tests for each endpoint in the rollback harness.

    Explain

    What would drift if the API had its own copy of the validation rules?

    Check

    Import, rejections and reports endpoints with contract and API tests — checked (T3): seat_file seat_file seat_test craft

    #s8-5

  6. 8.6Lab: Orders proven12 minproven · T4

    Predict

    The hidden file has 41 orders, 7 of them invalid in ways not in the published set. What must still hold?

    Lesson

    Orders complete: architecture, async, HTTP, routing, tests at scale, performance, review. The grader drives the import page and the API with hidden order files and checks reason codes, reconciliation, DOM summary and the rejections filter.

    Demo

    The tutor runs the full demo suite with coverage and repeats, then an example hidden file from a past term through the import page object and the API, showing the DOM summary and reports reconcile.

    Hands-on

    Final pass: coverage ≥85%, lint 0, budgets met, all suites green, docs complete (PLAN, ADR, TESTING, PERF, REVIEW). Ask the tutor to run the capstone check.

    Explain

    Which three decisions from earlier sessions made the capstone hidden tests passable?

    Check

    Orders passes its own suites and the hidden order files — proven (T4): seat_build seat_lint seat_coverage seat_file seat_test web_dom

    #s8-6

Outcome

By the end of the course the tutor's checks will have verified: a building, linting, tested feature for every hands-on step; eight session labs that each grow the last — the architected shell, the cancellable search with resource(), the typed API client and interceptors, the lazy feature routes with resolvers, the fake-backed test suite at 85% coverage, the @defer-optimised list under budget, the written review of a defective component; and the Orders capstone, whose validation, pricing rules, reject reasons and reports pass hidden Vitest tests and DOM checks over order files you did not see.

Certificate

Agile System Software Certificate — Angular, advanced (NG300) attests that you can architect, build, test and tune a production-shaped Angular 22 front end over a typed HTTP API, with async state, lazy routing, fakes and coverage, and review code written by others or by machines, verified by a machine from your own seat.

To earn it:

  • Pass the lab check of every session (steps 1.6 to 7.6) on your own code.
  • Pass the Orders capstone check (8.6) including the hidden Vitest tests and DOM checks the grader supplies.
  • Score 80% or better on the 30-question reading quiz.

Gates (step · tier required):

Plus a 30-question reading quiz at 80%, retake after 48 h. Certificates are issued once the seat check runner is live; until then every step records what the engine saw.

Environment

A VS Code seat in your browser with Node 22, TypeScript 6, Angular 22, Vitest with jsdom and coverage, ESLint, tsx, the pg driver and PostgreSQL 17 running inside the seat, plus the App Preview; all offline, nothing to install. Your work lives in node/ng300; the tutor's demo files live apart from yours and the tutor never edits your hands-on file while you are working on it.

Open Coding Tutor at codingtutor.agilesystemsoftware.com, pick NG300 from the course button in the chat and press Tutor me. Every lesson, demo and hands-on step is also published below.