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
| Need | Angular call | HTTP method |
|---|---|---|
| Load a user or a small filtered list | http.get() | GET |
| Create a record | http.post() | POST |
| Replace a complete record | http.put() | PUT |
| Change selected fields | http.patch() | PATCH |
| Run a complex, read-only search with a JSON body | http.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.