๐ฏ Use Case: UserService
Goal: Create a service that:
-
Fetches user profile from an API
-
Updates the user’s profile
-
Calculates the user's age
-
Caches the user data in a
BehaviorSubject
for shared use -
Handles errors cleanly
-
Is testable and easy to maintain
๐ฆ 1. Define the User Model
// models/user.model.ts
export interface User {
id: string;
name: string;
email: string;
dateOfBirth: string; // ISO date string
phone?: string;
}
๐ ️ 2. Create the UserService
// services/user.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { BehaviorSubject, Observable, catchError, map, of, throwError } from 'rxjs';
import { User } from '../models/user.model';
@Injectable({
providedIn: 'root'
})
export class UserService {
private userSubject = new BehaviorSubject<User | null>(null);
public user$ = this.userSubject.asObservable();
private readonly API_URL = '/api/users';
constructor(private http: HttpClient) {}
/**
* Loads user from backend and updates local state
*/
loadUser(userId: string): Observable<User> {
return this.http.get<User>(`${this.API_URL}/${userId}`).pipe(
map(user => {
this.userSubject.next(user);
return user;
}),
catchError(error => {
console.error('Error loading user', error);
return throwError(() => new Error('User load failed'));
})
);
}
/**
* Returns current user snapshot (not observable)
*/
getCurrentUser(): User | null {
return this.userSubject.value;
}
/**
* Updates user and refreshes local cache
*/
updateUser(user: Partial<User>): Observable<User> {
const currentUser = this.getCurrentUser();
if (!currentUser) return throwError(() => new Error('No user loaded'));
return this.http.put<User>(`${this.API_URL}/${currentUser.id}`, user).pipe(
map(updatedUser => {
this.userSubject.next(updatedUser);
return updatedUser;
}),
catchError(error => {
console.error('Error updating user', error);
return throwError(() => new Error('User update failed'));
})
);
}
/**
* Calculates age from dateOfBirth
*/
calculateAge(dateOfBirth: string): number {
const dob = new Date(dateOfBirth);
const today = new Date();
let age = today.getFullYear() - dob.getFullYear();
const m = today.getMonth() - dob.getMonth();
if (m < 0 || (m === 0 && today.getDate() < dob.getDate())) {
age--;
}
return age;
}
}
๐ 3. How a Component Would Use It
// profile.component.ts
export class ProfileComponent implements OnInit {
user$ = this.userService.user$;
age: number | null = null;
constructor(private userService: UserService) {}
ngOnInit() {
this.userService.loadUser('12345').subscribe(user => {
this.age = this.userService.calculateAge(user.dateOfBirth);
});
}
updatePhoneNumber(newPhone: string) {
this.userService.updateUser({ phone: newPhone }).subscribe(updated => {
console.log('Updated:', updated);
});
}
}
✅ Why This Is High-Level and Clean
Feature | Benefit |
---|---|
✅ BehaviorSubject caching | State is shareable and reactive across components |
✅ Separation of logic | API, business logic (age calc), and UI are well-separated |
✅ Clear, meaningful methods | Easy to understand and self-documenting |
✅ Centralized error handling | Errors managed at the service layer, reducing repetition |
✅ Testable | Pure functions (calculateAge ) and DI-based logic are easy to unit test |
๐งช 4. Test Example (for calculateAge
)
it('should calculate correct age', () => {
const service = new UserService({} as any); // mock HttpClient
const age = service.calculateAge('2000-01-01');
const thisYear = new Date().getFullYear();
expect(age).toBe(thisYear - 2000);
});
๐ Final Thoughts
This service is:
-
✅ Modular
-
✅ Testable
-
✅ Scalable
-
✅ Follows Angular and SOLID best practices
Advanced UserService
with Role-Based Access and Token Auth
We'll build on the original UserService
to include:
✅ Features:
-
JWT token integration (e.g., get token from
AuthService
) -
Role-based logic (
isAdmin()
,hasRole()
) -
Guard-ready helper methods
-
Secure user update with token header
๐ Project Structure Update
src/
├── models/
│ └── user.model.ts
├── services/
│ ├── auth.service.ts ✅ New
│ └── user.service.ts
๐ค Updated User Model with Roles
// models/user.model.ts
export interface User {
id: string;
name: string;
email: string;
dateOfBirth: string;
roles: string[]; // ['user', 'admin', ...]
token?: string; // Optional JWT token
}
๐ AuthService – Manages JWT Token
// services/auth.service.ts
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class AuthService {
private readonly TOKEN_KEY = 'auth_token';
setToken(token: string) {
localStorage.setItem(this.TOKEN_KEY, token);
}
getToken(): string | null {
return localStorage.getItem(this.TOKEN_KEY);
}
clearToken() {
localStorage.removeItem(this.TOKEN_KEY);
}
isAuthenticated(): boolean {
return !!this.getToken();
}
}
๐ Updated Advanced UserService
// services/user.service.ts
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { BehaviorSubject, Observable, catchError, map, throwError } from 'rxjs';
import { User } from '../models/user.model';
import { AuthService } from './auth.service';
@Injectable({
providedIn: 'root'
})
export class UserService {
private userSubject = new BehaviorSubject<User | null>(null);
user$ = this.userSubject.asObservable();
private readonly API_URL = '/api/users';
constructor(private http: HttpClient, private auth: AuthService) {}
/**
* Load user and set token
*/
loadUser(userId: string): Observable<User> {
const headers = this.getAuthHeaders();
return this.http.get<User>(`${this.API_URL}/${userId}`, { headers }).pipe(
map(user => {
this.userSubject.next(user);
if (user.token) this.auth.setToken(user.token); // Store token if returned
return user;
}),
catchError(error => {
console.error('Error loading user', error);
return throwError(() => new Error('Failed to load user'));
})
);
}
getCurrentUser(): User | null {
return this.userSubject.value;
}
updateUser(data: Partial<User>): Observable<User> {
const currentUser = this.getCurrentUser();
if (!currentUser) return throwError(() => new Error('No user loaded'));
return this.http.put<User>(`${this.API_URL}/${currentUser.id}`, data, {
headers: this.getAuthHeaders()
}).pipe(
map(user => {
this.userSubject.next(user);
return user;
}),
catchError(error => {
console.error('Error updating user', error);
return throwError(() => new Error('Update failed'));
})
);
}
calculateAge(dob: string): number {
const birth = new Date(dob);
const today = new Date();
let age = today.getFullYear() - birth.getFullYear();
const m = today.getMonth() - birth.getMonth();
if (m < 0 || (m === 0 && today.getDate() < birth.getDate())) {
age--;
}
return age;
}
/**
* Checks if current user has a specific role
*/
hasRole(role: string): boolean {
const user = this.getCurrentUser();
return !!user?.roles.includes(role);
}
/**
* Shortcut for admin check
*/
isAdmin(): boolean {
return this.hasRole('admin');
}
/**
* Prepares auth headers using token
*/
private getAuthHeaders(): HttpHeaders {
const token = this.auth.getToken();
return new HttpHeaders({
'Authorization': `Bearer ${token ?? ''}`
});
}
}
๐ Bonus: Can Be Used in Guards or Route Conditions
// auth.guard.ts
canActivate(): boolean {
return this.userService.isAdmin(); // Only admins allowed
}
๐งช Optional Unit Test Example (hasRole)
it('should return true if user has role', () => {
const service = new UserService({} as any, {
getToken: () => 'token123'
} as any);
(service as any).userSubject.next({
id: '1',
name: 'Test',
email: '',
dateOfBirth: '',
roles: ['admin', 'user']
});
expect(service.hasRole('admin')).toBeTrue();
expect(service.hasRole('editor')).toBeFalse();
});
✅ Summary: What You Now Have
Feature | Benefit |
---|---|
✅ JWT integration | Secure authenticated requests |
✅ Role-based logic | Centralized access control helpers |
✅ BehaviorSubject state | Reactive and shared user state |
✅ Error handling | Safer and more maintainable |
✅ Easy to test and reuse | Services are self-contained, pure, and injectable |
⏭️ Want to Go Further?
Here are some next steps if you want:
-
✅ Add refresh token and token expiration logic
-
✅ Protect routes using
CanActivate
guards with role checks -
✅ Store state in NgRx or SignalStore (for larger apps)
-
✅ Add interceptors to automatically inject the token into all HTTP calls
-
✅ Implement logout flow
Let me know if you want help with any of those next steps — I can create examples too.
Great — let’s continue by adding the next advanced features one by one. Since you said yes, here’s what we’ll build next:
๐ Next Advanced Features:
-
✅ Token Auto-Injection using HTTP Interceptor
-
✅ Role-Based Route Protection using
AuthGuard
withCanActivate
-
✅ Logout and Session Expiry Handling
-
✅ (Optional) Token Refresh Flow (JWT refresh tokens)
Let’s tackle each part cleanly and step-by-step.
๐ 1. HTTP Interceptor – Auto-Add JWT to All Requests
✅ What It Does:
-
Automatically attaches
Authorization: Bearer <token>
header to every HTTP request -
Centralizes token logic instead of adding headers manually in each service
๐ฆ Create AuthInterceptor
// interceptors/auth.interceptor.ts
import { Injectable } from '@angular/core';
import {
HttpInterceptor,
HttpRequest,
HttpHandler,
HttpEvent
} from '@angular/common/http';
import { Observable } from 'rxjs';
import { AuthService } from '../services/auth.service';
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
constructor(private auth: AuthService) {}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const token = this.auth.getToken();
if (token) {
const cloned = req.clone({
setHeaders: {
Authorization: `Bearer ${token}`
}
});
return next.handle(cloned);
}
return next.handle(req);
}
}
๐ Register Interceptor in App Module
// app.module.ts
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { AuthInterceptor } from './interceptors/auth.interceptor';
@NgModule({
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: AuthInterceptor,
multi: true
}
]
})
export class AppModule {}
✅ Now all services (e.g., UserService
) no longer need to manually set headers.
๐ก️ 2. AuthGuard – Protect Routes by Role
✅ Goal:
Allow/deny access based on user role (like 'admin'
, 'editor'
, etc.)
๐ฆ Create AuthGuard
// guards/auth.guard.ts
import { Injectable } from '@angular/core';
import {
CanActivate,
ActivatedRouteSnapshot,
RouterStateSnapshot,
Router
} from '@angular/router';
import { UserService } from '../services/user.service';
@Injectable({
providedIn: 'root'
})
export class AuthGuard implements CanActivate {
constructor(private userService: UserService, private router: Router) {}
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
const requiredRole = route.data['role'] as string;
if (!this.userService.getCurrentUser()) {
this.router.navigate(['/login']);
return false;
}
if (requiredRole && !this.userService.hasRole(requiredRole)) {
this.router.navigate(['/unauthorized']);
return false;
}
return true;
}
}
๐งญ Usage in Routes
// app-routing.module.ts
const routes: Routes = [
{
path: 'admin',
component: AdminPanelComponent,
canActivate: [AuthGuard],
data: { role: 'admin' } // only admin can access
},
{
path: 'profile',
component: ProfileComponent,
canActivate: [AuthGuard]
}
];
๐ช 3. Logout and Session Expiry Handling
✅ AuthService: Add logout logic
logout() {
this.clearToken();
// Optionally clear other data like BehaviorSubjects
}
✅ UserService: Add logout and cleanup
logout() {
this.auth.clearToken();
this.userSubject.next(null);
}
⛔ Handle Token Expiry (Basic Way)
Inside AuthInterceptor
, detect expired token (e.g., 401 error) and redirect:
import { catchError } from 'rxjs/operators';
import { of, throwError } from 'rxjs';
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const token = this.auth.getToken();
const cloned = token
? req.clone({ setHeaders: { Authorization: `Bearer ${token}` } })
: req;
return next.handle(cloned).pipe(
catchError(err => {
if (err.status === 401) {
this.auth.clearToken();
this.router.navigate(['/login']);
}
return throwError(() => err);
})
);
}
๐ 4. (Optional) Refresh Token Flow
If your backend returns:
-
an access token (short-lived)
-
and a refresh token (longer-lived),
Then you can:
-
store the refresh token securely (e.g.,
HttpOnly cookie
) -
and use a token refresh endpoint (
/auth/refresh
) when access token expires
Let me know if you want me to implement that too — it’s more advanced and uses token queuing logic to prevent race conditions.
✅ Summary of What You Now Have
Feature | Description |
---|---|
✅ Auth Interceptor | Adds token to all HTTP requests automatically |
✅ Auth Guard | Protects routes based on login + roles |
✅ Role-based access | Easy to add .hasRole('admin') logic |
✅ Logout and cleanup | Centralized logout + session cleanup |
✅ Token Expiry handling | Detect 401s, auto-redirect to login |
⏭️ Want to Add Next?
Here are options you can choose:
-
๐ Implement refresh token flow
-
๐งช Write unit tests for the guards/interceptor
-
๐ก Show Signal-based global state instead of
BehaviorSubject
-
๐งฐ Build a TokenService to manage both access + refresh cleanly
Let me know what you'd like next — or if you'd prefer a GitHub project template version of all this.
Perfect — since you've said yes, let's now implement a full Refresh Token Flow (step 4), which is a more advanced and realistic solution for production-level Angular apps that use JWT authentication.
๐ What Is the Refresh Token Flow?
In modern JWT-based authentication:
-
๐ Access Token: short-lived (e.g. 5–15 mins)
-
♻️ Refresh Token: long-lived (e.g. 1–7 days), used to get a new access token when it expires
✅ Goal:
Automatically refresh the access token when it expires, without logging the user out or requiring a manual login.
๐งฑ What We'll Build:
-
✅
TokenService
: Stores and manages access/refresh tokens -
✅ Updated
AuthService
: Handles login, logout, and token refresh -
✅
AuthInterceptor
:-
Adds access token to all requests
-
Detects 401 errors
-
Refreshes token and retries request
-
-
✅ Backend expected endpoints:
-
POST /auth/login
-
POST /auth/refresh
with refresh token -
POST /auth/logout
-
1. ๐งฐ TokenService
// services/token.service.ts
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class TokenService {
private ACCESS = 'access_token';
private REFRESH = 'refresh_token';
setTokens(accessToken: string, refreshToken: string) {
localStorage.setItem(this.ACCESS, accessToken);
localStorage.setItem(this.REFRESH, refreshToken);
}
getAccessToken(): string | null {
return localStorage.getItem(this.ACCESS);
}
getRefreshToken(): string | null {
return localStorage.getItem(this.REFRESH);
}
clearTokens() {
localStorage.removeItem(this.ACCESS);
localStorage.removeItem(this.REFRESH);
}
}
2. ๐ AuthService
// services/auth.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { TokenService } from './token.service';
import { Observable, tap } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class AuthService {
private readonly API = '/api/auth';
constructor(private http: HttpClient, private tokenService: TokenService) {}
login(email: string, password: string): Observable<any> {
return this.http.post<any>(`${this.API}/login`, { email, password }).pipe(
tap(tokens => {
this.tokenService.setTokens(tokens.accessToken, tokens.refreshToken);
})
);
}
logout(): Observable<any> {
return this.http.post(`${this.API}/logout`, {}).pipe(
tap(() => this.tokenService.clearTokens())
);
}
refreshToken(): Observable<any> {
const refreshToken = this.tokenService.getRefreshToken();
return this.http.post<any>(`${this.API}/refresh`, { refreshToken }).pipe(
tap(tokens => {
this.tokenService.setTokens(tokens.accessToken, tokens.refreshToken);
})
);
}
isLoggedIn(): boolean {
return !!this.tokenService.getAccessToken();
}
}
3. ๐ AuthInterceptor with Token Refresh Logic
// interceptors/auth.interceptor.ts
import {
HttpInterceptor,
HttpRequest,
HttpHandler,
HttpEvent,
HttpErrorResponse
} from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable, catchError, filter, switchMap, take, throwError, BehaviorSubject } from 'rxjs';
import { AuthService } from '../services/auth.service';
import { TokenService } from '../services/token.service';
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
private isRefreshing = false;
private refreshTokenSubject = new BehaviorSubject<string | null>(null);
constructor(private auth: AuthService, private tokenService: TokenService) {}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const token = this.tokenService.getAccessToken();
const cloned = token
? req.clone({ setHeaders: { Authorization: `Bearer ${token}` } })
: req;
return next.handle(cloned).pipe(
catchError((error: HttpErrorResponse) => {
if (error.status === 401 && !cloned.url.endsWith('/auth/refresh')) {
return this.handle401Error(cloned, next);
}
return throwError(() => error);
})
);
}
private handle401Error(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
if (!this.isRefreshing) {
this.isRefreshing = true;
this.refreshTokenSubject.next(null);
return this.auth.refreshToken().pipe(
switchMap((tokens) => {
this.isRefreshing = false;
this.refreshTokenSubject.next(tokens.accessToken);
return next.handle(this.addToken(req, tokens.accessToken));
}),
catchError((err) => {
this.isRefreshing = false;
this.tokenService.clearTokens();
return throwError(() => err);
})
);
} else {
return this.refreshTokenSubject.pipe(
filter(token => token != null),
take(1),
switchMap(token => next.handle(this.addToken(req, token!)))
);
}
}
private addToken(req: HttpRequest<any>, token: string): HttpRequest<any> {
return req.clone({
setHeaders: {
Authorization: `Bearer ${token}`
}
});
}
}
4. ✅ Register Interceptor in AppModule
// app.module.ts
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { AuthInterceptor } from './interceptors/auth.interceptor';
@NgModule({
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: AuthInterceptor,
multi: true
}
]
})
export class AppModule {}
๐ก What Backend Should Support
Your API should expose:
Endpoint | Purpose |
---|---|
POST /auth/login | Authenticates user, returns tokens |
POST /auth/refresh | Accepts refresh token, returns new access token |
POST /auth/logout | Invalidates refresh token (optional) |
✅ Final Result
-
๐ก Access tokens are auto-added to all requests
-
♻️ Expired tokens are auto-refreshed behind the scenes
-
๐ก️ User stays logged in until refresh token expires
-
๐ง No need to handle token logic in components or services