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.

  1. Create the shared/models directory:

    mkdir -p src/app/shared/models
    
  2. Create user.interface.ts: Open src/app/shared/models/user.interface.ts and 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 User interface with id, name, email, and role.
  • id is marked as optional (id?) because when we create a new user, the id will be generated by the backend (our JSON Server).
  • NewUser type is derived from User using Omit utility type to explicitly state that id is 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.

  1. Create the core/services directory:

    mkdir -p src/app/core/services
    
  2. Generate the UserService:

    ng generate service core/services/user --skip-tests
    
  3. Implement UserService: Open src/app/core/services/user.service.ts and 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 our UserService a singleton and registers it with the root injector, meaning it’s available throughout the entire application.
  • private http = inject(HttpClient);: Injects HttpClient. Remember, in Angular v21, HttpClient is available by default!
  • private apiUrl: Our base URL for the JSON Server users endpoint.
  • _users: BehaviorSubject<User[]>: We use a BehaviorSubject to manage the list of users.
    • BehaviorSubject is 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 public Observable derived from _users so components can subscribe to user list changes without being able to directly modify the BehaviorSubject.
  • fetchUsers(): Retrieves all users from the API.
    • It updates loadingUsers and errorLoadingUsers BehaviorSubject to 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.
  • addUser(): Sends a POST request to create a new user.
    • After a successful creation, we optimistically update our local _users BehaviorSubject with the new user, so the UI updates immediately.
  • handleError(): A private utility method to log and re-throw HTTP errors consistently.

Step 3: Start Your Mock API and Angular App

  1. Start JSON Server (in one terminal):

    npm run serve:json-api
    
  2. Start 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 User interface and NewUser type for our application data.
  • We created a UserService responsible for all API communication with our mock backend.
  • The UserService leverages Angular v21’s default HttpClient without additional configuration.
  • It uses a BehaviorSubject (_users) to manage the global list of users, allowing components to react to updates.
  • We also added BehaviorSubject for loadingUsers and errorLoadingUsers to handle application state feedback.
  • Error handling is centralized using the catchError and throwError RxJS 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.