Welcome to the first coding chapter of our User Management Application project! We’ll start by establishing the foundational elements: the data model for a user and a service to handle all communication with our (mock) backend API.
This chapter directly applies our understanding of Angular’s new HttpClient default and best practices for creating services.
Step 1: Define the User Interface
First, let’s define what a User looks like in our application. This promotes type safety throughout our code.
Create the
shared/modelsdirectory:mkdir -p src/app/shared/modelsCreate
user.interface.ts: Opensrc/app/shared/models/user.interface.tsand add:// src/app/shared/models/user.interface.ts export interface User { id?: number; // Optional because the backend will assign it for new users name: string; email: string; role: 'admin' | 'user' | 'guest'; // Example roles } // For creating a new user, id is not required export type NewUser = Omit<User, 'id'>;
Explanation:
- We define a
Userinterface withid,name,email, androle. idis marked as optional (id?) because when we create a new user, theidwill be generated by the backend (our JSON Server).NewUsertype is derived fromUserusingOmitutility type to explicitly state thatidis not present when creating. This is good for type safety when sending data.
Step 2: Create the User Service
Next, we’ll create a UserService that encapsulates all the logic for interacting with our user API endpoint. This service will use HttpClient to make requests to our json-server.
Create the
core/servicesdirectory:mkdir -p src/app/core/servicesGenerate the
UserService:ng generate service core/services/user --skip-testsImplement
UserService: Opensrc/app/core/services/user.service.tsand replace its content with:// src/app/core/services/user.service.ts import { Injectable, inject } from '@angular/core'; import { HttpClient, HttpErrorResponse } from '@angular/common/http'; import { Observable, catchError, throwError, tap, BehaviorSubject } from 'rxjs'; import { User, NewUser } from '../../shared/models/user.interface'; // Import our User interface @Injectable({ providedIn: 'root', // Make the service a singleton and available throughout the app }) export class UserService { // Angular v21: HttpClient is provided by default, no need for provideHttpClient() in app.config.ts! private http = inject(HttpClient); private apiUrl = 'http://localhost:3000/users'; // Our JSON Server endpoint // Using a BehaviorSubject to hold the current list of users // This allows components to subscribe and react to changes in the user list private _users = new BehaviorSubject<User[]>([]); public readonly users$ = this._users.asObservable(); // Expose as Observable for components // Using a signal to hold a loading state, this will be particularly useful in zoneless context loadingUsers = new BehaviorSubject<boolean>(false); errorLoadingUsers = new BehaviorSubject<string | null>(null); constructor() { // Initial load of users when the service is instantiated this.fetchUsers(); } /** * Fetches all users from the API and updates the _users BehaviorSubject. */ fetchUsers(): void { this.loadingUsers.next(true); this.errorLoadingUsers.next(null); this.http.get<User[]>(this.apiUrl) .pipe( tap((users) => { // Update the BehaviorSubject with the fetched users this._users.next(users); this.loadingUsers.next(false); }), catchError(this.handleError) ) .subscribe({ next: () => console.log('Users fetched and updated.'), error: (err) => { this.errorLoadingUsers.next('Failed to load users. Please try again.'); this.loadingUsers.next(false); console.error('Error fetching users:', err); } }); } /** * Adds a new user to the API. * @param user The new user data. * @returns An Observable of the newly created user. */ addUser(newUser: NewUser): Observable<User> { this.loadingUsers.next(true); return this.http.post<User>(this.apiUrl, newUser) .pipe( tap((createdUser) => { // Optimistically update the local user list const currentUsers = this._users.getValue(); this._users.next([...currentUsers, createdUser]); this.loadingUsers.next(false); }), catchError(this.handleError) ); } /** * Handles HTTP errors. * @param error The HttpErrorResponse. * @returns An Observable that re-throws the error. */ private handleError(error: HttpErrorResponse): Observable<never> { let errorMessage = 'An unknown error occurred!'; if (error.error instanceof ErrorEvent) { // Client-side errors errorMessage = `Error: ${error.error.message}`; } else { // Backend errors errorMessage = `Server Error Code: ${error.status}\nMessage: ${error.message}`; } console.error(errorMessage); return throwError(() => new Error(errorMessage)); } }
Explanation:
@Injectable({ providedIn: 'root' }): This makes ourUserServicea singleton and registers it with the root injector, meaning it’s available throughout the entire application.private http = inject(HttpClient);: InjectsHttpClient. Remember, in Angular v21,HttpClientis available by default!private apiUrl: Our base URL for the JSON Server users endpoint._users: BehaviorSubject<User[]>: We use aBehaviorSubjectto manage the list of users.BehaviorSubjectis an observable that stores the latest value emitted to its consumers, and it always provides that value upon subscription. This is great for state management where you want components to get the current state immediately when they subscribe.users$(.asObservable()): We expose a publicObservablederived from_usersso components can subscribe to user list changes without being able to directly modify theBehaviorSubject.
fetchUsers(): Retrieves all users from the API.- It updates
loadingUsersanderrorLoadingUsersBehaviorSubjectto provide feedback. tap()operator is used to perform side effects (like updating_users) without modifying the observable stream.- The fetched users are pushed into
_users.next(users). catchError()handles any HTTP errors.
- It updates
addUser(): Sends aPOSTrequest to create a new user.- After a successful creation, we optimistically update our local
_usersBehaviorSubjectwith the new user, so the UI updates immediately.
- After a successful creation, we optimistically update our local
handleError(): A private utility method to log and re-throw HTTP errors consistently.
Step 3: Start Your Mock API and Angular App
Start JSON Server (in one terminal):
npm run serve:json-apiStart Angular Development Server (in another terminal):
ng serve
At this point, you won’t see anything yet, as we haven’t created any components to display the users. But your UserService is initialized and will attempt to fetch users from the JSON Server. You can check your browser’s console (F12) to see the “Users fetched and updated.” message from the UserService.
Mini-Challenge: Add a getUserById Method
Extend the UserService by adding a method getUserById(id: number): Observable<User> that fetches a single user from the json-server API (http://localhost:3000/users/:id).
// HINT: Add to UserService
/**
* Fetches a single user by ID.
* @param id The ID of the user to fetch.
* @returns An Observable of the user.
*/
getUserById(id: number): Observable<User> {
const url = `${this.apiUrl}/${id}`;
return this.http.get<User>(url)
.pipe(
catchError(this.handleError)
);
}
Summary/Key Takeaways
- We’ve defined a type-safe
Userinterface andNewUsertype for our application data. - We created a
UserServiceresponsible for all API communication with our mock backend. - The
UserServiceleverages Angular v21’s defaultHttpClientwithout additional configuration. - It uses a
BehaviorSubject(_users) to manage the global list of users, allowing components to react to updates. - We also added
BehaviorSubjectforloadingUsersanderrorLoadingUsersto handle application state feedback. - Error handling is centralized using the
catchErrorandthrowErrorRxJS operators.
With our user model and service in place, we’re ready to start building components to display and interact with this data! In the next chapter, we’ll create the UserListComponent to show our list of users.