Angular HTTP: Query Parameters, Headers, PUT, PATCH and DELETE Requests

Angular Tutorial: Beginner to Advanced · 10. HTTP and RxJS · 5 min read · Updated

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, patch and delete
  • 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. Use HttpParams.
// 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.
  • set replaces a key, append adds another value for the same key.
  • Angular encodes values in the query string for you. For path parameters, wrap user-supplied values in encodeURIComponent so special characters cannot change the URL.
  • observe: 'response' returns an HttpResponse with status, headers and body. For example response.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 with reportProgress: true.
  • responseType: 'blob' (or 'text', 'arraybuffer') is for non-JSON responses.
  • To upload a file, send FormData as the body. Do not set Content-Type yourself, 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

  1. Add a sort and direction parameter to list and a test for them.
  2. Read the X-Total-Count header to build "page 2 of 5" text.
  3. Write a method that uploads a file with FormData and a test that checks the body is a FormData.

Quick check

  1. Why must you assign params = params.set(...)?
  2. What is the difference between put and patch?
  3. How do you read a response header?

Next, continue with RxJS Basics for Angular: Observables and Operators.

Frequently asked questions

Can I send a body with GET?

The HTTP rules do not forbid it, but many servers and proxies ignore it. Use query parameters for GET and a body for POST.

Where should the API base URL live?

In configuration, such as an InjectionToken provided in app.config.ts or an environment value, so you can change it per environment.