Skip to main content

How to make HTTP requests in Angular

Angular applications normally call APIs through HttpClient. It provides typed responses, headers, URL parameters, error handling, interceptors, and RxJS Observables.

This guide starts with a normal GET request and then shows how the same client can send a custom QUERY request when an API supports it.

1. Enable HttpClient​

In a standalone Angular app, register the client when the application starts:

// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideHttpClient } from '@angular/common/http';

export const appConfig: ApplicationConfig = {
providers: [provideHttpClient()],
};

For an NgModule-based application, import HttpClientModule instead. Configure this once in the root of the application.

2. A simple http.get() example​

Suppose the API returns users:

[
{ "id": 1, "name": "Asha", "email": "asha@example.com" }
]

Create a type for the response and a service that owns the request:

// user.service.ts
import { HttpClient } from '@angular/common/http';
import { Injectable, inject } from '@angular/core';
import { Observable } from 'rxjs';

export interface User {
id: number;
name: string;
email: string;
}

@Injectable({ providedIn: 'root' })
export class UserService {
private readonly http = inject(HttpClient);

getUsers(): Observable<User[]> {
return this.http.get<User[]>('https://api.example.com/users');
}
}

The User[] generic lets TypeScript describe the data you expect. It does not validate an untrusted response at runtime, so add runtime validation when that boundary needs it.

Use the service in a component. The async pipe subscribes and cleans up automatically:

// user-list.component.ts
import { AsyncPipe } from '@angular/common';
import { Component, inject } from '@angular/core';
import { UserService } from './user.service';

@Component({
selector: 'app-user-list',
imports: [AsyncPipe],
template: `
@if (users$ | async; as users) {
<ul>
@for (user of users; track user.id) {
<li>{{ user.name }} — {{ user.email }}</li>
}
</ul>
}
`,
})
export class UserListComponent {
private readonly users = inject(UserService);
readonly users$ = this.users.getUsers();
}

HttpClient observables are cold: the request is sent only when something subscribes. In this example, the async pipe is that subscription.

3. Add query parameters safely​

Use HttpParams instead of manually joining URL strings. Its methods return a new value, so keep the returned instance:

import { HttpClient, HttpParams } from '@angular/common/http';

const params = new HttpParams()
.set('status', 'active')
.set('pageSize', '25');

this.http.get<User[]>('https://api.example.com/users', { params });

Angular encodes the values and produces a URL similar to:

https://api.example.com/users?status=active&pageSize=25

4. Handle errors​

Handle expected failures near the request or in a shared interceptor:

import { HttpErrorResponse } from '@angular/common/http';
import { catchError, throwError } from 'rxjs';

getUsers(): Observable<User[]> {
return this.http.get<User[]>('https://api.example.com/users').pipe(
catchError((error: HttpErrorResponse) => {
const message = error.status === 0
? 'Network error. Check your connection.'
: `Could not load users (${error.status}).`;

return throwError(() => new Error(message));
}),
);
}

Do not put secrets in browser code. Use HTTPS, and usually add authentication in an Angular HTTP interceptor instead of repeating an authorization header in every service.

5. Send a custom method with http.request()​

HttpClient.request() accepts the method name as a string. This covers standard methods such as POST and PATCH, and a newer or custom method for which Angular has no shortcut.

Office Orbit uses it for HTTP QUERY. QUERY is a safe, read-only method for complex searches that need a JSON body. Its API uses GET for simple URL-sized filters and QUERY when filters contain arrays, multiple criteria, pagination, or relation options.

This is the core pattern used in Office Orbit's API service:

import { HttpClient } from '@angular/common/http';
import { Injectable, inject } from '@angular/core';
import { Observable } from 'rxjs';

interface WorkLog {
id: string;
title: string;
}

interface ListResponse<T> {
data: T[];
count: number;
hasMore: boolean;
nextCursor: string | null;
}

interface WorkLogFilters {
categories?: string[];
types?: string[];
projectIds?: string[];
}

@Injectable({ providedIn: 'root' })
export class WorkLogApiService {
private readonly http = inject(HttpClient);

queryWorkLogs(filters: WorkLogFilters): Observable<ListResponse<WorkLog>> {
return this.http.request<ListResponse<WorkLog>>(
'QUERY',
'https://api.example.com/api/work-logs',
{
body: {
filters,
pageSize: 25,
includeRelations: true,
},
},
);
}
}

The request sends structured JSON:

{
"filters": {
"categories": ["Office Work", "Grooming"],
"types": ["Work", "Support"],
"projectIds": ["project-page-id"]
},
"pageSize": 25,
"includeRelations": true
}

Use QUERY only when the server explicitly supports it. A cross-origin browser request also needs CORS support from the API, including QUERY in Access-Control-Allow-Methods:

Access-Control-Allow-Methods: GET, QUERY, POST, PATCH, DELETE, OPTIONS

6. Pick the right method​

NeedAngular callHTTP method
Load a user or a small filtered listhttp.get()GET
Create a recordhttp.post()POST
Replace a complete recordhttp.put()PUT
Change selected fieldshttp.patch()PATCH
Run a complex, read-only search with a JSON bodyhttp.request('QUERY', ...)QUERY

Keep API calls in services, type their successful responses, and let interceptors handle cross-cutting concerns such as authentication, retry policy, and request logging. Components can then focus on presenting data instead of constructing HTTP requests.