We use cookies to enhance your experience on the site
CodeWorlds

The courier station - HttpClient from the first scroll

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.

What you will learn

  • to write the courier station into the castle's roll with the
    provideHttpClient()
    function
  • to choose its features,
    withFetch()
    first among them, and to know the one case where that feature is better left out
  • to bring
    HttpClient
    into a guild with a single import line and a single
    inject()
    call
  • to send a GET request that announces the type it expects to come back
  • to understand why a stream leaves the method instead of data, and why nobody runs until somebody listens
  • to glue questions onto an address with the
    HttpParams
    class instead of pasting strings together

The post station in the castle's roll

You 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 station's features, or what the parentheses in provideHttpClient are for

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.

One line worth knowing by heart

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.

What a samurai is in code

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.

A courier in the guild, or inject instead of new

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 scroll, or get

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.

Why a stream comes back and not the data

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.

A question glued onto the address

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.

One entry, one address

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.

Go to CodeWorlds