Chapter 1 — What is HTTP and Why Do You Need It?
1.1 — Your App Needs to Talk to the Outside World
Everything you have built so far has used data that was hardcoded directly in your components and services. The product list was a plain array in a TypeScript file. The blog posts were objects you typed manually. The user was a mock object you created yourself.
Real applications do not work this way. Real applications get their data from a server. When a user logs in, their credentials are verified by a server. When they browse products, those products come from a database through a server. When they place an order, that order is saved on a server. When they update their profile, that update is sent to a server.
The way your Angular app communicates with a server is through HTTP — Hypertext Transfer Protocol. It is the same protocol your browser uses to load web pages. Your Angular app sends HTTP requests to an API (a server endpoint), and the server sends back HTTP responses with data.
This phase covers how Angular handles all of this.
1.2 — What is a REST API?
When Angular talks to a server, it almost always talks to a REST API. REST (Representational State Transfer) is a set of conventions for how URLs and HTTP methods should be organized.
The conventions work like this. Each URL represents a resource — a type of data. The HTTP method tells the server what to do with that resource:
GET /api/products → fetch all products
GET /api/products/42 → fetch one product with id 42
POST /api/products → create a new product
PUT /api/products/42 → replace the entire product with id 42
PATCH /api/products/42 → update specific fields of product with id 42
DELETE /api/products/42 → delete the product with id 42
When you fetch data, you use GET. When you create something new, you use POST. When you update something, you use PUT or PATCH. When you delete something, you use DELETE.
The server responds with data in JSON format — which is perfect for JavaScript and TypeScript because JSON maps directly to objects and arrays.
1.3 — Angular's HttpClient
Angular provides a built-in service called HttpClient for making HTTP requests. It lives in @angular/common/http. It is the official, recommended way to communicate with APIs from Angular.
HttpClient returns Observables from every method. An Observable is a stream of data over time. When you make an HTTP request, the Observable emits once when the response arrives, and then completes. We go deep into Observables in Phase 9. For now just know: you call .subscribe() on an Observable to actually make the request and receive the response.
Chapter 2 — Setting Up HttpClient
2.1 — Providing HttpClient in Your App
In Angular, you need to make HttpClient available before you can use it. You do this in app.config.ts with provideHttpClient:
src/app/app.config.ts:
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes),
provideHttpClient() // ← add this one line
]
};
That single line makes HttpClient available for injection in every service and component in your entire application.
2.2 — A Note About the API We Will Use
Throughout this phase, we will use a real public API called JSONPlaceholder at https://jsonplaceholder.typicode.com. It is a free fake REST API for testing and learning. It has endpoints for posts, users, comments, todos, and albums — all returning real-looking JSON data.
This means you do not need to build or run a backend server. The requests go to the real internet and come back with real data.
Chapter 3 — Making HTTP Requests
3.1 — Your First GET Request
Let's build a service that fetches a list of posts from the API.
Generate the service:
ng g s services/posts --skip-tests
src/app/services/posts.ts:
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
export interface Post {
id: number;
userId: number;
title: string;
body: string;
}
@Injectable({
providedIn: 'root'
})
export class Posts {
private http = inject(HttpClient);
private apiUrl = 'https://jsonplaceholder.typicode.com';
// GET all posts
getAllPosts(): Observable<Post[]> {
return this.http.get<Post[]>(`${this.apiUrl}/posts`);
}
// GET a single post by ID
getPostById(id: number): Observable<Post> {
return this.http.get<Post>(`${this.apiUrl}/posts/${id}`);
}
}
this.http.get<Post[]>(url) — the <Post[]> is a TypeScript generic. You are telling Angular "I expect the response to be an array of Post objects." TypeScript then knows the shape of the data when you use it.
The method returns Observable<Post[]> — a stream that will eventually emit the array of posts when the server responds.
Nothing actually happens yet. The HTTP request is not sent until something subscribes to the Observable.
3.2 — Using the Service in a Component
Now let's create a component that uses this service:
src/app/pages/posts/posts.ts:
import { Component, inject, OnInit, signal } from '@angular/core';
import { Posts as PostsService, Post } from '../../services/posts';
@Component({
selector: 'app-posts',
imports: [],
templateUrl: './posts.html',
styleUrl: './posts.css'
})
export class PostsPage implements OnInit {
private postsService = inject(PostsService);
posts = signal<Post[]>([]);
isLoading = signal<boolean>(false);
error = signal<string>('');
ngOnInit(): void {
this.loadPosts();
}
loadPosts(): void {
this.isLoading.set(true);
this.error.set('');
this.postsService.getAllPosts().subscribe({
next: (data) => {
this.posts.set(data);
this.isLoading.set(false);
},
error: (err) => {
this.error.set('Failed to load posts. Please try again.');
this.isLoading.set(false);
console.error('HTTP Error:', err);
}
});
}
}
The .subscribe() call is where the magic happens. This is what actually fires the HTTP request. You pass an object with two callbacks:
next — runs when the response arrives successfully. The data parameter contains the parsed JSON response, already typed as Post[] because of the generic you specified.
error — runs when something goes wrong — the server returned an error status code, the network is down, or any other HTTP error. The err parameter contains information about what went wrong.
There is also an optional third callback:
complete — runs after next, when the Observable finishes emitting. For HTTP requests this is not commonly used because HTTP responses only emit once.
src/app/pages/posts/posts.html:
<div class="posts-page">
<h1>Latest Posts</h1>
@if (isLoading()) {
<div class="loading">
<div class="spinner"></div>
<p>Loading posts...</p>
</div>
}
@if (error()) {
<div class="error-banner">
<p>{{ error() }}</p>
<button (click)="loadPosts()">Try Again</button>
</div>
}
@if (!isLoading() && !error()) {
<div class="posts-grid">
@for (post of posts(); track post.id) {
<div class="post-card">
<span class="post-id">#{{ post.id }}</span>
<h3>{{ post.title }}</h3>
<p>{{ post.body }}</p>
</div>
}
</div>
}
</div>
src/app/pages/posts/posts.css:
.posts-page {
max-width: 900px;
margin: 0 auto;
padding: 48px 24px;
}
h1 {
font-size: 36px;
font-weight: 800;
color: #1a1a2e;
margin-bottom: 32px;
}
.loading {
display: flex;
flex-direction: column;
align-items: center;
padding: 64px;
gap: 16px;
color: #888;
}
.spinner {
width: 40px;
height: 40px;
border: 3px solid #e0e0e0;
border-top-color: #0070f3;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.error-banner {
background: #fef2f2;
border: 1px solid #fecaca;
border-radius: 12px;
padding: 24px;
text-align: center;
color: #dc2626;
}
.error-banner button {
margin-top: 12px;
background: #dc2626;
color: white;
border: none;
padding: 8px 20px;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
}
.posts-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 20px;
}
.post-card {
background: white;
padding: 22px;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.07);
transition: box-shadow 0.2s;
}
.post-card:hover {
box-shadow: 0 4px 20px rgba(0,0,0,0.12);
}
.post-id {
font-size: 12px;
font-weight: 700;
color: #0070f3;
text-transform: uppercase;
letter-spacing: 1px;
}
.post-card h3 {
font-size: 16px;
font-weight: 600;
color: #1a1a2e;
margin: 8px 0;
text-transform: capitalize;
}
.post-card p {
font-size: 14px;
color: #666;
line-height: 1.6;
}
3.3 — The Three States of Every HTTP Call
Every HTTP request has three possible states and your UI should handle all three:
Loading — the request has been sent but no response yet. Show a spinner or skeleton screen. Disable any action buttons.
Success — the server responded with data. Show the data.
Error — something went wrong. Show a meaningful error message. Provide a way to retry.
This pattern of three signals — isLoading, data, and error — is something you will use in almost every component that makes an HTTP call. It becomes second nature.
3.4 — POST — Creating New Data
Sending a POST request creates a new resource on the server. You pass the data to create as the second argument to http.post():
export interface CreatePostDto {
title: string;
body: string;
userId: number;
}
@Injectable({
providedIn: 'root'
})
export class Posts {
private http = inject(HttpClient);
private apiUrl = 'https://jsonplaceholder.typicode.com';
createPost(data: CreatePostDto): Observable<Post> {
return this.http.post<Post>(`${this.apiUrl}/posts`, data);
}
}
Using it in a component:
newPostTitle: string = '';
newPostBody: string = '';
isCreating = signal<boolean>(false);
createPost(): void {
this.isCreating.set(true);
const newPost: CreatePostDto = {
title: this.newPostTitle,
body: this.newPostBody,
userId: 1
};
this.postsService.createPost(newPost).subscribe({
next: (createdPost) => {
console.log('Created post:', createdPost);
// The API returns the created object with a new ID
this.posts.update(current => [createdPost, ...current]);
this.newPostTitle = '';
this.newPostBody = '';
this.isCreating.set(false);
},
error: (err) => {
console.error('Failed to create post:', err);
this.isCreating.set(false);
}
});
}
http.post() takes the URL and the request body. The body is automatically serialized to JSON. Angular automatically adds the Content-Type: application/json header.
3.5 — PUT — Replacing an Entire Resource
PUT replaces the entire resource with the new data you send:
updatePost(id: number, data: Post): Observable<Post> {
return this.http.put<Post>(`${this.apiUrl}/posts/${id}`, data);
}
3.6 — PATCH — Updating Specific Fields
PATCH sends only the fields you want to change, not the entire object:
patchPost(id: number, changes: Partial<Post>): Observable<Post> {
return this.http.patch<Post>(`${this.apiUrl}/posts/${id}`, changes);
}
Partial<Post> is a TypeScript utility type that makes all fields of Post optional. This is perfect for PATCH because you might only be sending { title: 'New Title' } without the other fields.
3.7 — DELETE — Removing a Resource
DELETE removes a resource. The server usually returns an empty object or no body:
deletePost(id: number): Observable<void> {
return this.http.delete<void>(`${this.apiUrl}/posts/${id}`);
}
Using it:
deletePost(postId: number): void {
this.postsService.deletePost(postId).subscribe({
next: () => {
// Remove from local array after successful deletion
this.posts.update(current => current.filter(p => p.id !== postId));
console.log('Post deleted successfully');
},
error: (err) => {
console.error('Failed to delete:', err);
}
});
}
Chapter 4 — Request Options
4.1 — HTTP Headers
Sometimes you need to send extra information with your request in the form of headers. The most common is an Authorization header for authenticated requests, or a custom Content-Type header.
import { HttpClient, HttpHeaders } from '@angular/common/http';
getSecureData(): Observable<any> {
const headers = new HttpHeaders({
'Authorization': 'Bearer your-jwt-token-here',
'X-Custom-Header': 'some-value'
});
return this.http.get<any>(`${this.apiUrl}/secure-endpoint`, { headers });
}
HttpHeaders is immutable — once created, you cannot change it. But you can create a new one from an existing one:
const baseHeaders = new HttpHeaders({ 'Content-Type': 'application/json' });
const authHeaders = baseHeaders.set('Authorization', 'Bearer token123');
// baseHeaders still does NOT have the Authorization header
// authHeaders is a new object that has both headers
4.2 — Query Parameters
For adding query parameters programmatically, use HttpParams:
import { HttpClient, HttpParams } from '@angular/common/http';
searchPosts(query: string, page: number = 1, limit: number = 10): Observable<Post[]> {
const params = new HttpParams()
.set('q', query)
.set('_page', page.toString())
.set('_limit', limit.toString());
return this.http.get<Post[]>(`${this.apiUrl}/posts`, { params });
// Makes request to: /posts?q=angular&_page=1&_limit=10
}
Like HttpHeaders, HttpParams is immutable. Each .set() returns a new HttpParams object.
4.3 — Observe the Full Response
By default, Angular gives you just the response body — the actual data. But sometimes you need the full HTTP response including the status code and response headers. Use observe: 'response':
import { HttpClient, HttpResponse } from '@angular/common/http';
getPostWithHeaders(id: number): Observable<HttpResponse<Post>> {
return this.http.get<Post>(`${this.apiUrl}/posts/${id}`, {
observe: 'response' // give me the full response object
});
}
Using it:
this.postsService.getPostWithHeaders(1).subscribe({
next: (response) => {
console.log('Status code:', response.status); // 200
console.log('Status text:', response.statusText); // OK
console.log('Headers:', response.headers);
console.log('Body (actual data):', response.body); // the Post object
}
});
4.4 — Response Types
By default Angular expects JSON responses. For other formats, use the responseType option:
// Download a file as blob
downloadFile(fileName: string): Observable<Blob> {
return this.http.get(`${this.apiUrl}/files/${fileName}`, {
responseType: 'blob'
});
}
// Get plain text response
getTextContent(): Observable<string> {
return this.http.get(`${this.apiUrl}/content`, {
responseType: 'text'
});
}
Chapter 5 — Error Handling
5.1 — Types of HTTP Errors
When an HTTP request fails, Angular gives you an HttpErrorResponse object. Errors come in two categories:
Client-side / network errors — the request never reached the server. The user is offline, the URL is wrong, a network timeout occurred. These errors have error.status === 0.
Server-side errors — the server received the request but responded with an error status code. 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 500 Internal Server Error. These have a non-zero status code.
5.2 — Handling Errors in the Component
The simplest way to handle errors is in the component's subscribe callback:
import { HttpErrorResponse } from '@angular/common/http';
loadPosts(): void {
this.isLoading.set(true);
this.error.set('');
this.postsService.getAllPosts().subscribe({
next: (data) => {
this.posts.set(data);
this.isLoading.set(false);
},
error: (err: HttpErrorResponse) => {
this.isLoading.set(false);
if (err.status === 0) {
// Network error — no connection to server
this.error.set('Network error. Please check your internet connection.');
} else if (err.status === 404) {
this.error.set('The requested data was not found.');
} else if (err.status === 401) {
this.error.set('You are not authorized. Please log in again.');
} else if (err.status === 500) {
this.error.set('Server error. Please try again later.');
} else {
this.error.set(`Something went wrong (Error ${err.status}).`);
}
console.error('HTTP Error Details:', err);
}
});
}
5.3 — Handling Errors in the Service with catchError
For reusable error handling that you want to centralize in the service rather than handle in every component, use the catchError RxJS operator:
import { Injectable, inject } from '@angular/core';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError, retry } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class Posts {
private http = inject(HttpClient);
private apiUrl = 'https://jsonplaceholder.typicode.com';
getAllPosts(): Observable<Post[]> {
return this.http.get<Post[]>(`${this.apiUrl}/posts`).pipe(
retry(1), // retry the request once before giving up
catchError(this.handleError)
);
}
private handleError(error: HttpErrorResponse): Observable<never> {
let errorMessage = '';
if (error.status === 0) {
errorMessage = 'Network error. Please check your connection.';
} else {
errorMessage = `Server error ${error.status}: ${error.message}`;
}
console.error(errorMessage);
return throwError(() => new Error(errorMessage));
}
}
.pipe() is how you chain RxJS operators on an Observable. Think of it as a pipeline — the HTTP response flows through each operator in order.
retry(1) automatically retries the failed request one time before passing the error down. Useful for transient network issues.
catchError catches any error in the pipeline, lets you handle it, and then decides what to return. throwError(() => new Error(message)) re-throws the error so the component's error callback still fires.
We cover RxJS operators thoroughly in Phase 9.
Chapter 6 — HTTP Interceptors
6.1 — What is an Interceptor?
An interceptor is a piece of code that sits between your application and every HTTP request and response. It intercepts every request going out and every response coming in.
This is incredibly powerful for cross-cutting concerns — things that need to happen for every single HTTP call without you having to add code to every single service:
- Adding an auth token to every request automatically
- Showing a global loading indicator when any request is in flight
- Logging every request and response for debugging
- Handling 401 errors globally by redirecting to login
- Retrying failed requests automatically
- Caching responses
Without interceptors, you would have to add the auth token manually in every service method. With an interceptor, you write it once and it applies everywhere automatically.
6.2 — Creating an Auth Token Interceptor
Generate an interceptor:
ng generate interceptor interceptors/auth-token --skip-tests
src/app/interceptors/auth-token.ts:
import { HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
import { Auth } from '../services/auth';
export const authTokenInterceptor: HttpInterceptorFn = (request, next) => {
const authService = inject(Auth);
// Get the auth token from the auth service
const token = authService.getToken();
if (token) {
// Clone the request and add the Authorization header
const authRequest = request.clone({
headers: request.headers.set('Authorization', `Bearer ${token}`)
});
return next(authRequest); // pass the modified request along
}
return next(request); // no token — pass original request unchanged
};
An interceptor is a function that receives the outgoing request and a next function. You call next(request) to pass the request to the next interceptor or to actually send it to the server.
The most important thing here is request.clone(). HTTP requests are immutable in Angular — you cannot modify them directly. You must create a modified copy using .clone(). Pass an object with the properties you want to change.
6.3 — Creating a Loading Interceptor
This interceptor increments a counter when a request starts and decrements it when it finishes. Any component can read this counter to know if any HTTP call is in progress:
First, create a loading service:
src/app/services/loading.ts:
import { Injectable, signal, computed } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class Loading {
private requestCount = signal<number>(0);
readonly isLoading = computed(() => this.requestCount() > 0);
increment(): void {
this.requestCount.update(count => count + 1);
}
decrement(): void {
this.requestCount.update(count => Math.max(0, count - 1));
}
}
Now the interceptor:
src/app/interceptors/loading.ts:
import { HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
import { finalize } from 'rxjs/operators';
import { Loading } from '../services/loading';
export const loadingInterceptor: HttpInterceptorFn = (request, next) => {
const loadingService = inject(Loading);
loadingService.increment(); // request started
return next(request).pipe(
finalize(() => {
loadingService.decrement(); // request finished (success OR error)
})
);
};
finalize is an RxJS operator that runs a callback when the Observable completes or errors. It is the perfect place to turn off the loading state because it runs regardless of whether the request succeeded or failed.
6.4 — Creating an Error Interceptor
This interceptor handles common HTTP error scenarios globally:
src/app/interceptors/error.ts:
import { HttpInterceptorFn, HttpErrorResponse } from '@angular/common/http';
import { inject } from '@angular/core';
import { Router } from '@angular/router';
import { catchError, throwError } from 'rxjs';
export const errorInterceptor: HttpInterceptorFn = (request, next) => {
const router = inject(Router);
return next(request).pipe(
catchError((error: HttpErrorResponse) => {
if (error.status === 401) {
// Token expired or invalid — send user to login
console.warn('Unauthorized. Redirecting to login.');
router.navigate(['/login']);
}
if (error.status === 403) {
console.warn('Forbidden. You do not have permission for this action.');
}
if (error.status === 0) {
console.error('Network error. No connection to server.');
}
if (error.status >= 500) {
console.error('Server error:', error.status);
}
return throwError(() => error); // re-throw so components can still handle it
})
);
};
6.5 — Registering Interceptors
Interceptors are registered in app.config.ts using withInterceptors:
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { routes } from './app.routes';
import { authTokenInterceptor } from './interceptors/auth-token';
import { loadingInterceptor } from './interceptors/loading';
import { errorInterceptor } from './interceptors/error';
export const appConfig: ApplicationConfig = {
providers: [
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes),
provideHttpClient(
withInterceptors([
authTokenInterceptor, // runs first — adds auth token
loadingInterceptor, // runs second — tracks loading state
errorInterceptor // runs third — handles errors globally
])
)
]
};
Interceptors run in the order they are listed for outgoing requests, and in reverse order for incoming responses.
6.6 — Using the Loading Service in a Component
Now any component can show a global loading indicator by reading the Loading service:
import { Component, inject } from '@angular/core';
import { Loading } from './services/loading';
@Component({
selector: 'app-root',
imports: [],
template: `
@if (loadingService.isLoading()) {
<div class="global-loader">
<div class="loader-bar"></div>
</div>
}
<router-outlet></router-outlet>
`
})
export class App {
loadingService = inject(Loading);
}
Every single HTTP request across your entire app automatically triggers this loader. No code changes needed in any service.
Chapter 7 — Environment Variables
7.1 — Why Environment Variables?
Your API URL is different depending on where your app is running:
During development on your laptop, the API might be at http://localhost:3000/api.
During staging, it might be at https://staging-api.myapp.com/api.
In production, it might be at https://api.myapp.com/api.
You do not want to manually change the URL every time you deploy. Environment variables let you define different values for different environments and Angular automatically uses the right one.
7.2 — Creating Environment Files
Angular does not create environment files by default in new projects. You create them manually.
Create this folder and two files:
src/environments/
├── environment.ts ← used during development (ng serve)
└── environment.prod.ts ← used during production (ng build)
src/environments/environment.ts:
export const environment = {
production: false,
apiUrl: 'http://localhost:3000/api',
appName: 'MyApp (Dev)'
};
src/environments/environment.prod.ts:
export const environment = {
production: true,
apiUrl: 'https://api.myapp.com/api',
appName: 'MyApp'
};
7.3 — Telling Angular to Swap Files on Build
You need to configure angular.json so Angular knows to replace environment.ts with environment.prod.ts when building for production. Find the configurations → production section and add:
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
]
}
}
Now when you run ng build --configuration=production, Angular automatically swaps the files.
7.4 — Using Environment Variables in Services
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { environment } from '../../environments/environment';
@Injectable({
providedIn: 'root'
})
export class Posts {
private http = inject(HttpClient);
private apiUrl = environment.apiUrl; // ← reads from environment file
getAllPosts(): Observable<Post[]> {
return this.http.get<Post[]>(`${this.apiUrl}/posts`);
}
}
When you run ng serve, environment.apiUrl is http://localhost:3000/api. When you run ng build --configuration=production, environment.apiUrl is https://api.myapp.com/api. You never touch the service code.
Chapter 8 — Organizing API Services
8.1 — One Service Per Resource
The best practice is to create one service per API resource. Each service handles all the HTTP calls related to that resource:
src/app/services/
├── posts.ts ← all post-related API calls
├── users.ts ← all user-related API calls
├── auth.ts ← login, logout, token management
├── comments.ts ← all comment-related API calls
└── products.ts ← all product-related API calls
This keeps each service focused and easy to understand.
8.2 — A Complete, Well-Organized API Service
Here is what a complete, production-quality service looks like:
src/app/services/users.ts:
import { Injectable, inject } from '@angular/core';
import { HttpClient, HttpParams, HttpErrorResponse } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError, map } from 'rxjs/operators';
import { environment } from '../../environments/environment';
export interface User {
id: number;
name: string;
email: string;
phone: string;
website: string;
company: {
name: string;
catchPhrase: string;
};
address: {
city: string;
street: string;
};
}
export interface CreateUserDto {
name: string;
email: string;
phone: string;
}
export interface UpdateUserDto {
name?: string;
email?: string;
phone?: string;
}
@Injectable({
providedIn: 'root'
})
export class Users {
private http = inject(HttpClient);
private baseUrl = `${environment.apiUrl}/users`;
getAll(): Observable<User[]> {
return this.http.get<User[]>(this.baseUrl).pipe(
catchError(this.handleError)
);
}
getById(id: number): Observable<User> {
return this.http.get<User>(`${this.baseUrl}/${id}`).pipe(
catchError(this.handleError)
);
}
search(name: string): Observable<User[]> {
const params = new HttpParams().set('name_like', name);
return this.http.get<User[]>(this.baseUrl, { params }).pipe(
catchError(this.handleError)
);
}
create(userData: CreateUserDto): Observable<User> {
return this.http.post<User>(this.baseUrl, userData).pipe(
catchError(this.handleError)
);
}
update(id: number, changes: UpdateUserDto): Observable<User> {
return this.http.patch<User>(`${this.baseUrl}/${id}`, changes).pipe(
catchError(this.handleError)
);
}
delete(id: number): Observable<void> {
return this.http.delete<void>(`${this.baseUrl}/${id}`).pipe(
catchError(this.handleError)
);
}
private handleError(error: HttpErrorResponse): Observable<never> {
const message = error.status === 0
? 'Network error. Please check your connection.'
: `Server error ${error.status}: ${error.message}`;
return throwError(() => new Error(message));
}
}
This service is clean and predictable. Each method does exactly one thing. All methods are typed. Error handling is centralized. Any component that injects this service gets a consistent, reliable interface.
Chapter 9 — Putting It All Together — A Complete App
Let's build a complete User Management Dashboard that demonstrates everything from this phase — GET, POST, PATCH, DELETE, loading states, error handling, interceptors, and environment configuration.
Project Setup
ng new user-dashboard --style=css
cd user-dashboard
ng g s services/users --skip-tests
ng g s services/loading --skip-tests
ng g s services/notifications --skip-tests
ng g interceptor interceptors/loading --skip-tests
ng g interceptor interceptors/error --skip-tests
ng g c pages/users/user-list --skip-tests
ng g c pages/users/user-detail --skip-tests
ng g c components/global-loader --skip-tests
ng g c components/toast --skip-tests
App Config with HttpClient and Interceptors
src/app/app.config.ts:
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { routes } from './app.routes';
import { loadingInterceptor } from './interceptors/loading';
import { errorInterceptor } from './interceptors/error';
export const appConfig: ApplicationConfig = {
providers: [
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes),
provideHttpClient(
withInterceptors([loadingInterceptor, errorInterceptor])
)
]
};
Environment Files
src/environments/environment.ts:
export const environment = {
production: false,
apiUrl: 'https://jsonplaceholder.typicode.com'
};
Loading Service
src/app/services/loading.ts:
import { Injectable, signal, computed } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class Loading {
private count = signal(0);
readonly isLoading = computed(() => this.count() > 0);
increment() { this.count.update(n => n + 1); }
decrement() { this.count.update(n => Math.max(0, n - 1)); }
}
Loading Interceptor
src/app/interceptors/loading.ts:
import { HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
import { finalize } from 'rxjs/operators';
import { Loading } from '../services/loading';
export const loadingInterceptor: HttpInterceptorFn = (req, next) => {
const loading = inject(Loading);
loading.increment();
return next(req).pipe(finalize(() => loading.decrement()));
};
Error Interceptor
src/app/interceptors/error.ts:
import { HttpInterceptorFn, HttpErrorResponse } from '@angular/common/http';
import { inject } from '@angular/core';
import { catchError, throwError } from 'rxjs';
import { Notifications } from '../services/notifications';
export const errorInterceptor: HttpInterceptorFn = (req, next) => {
const notifications = inject(Notifications);
return next(req).pipe(
catchError((error: HttpErrorResponse) => {
if (error.status === 0) {
notifications.error('Network error. Please check your connection.');
} else if (error.status >= 500) {
notifications.error(`Server error ${error.status}. Please try again later.`);
}
return throwError(() => error);
})
);
};
Notifications Service
src/app/services/notifications.ts:
import { Injectable, signal } from '@angular/core';
export interface ToastMessage {
id: number;
text: string;
type: 'success' | 'error' | 'info';
}
@Injectable({ providedIn: 'root' })
export class Notifications {
private list = signal<ToastMessage[]>([]);
readonly messages = this.list.asReadonly();
private nextId = 1;
show(text: string, type: ToastMessage['type'] = 'info'): void {
const id = this.nextId++;
this.list.update(msgs => [...msgs, { id, text, type }]);
setTimeout(() => this.remove(id), 3500);
}
success(text: string) { this.show(text, 'success'); }
error(text: string) { this.show(text, 'error'); }
info(text: string) { this.show(text, 'info'); }
remove(id: number): void {
this.list.update(msgs => msgs.filter(m => m.id !== id));
}
}
Users Service
src/app/services/users.ts:
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { environment } from '../../environments/environment';
export interface User {
id: number;
name: string;
email: string;
phone: string;
website: string;
company: { name: string };
address: { city: string };
}
@Injectable({ providedIn: 'root' })
export class Users {
private http = inject(HttpClient);
private url = `${environment.apiUrl}/users`;
getAll(): Observable<User[]> {
return this.http.get<User[]>(this.url);
}
getById(id: number): Observable<User> {
return this.http.get<User>(`${this.url}/${id}`);
}
create(data: Partial<User>): Observable<User> {
return this.http.post<User>(this.url, data);
}
update(id: number, data: Partial<User>): Observable<User> {
return this.http.patch<User>(`${this.url}/${id}`, data);
}
delete(id: number): Observable<void> {
return this.http.delete<void>(`${this.url}/${id}`);
}
}
Toast Component
src/app/components/toast/toast.ts:
import { Component, inject } from '@angular/core';
import { NgClass } from '@angular/common';
import { Notifications } from '../../services/notifications';
@Component({
selector: 'app-toast',
imports: [NgClass],
template: `
<div class="toast-container">
@for (msg of notifications.messages(); track msg.id) {
<div class="toast" [ngClass]="'toast-' + msg.type">
{{ msg.text }}
<button (click)="notifications.remove(msg.id)">×</button>
</div>
}
</div>
`,
styles: [`
.toast-container {
position: fixed; top: 20px; right: 20px;
z-index: 9999; display: flex; flex-direction: column; gap: 10px;
}
.toast {
display: flex; align-items: center; justify-content: space-between;
padding: 14px 18px; border-radius: 8px; font-size: 14px; font-weight: 500;
min-width: 280px; box-shadow: 0 4px 16px rgba(0,0,0,0.12);
animation: slideIn 0.3s ease;
}
@keyframes slideIn {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
.toast-success { background: #d4edda; color: #155724; border-left: 4px solid #28a745; }
.toast-error { background: #f8d7da; color: #721c24; border-left: 4px solid #dc3545; }
.toast-info { background: #d1ecf1; color: #0c5460; border-left: 4px solid #17a2b8; }
button {
background: none; border: none; font-size: 18px;
cursor: pointer; color: inherit; margin-left: 12px; opacity: 0.7;
}
button:hover { opacity: 1; }
`]
})
export class Toast {
notifications = inject(Notifications);
}
Global Loader Component
src/app/components/global-loader/global-loader.ts:
import { Component, inject } from '@angular/core';
import { Loading } from '../../services/loading';
@Component({
selector: 'app-global-loader',
imports: [],
template: `
@if (loading.isLoading()) {
<div class="loader-bar"></div>
}
`,
styles: [`
.loader-bar {
position: fixed; top: 0; left: 0; right: 0; height: 3px;
background: linear-gradient(90deg, #0070f3, #64ffda);
z-index: 9999;
animation: load 1.2s ease-in-out infinite;
}
@keyframes load {
0% { transform: translateX(-100%); }
100% { transform: translateX(100%); }
}
`]
})
export class GlobalLoader {
loading = inject(Loading);
}
User List Page
src/app/pages/users/user-list/user-list.ts:
import { Component, inject, OnInit, signal } from '@angular/core';
import { Router } from '@angular/router';
import { ReactiveFormsModule, FormBuilder } from '@angular/forms';
import { Users, User } from '../../../services/users';
import { Notifications } from '../../../services/notifications';
@Component({
selector: 'app-user-list',
imports: [ReactiveFormsModule],
templateUrl: './user-list.html',
styleUrl: './user-list.css'
})
export class UserList implements OnInit {
private usersService = inject(Users);
private notifications = inject(Notifications);
private router = inject(Router);
private fb = inject(FormBuilder);
users = signal<User[]>([]);
isLoading = signal(false);
error = signal('');
showCreateForm = signal(false);
deletingId = signal<number | null>(null);
createForm = this.fb.group({
name: [''],
email: [''],
phone: ['']
});
ngOnInit(): void {
this.loadUsers();
}
loadUsers(): void {
this.isLoading.set(true);
this.error.set('');
this.usersService.getAll().subscribe({
next: (data) => {
this.users.set(data);
this.isLoading.set(false);
},
error: (err) => {
this.error.set('Failed to load users. Please try again.');
this.isLoading.set(false);
}
});
}
viewUser(id: number): void {
this.router.navigate(['/users', id]);
}
createUser(): void {
const formValue = this.createForm.value;
if (!formValue.name || !formValue.email) return;
this.usersService.create(formValue).subscribe({
next: (newUser) => {
this.users.update(list => [...list, newUser]);
this.showCreateForm.set(false);
this.createForm.reset();
this.notifications.success(`User "${newUser.name}" created successfully!`);
},
error: () => {
this.notifications.error('Failed to create user.');
}
});
}
deleteUser(user: User, event: Event): void {
event.stopPropagation(); // prevent triggering viewUser
if (!confirm(`Delete user "${user.name}"?`)) return;
this.deletingId.set(user.id);
this.usersService.delete(user.id).subscribe({
next: () => {
this.users.update(list => list.filter(u => u.id !== user.id));
this.deletingId.set(null);
this.notifications.success(`User "${user.name}" deleted.`);
},
error: () => {
this.deletingId.set(null);
this.notifications.error('Failed to delete user.');
}
});
}
}
src/app/pages/users/user-list/user-list.html:
<div class="user-list-page">
<div class="page-header">
<div>
<h1>Users</h1>
<p class="subtitle">{{ users().length }} total users</p>
</div>
<button class="btn-primary" (click)="showCreateForm.set(!showCreateForm())">
{{ showCreateForm() ? 'Cancel' : '+ Add User' }}
</button>
</div>
@if (showCreateForm()) {
<div class="create-form-card" [formGroup]="createForm">
<h3>Create New User</h3>
<div class="form-row">
<input type="text" formControlName="name" placeholder="Full Name">
<input type="email" formControlName="email" placeholder="Email">
<input type="tel" formControlName="phone" placeholder="Phone">
<button class="btn-primary" (click)="createUser()">Create</button>
</div>
</div>
}
@if (isLoading()) {
<div class="loading-state">
<div class="spinner"></div>
<p>Loading users...</p>
</div>
}
@if (error()) {
<div class="error-state">
<p>{{ error() }}</p>
<button (click)="loadUsers()">Try Again</button>
</div>
}
@if (!isLoading() && !error()) {
<div class="user-grid">
@for (user of users(); track user.id) {
<div class="user-card" (click)="viewUser(user.id)">
<div class="user-avatar">{{ user.name[0] }}</div>
<div class="user-info">
<h3>{{ user.name }}</h3>
<p class="email">{{ user.email }}</p>
<p class="company">{{ user.company.name }}</p>
<p class="city">📍 {{ user.address.city }}</p>
</div>
<button
class="delete-btn"
(click)="deleteUser(user, $event)"
[disabled]="deletingId() === user.id">
{{ deletingId() === user.id ? '...' : '🗑' }}
</button>
</div>
}
</div>
}
</div>
src/app/pages/users/user-list/user-list.css:
.user-list-page {
max-width: 1000px;
margin: 0 auto;
padding: 40px 24px;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 32px;
}
h1 {
font-size: 32px;
font-weight: 800;
color: #1a1a2e;
margin-bottom: 4px;
}
.subtitle { color: #888; font-size: 14px; }
.btn-primary {
background: #0070f3;
color: white;
border: none;
padding: 10px 20px;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
}
.btn-primary:hover { background: #005ac1; }
.create-form-card {
background: white;
padding: 24px;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
margin-bottom: 28px;
border: 2px solid #e8f4ff;
}
.create-form-card h3 {
font-size: 16px;
color: #1a1a2e;
margin-bottom: 16px;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr 1fr auto;
gap: 12px;
align-items: center;
}
.form-row input {
padding: 10px 14px;
border: 1px solid #ddd;
border-radius: 8px;
font-size: 14px;
}
.form-row input:focus { outline: none; border-color: #0070f3; }
.loading-state {
display: flex;
flex-direction: column;
align-items: center;
padding: 64px;
gap: 16px;
color: #888;
}
.spinner {
width: 40px; height: 40px;
border: 3px solid #e0e0e0;
border-top-color: #0070f3;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.error-state {
background: #fef2f2;
border: 1px solid #fecaca;
border-radius: 12px;
padding: 24px;
text-align: center;
color: #dc2626;
}
.error-state button {
margin-top: 12px;
background: #dc2626;
color: white;
border: none;
padding: 8px 20px;
border-radius: 8px;
cursor: pointer;
}
.user-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 16px;
}
.user-card {
background: white;
border-radius: 12px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0,0,0,0.07);
cursor: pointer;
transition: all 0.2s;
display: flex;
gap: 16px;
align-items: flex-start;
position: relative;
}
.user-card:hover {
box-shadow: 0 6px 24px rgba(0,0,0,0.12);
transform: translateY(-2px);
}
.user-avatar {
width: 48px; height: 48px;
background: linear-gradient(135deg, #0070f3, #64ffda);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 20px;
font-weight: 700;
flex-shrink: 0;
}
.user-info { flex: 1; min-width: 0; }
.user-info h3 {
font-size: 15px;
font-weight: 700;
color: #1a1a2e;
margin-bottom: 4px;
}
.email, .company, .city {
font-size: 13px;
color: #888;
margin-bottom: 2px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.delete-btn {
background: none;
border: none;
font-size: 16px;
cursor: pointer;
padding: 4px;
opacity: 0.4;
transition: opacity 0.2s;
}
.delete-btn:hover { opacity: 1; }
.delete-btn:disabled { cursor: not-allowed; }
User Detail Page
src/app/pages/users/user-detail/user-detail.ts:
import { Component, inject, OnInit, signal } from '@angular/core';
import { ActivatedRoute, RouterLink } from '@angular/router';
import { ReactiveFormsModule, FormBuilder } from '@angular/forms';
import { Users, User } from '../../../services/users';
import { Notifications } from '../../../services/notifications';
@Component({
selector: 'app-user-detail',
imports: [RouterLink, ReactiveFormsModule],
templateUrl: './user-detail.html',
styleUrl: './user-detail.css'
})
export class UserDetail implements OnInit {
private route = inject(ActivatedRoute);
private usersService = inject(Users);
private notifications = inject(Notifications);
private fb = inject(FormBuilder);
user = signal<User | null>(null);
isLoading = signal(true);
isEditing = signal(false);
isSaving = signal(false);
editForm = this.fb.group({
name: [''],
email: [''],
phone: [''],
website: ['']
});
ngOnInit(): void {
const id = Number(this.route.snapshot.params['id']);
this.loadUser(id);
}
loadUser(id: number): void {
this.usersService.getById(id).subscribe({
next: (data) => {
this.user.set(data);
this.editForm.patchValue({
name: data.name,
email: data.email,
phone: data.phone,
website: data.website
});
this.isLoading.set(false);
},
error: () => {
this.isLoading.set(false);
this.notifications.error('Failed to load user.');
}
});
}
startEditing(): void {
this.isEditing.set(true);
}
cancelEditing(): void {
const u = this.user();
if (u) {
this.editForm.patchValue({
name: u.name, email: u.email,
phone: u.phone, website: u.website
});
}
this.isEditing.set(false);
}
saveChanges(): void {
const userId = this.user()?.id;
if (!userId) return;
this.isSaving.set(true);
this.usersService.update(userId, this.editForm.value).subscribe({
next: (updated) => {
this.user.set(updated);
this.isEditing.set(false);
this.isSaving.set(false);
this.notifications.success('User updated successfully!');
},
error: () => {
this.isSaving.set(false);
this.notifications.error('Failed to update user.');
}
});
}
}
src/app/pages/users/user-detail/user-detail.html:
<div class="user-detail">
<a routerLink="/users" class="back-link">← Back to Users</a>
@if (isLoading()) {
<div class="loading">
<div class="spinner"></div>
</div>
}
@if (user()) {
<div class="detail-card">
<div class="detail-header">
<div class="big-avatar">{{ user()!.name[0] }}</div>
<div class="header-info">
@if (isEditing()) {
<input [formControl]="editForm.get('name')!" type="text" class="edit-input name-input">
} @else {
<h1>{{ user()!.name }}</h1>
}
<p class="company">{{ user()!.company.name }}</p>
</div>
<div class="header-actions">
@if (isEditing()) {
<button class="btn-save" (click)="saveChanges()" [disabled]="isSaving()">
{{ isSaving() ? 'Saving...' : 'Save Changes' }}
</button>
<button class="btn-cancel" (click)="cancelEditing()">Cancel</button>
} @else {
<button class="btn-edit" (click)="startEditing()">Edit Profile</button>
}
</div>
</div>
<div class="detail-body" [formGroup]="editForm">
<div class="info-grid">
<div class="info-item">
<label>Email</label>
@if (isEditing()) {
<input type="email" formControlName="email" class="edit-input">
} @else {
<p>{{ user()!.email }}</p>
}
</div>
<div class="info-item">
<label>Phone</label>
@if (isEditing()) {
<input type="tel" formControlName="phone" class="edit-input">
} @else {
<p>{{ user()!.phone }}</p>
}
</div>
<div class="info-item">
<label>Website</label>
@if (isEditing()) {
<input type="text" formControlName="website" class="edit-input">
} @else {
<p>{{ user()!.website }}</p>
}
</div>
<div class="info-item">
<label>City</label>
<p>{{ user()!.address.city }}</p>
</div>
</div>
</div>
</div>
}
</div>
src/app/pages/users/user-detail/user-detail.css:
.user-detail {
max-width: 800px;
margin: 0 auto;
padding: 40px 24px;
}
.back-link {
display: inline-block;
color: #0070f3;
text-decoration: none;
font-size: 14px;
font-weight: 600;
margin-bottom: 28px;
}
.loading {
display: flex;
justify-content: center;
padding: 64px;
}
.spinner {
width: 40px; height: 40px;
border: 3px solid #e0e0e0;
border-top-color: #0070f3;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.detail-card {
background: white;
border-radius: 16px;
box-shadow: 0 4px 24px rgba(0,0,0,0.08);
overflow: hidden;
}
.detail-header {
display: flex;
align-items: center;
gap: 20px;
padding: 28px 32px;
background: linear-gradient(135deg, #0a192f, #112240);
}
.big-avatar {
width: 72px; height: 72px;
background: linear-gradient(135deg, #0070f3, #64ffda);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 32px;
font-weight: 700;
flex-shrink: 0;
}
.header-info { flex: 1; }
.header-info h1 {
font-size: 24px;
font-weight: 700;
color: white;
margin-bottom: 4px;
}
.company { color: #8892b0; font-size: 14px; }
.header-actions { display: flex; gap: 10px; }
.btn-edit {
background: rgba(255,255,255,0.1);
color: white;
border: 1px solid rgba(255,255,255,0.2);
padding: 8px 18px;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
}
.btn-edit:hover { background: rgba(255,255,255,0.2); }
.btn-save {
background: #28a745;
color: white;
border: none;
padding: 8px 18px;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
}
.btn-save:disabled { background: #ccc; cursor: not-allowed; }
.btn-cancel {
background: none;
color: #ccc;
border: 1px solid rgba(255,255,255,0.2);
padding: 8px 18px;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
}
.detail-body { padding: 32px; }
.info-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
}
.info-item label {
display: block;
font-size: 12px;
font-weight: 700;
color: #888;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 6px;
}
.info-item p {
font-size: 15px;
color: #1a1a2e;
}
.edit-input {
width: 100%;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 15px;
box-sizing: border-box;
}
.edit-input:focus { outline: none; border-color: #0070f3; }
.name-input {
background: rgba(255,255,255,0.1);
border-color: rgba(255,255,255,0.3);
color: white;
font-size: 20px;
font-weight: 700;
}
Routes
src/app/app.routes.ts:
import { Routes } from '@angular/router';
export const routes: Routes = [
{
path: '',
redirectTo: 'users',
pathMatch: 'full'
},
{
path: 'users',
loadComponent: () => import('./pages/users/user-list/user-list').then(m => m.UserList)
},
{
path: 'users/:id',
loadComponent: () => import('./pages/users/user-detail/user-detail').then(m => m.UserDetail)
}
];
App Component
src/app/app.ts:
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { GlobalLoader } from './components/global-loader/global-loader';
import { Toast } from './components/toast/toast';
@Component({
selector: 'app-root',
imports: [RouterOutlet, GlobalLoader, Toast],
template: `
<app-global-loader></app-global-loader>
<app-toast></app-toast>
<router-outlet></router-outlet>
`
})
export class App { }
src/styles.css:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
body {
background: #f0f2f5;
min-height: 100vh;
}
Run ng serve -o. You now have a complete User Management Dashboard that:
Fetches all 10 users from the JSONPlaceholder API on load. Shows a loading spinner and a top loading bar during every API call — powered by the loading interceptor. Displays users in a responsive card grid. Lets you view a user's full details on a separate page using route parameters. Lets you edit a user's details with a PATCH request. Lets you delete a user with a DELETE request. Shows success and error toast notifications for every action — with error toasts automatically shown by the error interceptor for server errors. Creates a new user with a POST request. All route code is lazy loaded.
Phase 8 — Complete Summary
Here is everything you learned in this phase.
What HTTP is — the protocol for communication between your Angular app and a server. REST APIs use GET, POST, PUT, PATCH, DELETE to create, read, update, and delete data.
Setting up HttpClient — provideHttpClient() in app.config.ts makes HttpClient available everywhere. Inject it with inject(HttpClient).
Making requests — http.get<T>(url) fetches data. http.post<T>(url, body) creates. http.put<T>(url, body) replaces. http.patch<T>(url, changes) updates specific fields. http.delete<T>(url) removes. All return Observables. Nothing happens until you .subscribe().
The three states — every HTTP call has loading, success, and error states. Handle all three in your components with isLoading, data, and error signals.
Error handling — HttpErrorResponse in the subscribe error callback. status === 0 is a network error. Other status codes are server errors. catchError in the service for centralized handling.
HTTP Headers — HttpHeaders for adding headers to specific requests. Immutable — use .set() to create new headers.
Query params — HttpParams for building query strings. Immutable — chain .set() calls.
Interceptors — functions that run for every HTTP request and response. HttpInterceptorFn is the modern function-based approach. Registered with withInterceptors([...]). Use for auth tokens, loading indicators, and global error handling. request.clone() to create a modified copy of a request.
Environment variables — environment.ts for development, environment.prod.ts for production. Configure file replacement in angular.json. Import environment directly in services.
Organizing services — one service per API resource. Define TypeScript interfaces for your data shapes. Centralize error handling in the service with catchError.
What's Next — Phase 9
In Phase 9 we go deep into RxJS and Observables — the reactive programming layer that powers everything HTTP in Angular:
What Observables are and why they exist. Creating Observables with of, from, interval, fromEvent. The difference between cold and hot Observables. Subjects and BehaviorSubjects for sharing state. The most important operators — map, filter, switchMap, mergeMap, debounceTime, distinctUntilChanged, catchError, forkJoin, combineLatest. Real patterns you will use constantly — search with debounce, chaining API calls, combining multiple requests.
No comments:
Post a Comment