The first HTTP lessons used get and post. This lesson covers the rest of the everyday toolkit: query strings, headers, the other methods, and how to read more than the body.
What you will learn
- URL (path) parameters and query strings with
HttpParams - Setting headers with
HttpHeaders put,patchanddelete- Reading the full response, status and headers
URL parameters and query strings
There are two kinds of parameters:
- Path parameters are part of the URL:
/users/42. Build them with a template string. - Query parameters come after
?:/users?page=2&sort=name. UseHttpParams.
// src/app/http2/user-api.ts
import { HttpClient, HttpHeaders, HttpParams, HttpResponse } from '@angular/common/http';
import { Injectable, inject } from '@angular/core';
import { Observable } from 'rxjs';
export interface User {
id: number;
name: string;
email: string;
}
export interface UserQuery {
page: number;
pageSize: number;
search?: string;
tags?: string[];
}
@Injectable({ providedIn: 'root' })
export class UserApi {
private readonly http = inject(HttpClient);
private readonly baseUrl = 'https://api.example.com/users';
// GET /users?page=1&pageSize=10&search=ada&tags=a&tags=b
list(query: UserQuery): Observable<User[]> {
let params = new HttpParams().set('page', query.page).set('pageSize', query.pageSize);
if (query.search) {
params = params.set('search', query.search);
}
for (const tag of query.tags ?? []) {
params = params.append('tags', tag); // append keeps several values for one key
}
return this.http.get<User[]>(this.baseUrl, { params });
}
// GET /users/42 (path parameter)
one(id: number): Observable<User> {
return this.http.get<User>(`${this.baseUrl}/${encodeURIComponent(id)}`);
}
// A simple object works too, and Angular builds the query string
listSimple(page: number): Observable<User[]> {
return this.http.get<User[]>(this.baseUrl, { params: { page, sort: 'name' } });
}
// POST with custom headers
create(user: Omit<User, 'id'>): Observable<User> {
const headers = new HttpHeaders({ 'X-Request-Source': 'angular-course' }).set('Accept-Language', 'en');
return this.http.post<User>(this.baseUrl, user, { headers });
}
// PUT replaces the whole resource
replace(user: User): Observable<User> {
return this.http.put<User>(`${this.baseUrl}/${user.id}`, user);
}
// PATCH changes only some fields
rename(id: number, name: string): Observable<User> {
return this.http.patch<User>(`${this.baseUrl}/${id}`, { name });
}
// DELETE usually returns no body
remove(id: number): Observable<void> {
return this.http.delete<void>(`${this.baseUrl}/${id}`);
}
// Read status and headers: observe 'response'
listWithTotal(): Observable<HttpResponse<User[]>> {
return this.http.get<User[]>(this.baseUrl, { observe: 'response' });
}
// Download a file as a Blob
exportCsv(): Observable<Blob> {
return this.http.get(`${this.baseUrl}/export`, { responseType: 'blob' });
}
}
Important facts:
- `HttpParams` and `HttpHeaders` are immutable.
params.set(...)returns a new object. If you forget to assign it back (params = params.set(...)), nothing changes. setreplaces a key,appendadds another value for the same key.- Angular encodes values in the query string for you. For path parameters, wrap user-supplied values in
encodeURIComponentso special characters cannot change the URL. observe: 'response'returns anHttpResponsewithstatus,headersandbody. For exampleresponse.headers.get('X-Total-Count')is a common way to read the total number of items for pagination.observe: 'events'also reports upload and download progress, together withreportProgress: true.responseType: 'blob'(or'text','arraybuffer') is for non-JSON responses.- To upload a file, send
FormDataas the body. Do not setContent-Typeyourself, the browser adds the correct boundary. - Never put secrets or tokens in query strings, because URLs are saved in logs and history. Use headers or the body.
Which method for what?
- `GET`: Purpose: Read data; Body: None
- `POST`: Purpose: Create something; Body: Yes
- `PUT`: Purpose: Replace something completely; Body: Yes
- `PATCH`: Purpose: Change some fields; Body: Yes
- `DELETE`: Purpose: Remove something; Body: Usually none
Test the requests
Tests are the best way to see exactly what is sent:
// src/app/http2/user-api.spec.ts
import { TestBed } from '@angular/core/testing';
import { provideHttpClient } from '@angular/common/http';
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
import { UserApi } from './user-api';
describe('UserApi', () => {
let api: UserApi;
let http: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({ providers: [provideHttpClient(), provideHttpClientTesting()] });
api = TestBed.inject(UserApi);
http = TestBed.inject(HttpTestingController);
});
afterEach(() => http.verify());
it('builds the query string, including repeated keys', () => {
api.list({ page: 2, pageSize: 10, search: 'ada', tags: ['a', 'b'] }).subscribe();
const req = http.expectOne((r) => r.url === 'https://api.example.com/users');
expect(req.request.params.get('page')).toBe('2');
expect(req.request.params.get('search')).toBe('ada');
expect(req.request.params.getAll('tags')).toEqual(['a', 'b']);
req.flush([]);
});
it('sends custom headers on POST', () => {
api.create({ name: 'Ada', email: 'ada@example.com' }).subscribe();
const req = http.expectOne('https://api.example.com/users');
expect(req.request.method).toBe('POST');
expect(req.request.headers.get('X-Request-Source')).toBe('angular-course');
req.flush({ id: 1, name: 'Ada', email: 'ada@example.com' });
});
it('uses PUT, PATCH and DELETE with the right urls', () => {
api.replace({ id: 5, name: 'A', email: 'a@x.dev' }).subscribe();
api.rename(5, 'B').subscribe();
api.remove(5).subscribe();
const urls = [
http.expectOne((r) => r.method === 'PUT'),
http.expectOne((r) => r.method === 'PATCH'),
http.expectOne((r) => r.method === 'DELETE'),
];
expect(urls.map((r) => r.request.url)).toEqual(Array(3).fill('https://api.example.com/users/5'));
expect(urls[1].request.body).toEqual({ name: 'B' });
urls.forEach((r) => r.flush(null));
});
it('can read response headers', () => {
let total: string | null = null;
api.listWithTotal().subscribe((res) => (total = res.headers.get('X-Total-Count')));
http.expectOne('https://api.example.com/users').flush([], { headers: { 'X-Total-Count': '42' } });
expect(total).toBe('42');
});
});
Use the service from a component
// src/app/app.ts
import { Component, inject, signal } from '@angular/core';
import { User, UserApi } from './http2/user-api';
@Component({
imports: [],
selector: 'app-root',
templateUrl: './app.html',
styleUrl: './app.css',
})
export class App {
private readonly api = inject(UserApi);
protected readonly users = signal<User[]>([]);
protected load(): void {
this.api.list({ page: 1, pageSize: 5 }).subscribe({
next: (list) => this.users.set(list),
error: () => this.users.set([]),
});
}
}
<!-- src/app/app.html -->
<main>
<h1>Users</h1>
<button type="button" (click)="load()">Load</button>
<p>{{ users().length }} users</p>
</main>
Try it yourself
- Add a
sortanddirectionparameter tolistand a test for them. - Read the
X-Total-Countheader to build "page 2 of 5" text. - Write a method that uploads a file with
FormDataand a test that checks the body is aFormData.
Quick check
- Why must you assign
params = params.set(...)? - What is the difference between
putandpatch? - How do you read a response header?
Next, continue with RxJS Basics for Angular: Observables and Operators.