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.
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).
- 1. Architecture of a front end
- 2. Async and state
- 3. Talking to an API
- 4. Routing at scale
- 5. Testing at scale
- 6. Performance
- 7. Reading, reviewing and the AI era
- 8. Capstone: Orders
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.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_buildseat_fileseat_fileseat_file1.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_buildseat_fileseat_test1.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_fileseat_testcraft1.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_buildseat_fileseat_fileseat_file1.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_fileseat_lint1.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_fileseat_lintseat_buildseat_testseat_file
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
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_buildseat_file2.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_buildseat_file2.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_buildseat_fileseat_file2.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_buildseat_file2.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_fileseat_testseat_file2.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_testseat_lintseat_build
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
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_fileseat_buildcraft3.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_buildseat_fileseat_file3.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_fileseat_fileseat_testseat_file3.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_buildseat_fileseat_fileseat_file3.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_buildseat_filecraft3.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_testseat_testseat_lintseat_build
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
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_buildseat_fileseat_fileseat_file4.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_buildseat_fileseat_fileseat_file4.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_buildseat_fileseat_fileseat_fileseat_test4.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_buildseat_fileseat_fileseat_file4.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_buildseat_file4.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_fileseat_fileseat_fileseat_testseat_lintseat_build
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
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_file5.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_fileseat_fileseat_test5.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_fileseat_fileseat_fileseat_testseat_test5.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_fileseat_fileseat_test5.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_fileseat_coverageseat_file5.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_fileseat_coverageseat_testseat_lint
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
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_buildcraftseat_file6.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_buildseat_file6.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_fileseat_buildseat_filecraft6.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_buildseat_fileseat_test6.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_fileseat_file6.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_buildcraftseat_testseat_fileseat_lint
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
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_file7.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_file7.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_testcraft7.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_fileseat_fileseat_fileseat_test7.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_fileseat_fileseat_fileseat_lint7.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_fileseat_fileseat_testseat_lint
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
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_fileseat_fileseat_buildseat_lint8.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_fileseat_fileseat_test8.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_buildseat_fileseat_fileseat_fileseat_test8.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_fileseat_filecraftseat_fileseat_test8.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_fileseat_fileseat_testcraft8.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_buildseat_lintseat_coverageseat_fileseat_testweb_dom
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.