Twice already on the trail you have met the line
this.http.get(...). Once at the bridge to the signals, where toSignal turned a server's answer into smoke on the watchtower, and once on the ninja trails, where streams learned to wait and to filter. Both times that line simply worked, and both times we walked around two questions: where does the http field inside the class come from, and why does the very same line, copied into a freshly created application, bring it down moments after start-up.Because a courier is not merely a man with a bundle. Before anyone runs from castle to castle, the muster roll of the stronghold has to list a post station: the place where couriers wait for orders and from which any member of the household is allowed to summon one. When nobody has raised that station, calling for a courier does not end in silence - it ends in a crash. Angular breaks off building the class and prints an error carrying the line
NullInjectorError: No provider for HttpClient!. I handed an empty injector a request for HttpClient purely to see that message with my own eyes, and it reads exactly so.This lesson is about raising the station and sending out the first courier for a scroll. Nothing is carried to the neighbouring castle today - today we only bring things back.
provideHttpClient() functionwithFetch() first among them, and to know the one case where that feature is better left outHttpClient into a guild with a single import line and a single inject() callHttpParams class instead of pasting strings togetherYou already know the place where the station is raised, from the module on the Tokaido trails. It is the file
app.config.ts, and inside it the appConfig constant of type ApplicationConfig - the configuration object handed to the application at start-up. That type has exactly one field: the providers array, the roll of everything that is to be at hand for as long as the application runs. You added the map of routes to it with the provideRouter function, importing the routes constant from the neighbouring app.routes file. The courier station is added like a twin: with the provideHttpClient function, which you import from the @angular/common/http package. It creates no courier by itself - it returns a ready set of entries, which the providers array accepts exactly as it accepted the router's.1// app.config.ts
2import { ApplicationConfig } from '@angular/core';
3import { provideRouter } from '@angular/router';
4import { provideHttpClient } from '@angular/common/http';
5import { routes } from './app.routes';
6
7export const appConfig: ApplicationConfig = {
8 providers: [
9 provideRouter(routes),
10 provideHttpClient()
11 ]
12};From this moment on, every component and every service in the application may ask for
HttpClient and will get one. But notice how little changed along the way: we touched not a single component, we added nothing to any class, we did not even name a server. The station has no idea where its couriers will be running, and it does not need to - addresses come later, at the individual calls. No request has flown anywhere either, and none will until somebody asks for one.Learn the name of the function exactly, because three others sound just as believable and not one of them works.
provideHttp() and importHttpClient() do not exist - I put both through the compiler and each ends with error TS2305 saying that the module "@angular/common/http" has no such exported member. The third suggestion is sneakier, because HttpClientModule genuinely does exist and you will meet it in older examples on the web. It belongs, though, to the world of NgModule modules, and its home is the imports array of the @NgModule decorator - and app.config.ts has no such thing as an imports field, because ApplicationConfig carries providers and nothing else. Trying to add that field ends with error TS2353 stating outright that imports does not exist in type ApplicationConfig. Dropping the class into providers instead is the nastier version of the mistake, because it compiles without a murmur and then falls over at run time with that same NullInjectorError: No provider for HttpClient! - I checked it on a live injector. And in the Angular 19 typings the HttpClientModule class is marked deprecated anyway, with a note pointing at provideHttpClient() in providers as the replacement.The empty parentheses in
provideHttpClient() are not decoration. That function takes any number of features - small functions that each add one skill to the station. Their names all begin with with, and you will meet several in this module: withInterceptors posts a barrier on the road, which has a lesson of its own here, while withFetch swaps out the very way a courier covers the distance.By default Angular sends requests through the
XMLHttpRequest mechanism - the old, trusted horse that has been carrying scrolls since the first browsers. withFetch() tells it to use the newer Fetch API instead, built today into every browser and into Node.js from version 18. A feature is passed as an argument, so the call ends up with two storeys of parentheses: first you call the feature itself, then you put its result inside the provideHttpClient call.1// app.config.ts
2import { ApplicationConfig } from '@angular/core';
3import { provideHttpClient, withFetch } from '@angular/common/http';
4
5export const appConfig: ApplicationConfig = {
6 providers: [
7 provideHttpClient(withFetch())
8 ]
9};What matters most is what did not change. No service in the application looks any different after this, no method changes its name, no address needs correcting, and the calls still hand back the same streams. We swapped the horse under the courier - the road, the bundle and the orders were left untouched.
There is one case in which this feature is not switched on, and I would rather tell you now than after a lost afternoon. The Angular 19 typings describe
withFetch() in a single line and then add a warning: the Fetch API does not report progress on uploads. I went into the compiled package and it holds to the letter - the HttpEventType.UploadProgress event is emitted only by the old road built on XMLHttpRequest, while the road built on fetch knows nothing but download progress. So if the application is meant to show a progress bar while a file is being sent - and you will build such a bar in the very next lesson - the branch of code handling UploadProgress will never run with withFetch() enabled, though everything compiles without a single warning. My recommendation therefore comes in two parts, @name: in a new application start from provideHttpClient(withFetch()), all the more so when the application is also rendered on the server, where the browser's XMLHttpRequest does not exist natively and has to be emulated while fetch is native. But when an upload progress bar is on the plan, leave the call without that feature.The station stands, so it is time to bring a courier into the guild. The
HttpClient class lives in the @angular/common/http package - not in @angular/core, the home of inject, signal and Injectable. This is not a detail to memorise for its own sake: I put the import from @angular/core through the compiler and it ends with error TS2305 about a missing export. The import line looks like this, and only like this.1import { HttpClient } from '@angular/common/http';Read it token by token, because the order never varies. First the
import keyword. Then the name in braces - the braces are required, because HttpClient is a named export, one of many in that package, and not a default export. Then the word from, which separates what you are taking from where you are taking it. At the end the package path in quotes, and a semicolon. Nothing here can be reordered: from placed ahead of the braces has nothing left to separate, and a path without from is only a loose string.Notice what this line does not do. It does not create a courier. It brings in the class itself - the blueprint, and at the same time the token with which we will ask for a ready instance in a moment.
Before we send the first courier out, let us agree on what we are sending him for. The clan registry keeps entries about warriors, and every entry is an object with three fields: an
id number granted by the scribe in the castle, a given name in name, and the name of the house in clan. The number is a number, the two remaining fields are strings. All of this is described by a TypeScript interface - the way, known to you from the gate of the Academy, of telling the compiler what an object looks like.1interface Samurai {
2 id: number;
3 name: string;
4 clan: string;
5}An interface creates no object and never reaches the browser - it disappears at compilation and all that survives it is the compiler's knowledge. It serves one purpose here: so that when the courier returns your editor suggests
clan rather than staying silent, and so that a typo in a field name is caught before anybody runs the application.The service we are putting the courier into looks exactly like the artisan guilds from the fifth module. The
@Injectable decorator with the providedIn: 'root' option means one guild for the whole kingdom. The http field is filled in by the inject() function from the @angular/core package, to which you hand a class as a token and which hands back a ready instance from the station raised in the configuration. The apiUrl field holds the common beginning of the address so that it need not be repeated in every method. Both fields are marked private, because nothing outside the guild has any business reaching for them.1import { Injectable, inject } from '@angular/core';
2import { HttpClient } from '@angular/common/http';
3
4@Injectable({ providedIn: 'root' })
5export class SamuraiService {
6 private http = inject(HttpClient);
7 private apiUrl = '/api/samurais';
8}The class is empty for now, and that is exactly the point - what has appeared is the place we will be adding methods to. Look at what has not changed compared with the guilds you built in the artisans' quarter: the same decorator, the same
providedIn: 'root', the same inject(). The fact that this service talks to a server forced not one extra ceremony on us. The only thing that changed is the class passed to inject().Three other ways of getting hold of a courier are tempting, and I put all three through the compiler.
new HttpClient() ends with error TS2554 saying that one argument was expected and none was given - because HttpClient needs a dispatch mechanism to work, and normally Angular is the one who hands it over. Even if you supplied that argument, what you would get is a courier from outside the station: one that walks past the barriers waiting for you later in this module, and past the whole configuration from app.config.ts. HttpClient.getInstance() ends with error TS2339 reading that the property getInstance does not exist on type typeof HttpClient - it is a pattern borrowed from other languages, one Angular does not use, because handing out instances is the job of dependency injection. That leaves the third idea: since the class has already been imported, perhaps it is enough to call the methods straight on it? It is not - HttpClient.get('/api/samurais') is the same TS2339, this time about the property get. The get method belongs to an instance, and the import brings in only the class. An instance comes from one place and one place only, the inject(HttpClient) call - and whether you keep its result in a class field, as above, or in a plain const, is a question of convenience that changes nothing in the mechanism.The first method is to bring back the whole registry. We will call it
getAll, and inside it we call the get() method on the courier, handing it the address. In the angle brackets before the address we write the type we expect in the answer - here an array of samurai, that is Samurai[]. The type returned by the method itself is Observable<Samurai[]>, and the Observable class is imported from rxjs, exactly as on the ninja trails.1getAll(): Observable<Samurai[]> {
2 return this.http.get<Samurai[]>(this.apiUrl);
3}The
get<Samurai[]> notation is a promise made to the compiler, not an inspection at the gate. Angular does not verify that the server really sent an array of entries - I put a lying scribe behind the courier, one that returned an object { oops: ... } instead of an array, and the stream handed that object over without blinking, even though the code said Samurai[]. The angle brackets help you and your editor; they do not protect you from a server that changed its mind. Were the shape of the answer genuinely uncertain, the check would have to be written by hand.Notice as well what the method did not do. After calling it no request has flown anywhere. All that came into being is a description of the journey - who is to run, where to, and what for.
Here lies the question most people trip over: what exactly does
http.get() return? The answer is an Observable, the stream you know from the module of the ninja scouts. Not a Promise - I checked this on a working client and the returned object has no then method at all. Not the data - because the road to the neighbouring castle takes time while the method finishes at once, so if it were to return the entries it would have to be holding them already. And not a Callback - the get() method takes no function to be called later; inside the parentheses stand only the address and the optional options.The difference between a stream and a promise can be treacherous, so I checked that too:
await placed in front of such a call settles nothing - instead of waiting for the answer it hands you back the very same stream, the identical object. To reach the data you either subscribe, or deliberately convert the stream into a promise with a separate function from rxjs.The function comes in only at subscription time, and it is the subscription that sends the courier on his way. In a component it looks like this: you take the service with
inject(), and in the ngOnInit lifecycle method, familiar from the module on components, you subscribe to the stream. Everything around it is an ordinary standalone component - a selector under which it appears in a template, and a short inline template of its own.1import { Component, inject, OnInit } from '@angular/core';
2import { SamuraiService } from './samurai.service';
3
4@Component({
5 selector: 'app-roster',
6 standalone: true,
7 template: '<p>Clan registry</p>'
8})
9export class RosterComponent implements OnInit {
10 private samurai = inject(SamuraiService);
11
12 ngOnInit(): void {
13 this.samurai.getAll().subscribe(list => {
14 console.log('Entries in the registry:', list.length);
15 });
16 }
17}Only that one line with
subscribe pushes the courier out through the gate. I checked it on a stand-in scribe that counted visits: before the subscription the counter stood at zero, after the first subscription at one, and after a second one was added, at two. HTTP streams are lazy and cold: two subscriptions to the same stream mean two separate requests, not one answer served to two listeners. So if you were tempted to store the stream in a field and subscribe to it in two places in the template, you would be sending two couriers after the same scroll.The bare
subscribe is here so that the mechanics stay visible. In a real component you will reach instead for the AsyncPipe from the ninja trails, or for toSignal, with which you built the bridge to the watchtower - both of them subscribe on your behalf, and both clean the subscription up when the component leaves the screen.The clan registry can run long, so we rarely want all of it. We want the entries matching a phrase, sometimes narrowed to a single house. Conditions like that are added to the address after a question mark, and it is tempting to paste them on as ordinary text. Do not: the phrase "ronin of the north" pushed straight into the address leaves raw spaces sitting in it, and a single
& inside the phrase - "sword & spear" is enough - splits the address into two parameters. Safe assembly is the job of the HttpParams class, from the same package as HttpClient. You create an empty set with new HttpParams() and add further pairs with the set() method, giving it the parameter name and the value. The finished set goes into the options object as the params field. Note the question mark in clan? as well: narrowing by house is optional, so the second argument may be left out entirely.1search(query: string, clan?: string): Observable<Samurai[]> {
2 let params = new HttpParams().set('q', query);
3 if (clan) {
4 params = params.set('clan', clan);
5 }
6 return this.http.get<Samurai[]>(this.apiUrl, { params });
7}Look closely at the
let and at the params = params.set(...) assignment, because neither is an ornament. HttpParams is immutable: set() adds nothing to the object you call it on, it returns a new set. I checked it - after the set('clan', ...) call the old object still held its one parameter, and the second one existed only in the returned copy. Had you written a plain params.set('clan', clan); without the assignment, the narrowing by house would simply have vanished, and the compiler would not have said a word about it.What the class does with the values I checked on a finished request. The phrase with spaces travelled as
q=ronin%20of%20the%20north&clan=Takeda, and the ampersand in "sword & spear" came out as q=sword%20%26%20spear, harmless now that it is no longer read as a separator. That rewriting is the only thing that happened here - it is still a plain GET to the same address, and when I looked at the outgoing request its body was null and its list of headers was empty. Parameters are part of the address, not of the content.The last typical case is a single warrior fetched by number. The method brings nothing new - it is still
get() and still the same type in the angle brackets, only in the singular. The one thing that changes is the address, because the number has to be glued onto the path. We assemble it with a template literal: the notation written in backticks, in which the places for values are marked with ${...}.1getById(id: number): Observable<Samurai> {
2 return this.http.get<Samurai>(`${this.apiUrl}/${id}`);
3}Mind those backticks, because this is the commonest slip in the whole section and one the compiler will not report. I checked what happens with ordinary quotes: the request flew to the address
/api/samurais/${id}, carrying the notation itself instead of the number, so the server was handed a path it does not know. With a correct template literal the address read /api/samurais/7. And nothing changed but the address - it is still the same courier, the same method and the same lazy stream, which sets off only once somebody subscribes.Remember, @name: the station in
app.config.ts gives the application its couriers, inject(HttpClient) assigns one of them to your guild, and get() merely writes down his orders - the one who sends him onto the road is whoever starts listening.