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

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

More HTTP requests

  1. The same service can cover all the actions your API supports.
  2. Use HttpParams for query parameters such as page or search, and set headers when needed.
  3. PUT replaces a whole item. PATCH changes only some fields.
  4. DELETE removes an item by id.
  5. The API processes each request and returns the result.

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.