Angular / Intermediate / Lesson 9
HTTP and runtime validation
Separate HTTP transport from domain decoding, model request outcomes explicitly, and test with synthetic responses instead of contacting real services.
On this page
Lesson goals
- Provide HttpClient and explain cold request observables.
- Decode unknown response data before domain use.
- Test failure, cancellation, and duplicate subscription behavior.
A generic HTTP result type is not a validator
HttpClient offers a typed interface to browser HTTP behavior when provided in the application. The generic argument to get<T> describes what the caller claims the response contains; it does not inspect incoming JSON to establish T. Prefer unknown at an untrusted response boundary and decode the actual shape and domain rules. Separate malformed data from a transport error and from an authorized empty result. A service should expose the intended application contract rather than making every component repeat endpoint strings and assertions. No real HTTP endpoint is contacted in these lessons.
Construct a selected domain value after checks
The complete local decoder checks a synthetic course response for object identity, required fields, a positive safe identifier, and a nonempty title. It returns a newly selected object rather than trusting unrelated fields. This is a decoder for ordinary parsed JSON, not a guarantee about arbitrary proxies or getters. The program proves local acceptance and rejection behavior, not request delivery, CORS configuration, or server authorization. Extend the checks according to the real endpoint schema, including size limits for arrays or text. Avoid hiding numeric strings through coercion unless that normalization is a documented API rule.
export {};
type Course = { id: number; title: string };
function decodeCourse(value: unknown): Course | undefined {
if (typeof value !== "object" || value === null || !("id" in value) || !("title" in value)) { return undefined; }
if (typeof value.id !== "number" || !Number.isSafeInteger(value.id) || value.id < 1) { return undefined; }
if (typeof value.title !== "string" || value.title.length === 0) { return undefined; }
return { id: value.id, title: value.title };
}
console.log(JSON.stringify(decodeCourse({ id: 7, title: "Forms", internal: "omit" })));
console.log(decodeCourse({ id: "7", title: "Forms" }) === undefined);
{"id":7,"title":"Forms"}
true
The valid response becomes only id and title; the string identifier is rejected. No HTTP request is sent.
Keep request configuration at a deliberate boundary
The contextual service injects HttpClient and returns an Observable<unknown> for a synthetic relative endpoint. The application must supply provideHttpClient; a missing provider is not repaired by adding a TypeScript import alone. The caller must validate id under the route/domain contract and decode each result before use. Each subscription to a cold HttpClient observable can initiate another backend request, so accidental duplicate subscriptions can duplicate work. Unsubscribing can cancel client-side progress, but it does not prove that a server-side operation was undone. Mutation requests need explicit idempotency and retry policies.
import { Injectable, inject } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { Observable } from "rxjs";
@Injectable({ providedIn: "root" })
export class CourseApi {
private readonly http = inject(HttpClient);
load(id: number): Observable<unknown> {
return this.http.get<unknown>(`/api/courses/${id}`);
}
}
A correctly provided isolated HTTP test can observe a GET to the stated relative URL. This fragment is not subscribed or executed and contacts no server.
Represent loading, failure, and cancellation honestly
Expose request state so the view can distinguish loading, data, empty success, and failure without inventing an empty array for every error. Choose where errors are caught: catching inside an inner request stream can preserve a search stream, while catching at an outer boundary can end its subscription. Retry only operations and failure categories for which repetition is safe, with bounded attempts and delay. Functional interceptors can add shared transport behavior, but should not silently change every endpoint’s domain contract. A client token header does not replace server validation of identity and permission. Protect sensitive error details from presentation and logs.
Use the HTTP testing boundary rather than real services
An isolated Angular HTTP test can provide the client and testing backend, capture an expected request, flush synthetic data, and verify no unexpected requests remain. Provider ordering matters: the testing backend must override the client backend after client provisioning. Test malformed success bodies, transport failures, duplicate subscriptions, and latest-request behavior. A spy proving get was called does not prove decoding, template rendering, or actual browser networking. Keep no-request and cancellation assertions distinct from server completion claims. The local decoder remains useful even when the transport implementation changes because its contract concerns values rather than an endpoint’s availability.
Common errors and fixes
A get<Course> response contains an invalid identifier.
- Why it happens
- The generic annotation was mistaken for runtime decoding.
- How to fix it
- Request unknown and validate the actual response.
- Prevent it next time
- Keep negative decoder fixtures.
One view triggers duplicate backend requests.
- Why it happens
- A cold request observable has multiple unintended subscriptions.
- How to fix it
- Choose one consumption boundary or deliberate sharing policy.
- Prevent it next time
- Assert expected request counts in isolated tests.
HttpClient cannot be injected.
- Why it happens
- The application lacks the intended provider.
- How to fix it
- Provide the client at the supported application boundary.
- Prevent it next time
- Test actual provider configuration, not imports alone.
Check your understanding
1. Decode {id: 0, title: "Forms"} and {id: 7, title: ""}. State both results and why a type assertion is not a repair.
Show worked solution
Both return undefined: zero violates the positive identifier rule, and an empty title violates the text rule. An assertion changes checker treatment without fixing either runtime value. If the endpoint intentionally permits these values, change the schema and tests explicitly. Otherwise reject them before any component assumes a valid Course.
2. A request is unsubscribed after the user navigates away. Can the UI claim a remote save was rolled back? Explain the required contract.
Show worked solution
No. Client cancellation can stop local observation or abort supported transport work, but the server may already have committed the operation. Define mutation identity, retry behavior, status reconciliation, and user feedback according to the API. Test local cleanup separately from the server’s authoritative result rather than treating unsubscription as a distributed transaction rollback.
Key takeaways
- Transport typing and data validation differ.
- Subscriptions and cancellation have observable effects.
- Test with synthetic backend responses, not real services.