API INTEGRATION

Ecosistema completo para organización y gestión de contenido con interfaces espaciales

API Integration Guide - User Management Module

Complete guide for integrating the user-management module with backend APIs.

Table of Contents


API Service Overview

The UserApiService handles all HTTP communication with the backend:

import { UserApiService } from '@libs-core/user-management';

export class UserService {
  private userApi = inject(UserApiService);

  // All HTTP operations are handled through this service
}

Key Features:


API Endpoints

Base URL Configuration

The service uses a configurable base URL:

// In environment files
export const environment = {
  apiBaseUrl: 'https://api.example.com/v1',
  userApiPath: '/users',  // Default: '/users'
};

Endpoint Structure

BASE_URL/users
├── GET    /              - List users with filters
├── POST   /              - Create new user
├── GET    /:id           - Get user by ID
├── PUT    /:id           - Update user
├── DELETE /:id           - Delete user
├── PATCH  /:id/status    - Change user status
└── POST   /bulk          - Bulk operations

Request/Response Models

List Users

Request:

GET /users?search=john&status=active&page=1&limit=20

Query Parameters:

interface UserListParams {
  search?: string;           // Search in name, email, username
  status?: string[];         // User statuses (comma-separated)
  roleIds?: string[];        // Role IDs (comma-separated)
  createdAfter?: string;     // ISO date string
  createdBefore?: string;    // ISO date string
  lastActiveAfter?: string;  // ISO date string
  page?: number;             // Page number (1-based)
  limit?: number;            // Items per page (default: 20)
  sortBy?: string;           // Field to sort by
  sortOrder?: 'asc' | 'desc'; // Sort direction
}

Response:

interface UserListResponse {
  users: User[];
  total: number;
  page: number;
  limit: number;
  totalPages: number;
}

Example:

const response = await firstValueFrom(
  this.userApi.getUsers({
    search: 'john',
    status: ['active'],
    page: 1,
    limit: 20
  })
);

console.log(response.users);      // User[]
console.log(response.total);      // 150
console.log(response.totalPages); // 8

Get User by ID

Request:

GET /users/:id

Response:

User | null

Example:

const user = await firstValueFrom(
  this.userApi.getUserById('user-123')
);

if (user) {
  console.log(user.email);
}

Create User

Request:

POST /users
Content-Type: application/json

{
  "email": "[email protected]",
  "username": "johndoe",
  "firstName": "John",
  "lastName": "Doe",
  "primaryRole": "user",
  "status": "active"
}

Request Model:

interface CreateUserRequest {
  email: string;              // Required, valid email
  username: string;           // Required, unique
  firstName: string;          // Required
  lastName: string;           // Required
  primaryRole: SystemRoles;   // Required
  status?: UserStatus;        // Optional, defaults to 'pending'
  avatar?: string;            // Optional, URL
  metadata?: Record<string, unknown>; // Optional
}

Response:

User

Example:

const newUser = await firstValueFrom(
  this.userApi.createUser({
    email: '[email protected]',
    username: 'johndoe',
    firstName: 'John',
    lastName: 'Doe',
    primaryRole: SystemRoles.USER
  })
);

console.log('Created user:', newUser.id);

Update User

Request:

PUT /users/:id
Content-Type: application/json

{
  "firstName": "Jane",
  "lastName": "Smith",
  "avatar": "https://example.com/avatar.jpg"
}

Request Model:

interface UpdateUserRequest {
  userId: string;             // Required
  email?: string;             // Optional
  username?: string;          // Optional
  firstName?: string;         // Optional
  lastName?: string;          // Optional
  avatar?: string;            // Optional
  metadata?: Record<string, unknown>; // Optional
}

Response:

User

Example:

const updated = await firstValueFrom(
  this.userApi.updateUser({
    userId: 'user-123',
    firstName: 'Jane',
    lastName: 'Smith'
  })
);

Delete User

Request:

DELETE /users/:id

Response:

null

Example:

await firstValueFrom(
  this.userApi.deleteUser('user-123')
);

console.log('User deleted successfully');

Change User Status

Request:

PATCH /users/:id/status
Content-Type: application/json

{
  "newStatus": "suspended",
  "reason": "Policy violation"
}

Request Model:

interface ChangeUserStatusRequest {
  userId: string;             // Required
  newStatus: UserStatus;      // Required: 'active' | 'inactive' | 'suspended' | 'pending'
  reason?: string;            // Optional
}

Response:

User

Example:

const updated = await firstValueFrom(
  this.userApi.changeUserStatus({
    userId: 'user-123',
    newStatus: 'suspended',
    reason: 'Policy violation'
  })
);

Bulk Operations

Request:

POST /users/bulk
Content-Type: application/json

{
  "operation": "activate",
  "userIds": ["user-1", "user-2", "user-3"],
  "options": {}
}

Request Model:

interface BulkOperationRequest {
  operation: 'activate' | 'deactivate' | 'delete' | 'changeRole';
  userIds: string[];
  options: {
    newRole?: string;         // For 'changeRole' operation
    reason?: string;          // For status changes
    force?: boolean;          // Force delete even if user has data
  };
}

Response:

interface BulkOperationResult {
  successCount: number;
  failureCount: number;
  errors: Array<{
    userId: string;
    error: string;
  }>;
}

Example:

const result = await firstValueFrom(
  this.userApi.bulkUpdateUsers({
    operation: 'activate',
    userIds: ['user-1', 'user-2', 'user-3'],
    options: {}
  })
);

console.log(`Success: ${result.successCount}`);
console.log(`Failed: ${result.failureCount}`);
if (result.errors.length > 0) {
  console.log('Errors:', result.errors);
}

Authentication & Authorization

JWT Token Management

The service automatically includes JWT tokens in requests:

// Automatic token injection via HTTP interceptor
GET /users
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Token Refresh:

import { AuthService } from '@libs-core/security';

export class UserComponent {
  private authService = inject(AuthService);

  async loadUsers() {
    // Token is automatically refreshed if expired
    const users = await firstValueFrom(this.userApi.getUsers());
  }
}

Permission Checks

Before making API calls, check permissions:

import { PermissionService } from '@libs-core/security';

export class UserActionsComponent {
  private permissionService = inject(PermissionService);
  private userApi = inject(UserApiService);

  async createUser(request: CreateUserRequest) {
    // Check permission before API call
    if (!this.permissionService.hasPermission('users:create')) {
      throw new Error('Insufficient permissions');
    }

    return firstValueFrom(this.userApi.createUser(request));
  }

  async deleteUser(userId: string) {
    if (!this.permissionService.hasPermission('users:delete')) {
      throw new Error('Insufficient permissions');
    }

    return firstValueFrom(this.userApi.deleteUser(userId));
  }
}

Role-Based Access

import { RoleService, SystemRoles } from '@libs-core/security';

export class AdminActionsComponent {
  private roleService = inject(RoleService);
  private userApi = inject(UserApiService);

  async performAdminAction() {
    // Check role before API call
    if (!this.roleService.hasRole(SystemRoles.ADMIN)) {
      throw new Error('Admin role required');
    }

    // Perform admin-only operation
  }
}

Error Handling

API Error Structure

interface ApiError {
  status: number;           // HTTP status code
  statusText: string;       // HTTP status text
  error: {
    code: string;           // Error code (e.g., 'VALIDATION_ERROR')
    message: string;        // Human-readable message
    details?: unknown;      // Additional error details
    timestamp: string;      // ISO timestamp
    path: string;           // Request path
  };
}

Handling Errors

import { catchError, throwError } from 'rxjs';

export class UserService {
  private userApi = inject(UserApiService);

  createUser(request: CreateUserRequest) {
    return this.userApi.createUser(request).pipe(
      catchError(error => {
        // Log error
        console.error('Create user failed:', error);

        // Handle specific error codes
        if (error.error?.code === 'DUPLICATE_EMAIL') {
          this.showError('Email already exists');
        } else if (error.error?.code === 'VALIDATION_ERROR') {
          this.showError('Invalid user data');
        } else {
          this.showError('Failed to create user');
        }

        return throwError(() => error);
      })
    );
  }

  private showError(message: string) {
    // Show error notification
  }
}

Common Error Codes

Code Status Description
UNAUTHORIZED 401 Not authenticated
FORBIDDEN 403 Insufficient permissions
NOT_FOUND 404 User not found
DUPLICATE_EMAIL 409 Email already exists
DUPLICATE_USERNAME 409 Username already exists
VALIDATION_ERROR 400 Invalid request data
INTERNAL_ERROR 500 Server error

Retry Logic

import { retry, delay } from 'rxjs/operators';

export class UserService {
  loadUsers() {
    return this.userApi.getUsers().pipe(
      retry({
        count: 3,
        delay: 1000,
        resetOnSuccess: true
      }),
      catchError(error => {
        console.error('Failed after 3 retries:', error);
        return throwError(() => error);
      })
    );
  }
}

Mock API for Development

The module includes a complete mock API for development:

Enable Mock API

import { provideMockUserApi } from '@libs-core/user-management';

export const appConfig: ApplicationConfig = {
  providers: [
    provideMockUserApi(),  // Use mock API instead of real HTTP
    // ...
  ],
};

Mock Data

The mock API includes:

Mock API Features

// All operations work with mock data
const users = await firstValueFrom(this.userApi.getUsers());
// Returns mock users from in-memory storage

const user = await firstValueFrom(this.userApi.createUser(request));
// Adds to mock storage, generates ID

const updated = await firstValueFrom(this.userApi.updateUser(request));
// Updates mock storage

await firstValueFrom(this.userApi.deleteUser(id));
// Removes from mock storage

Customize Mock Data

import { USER_MOCK_DATA } from '@libs-core/user-management';

export const customMockData: User[] = [
  {
    id: 'custom-1',
    email: '[email protected]',
    username: 'admin',
    // ... custom data
  },
  // Add more custom users
];

// Provide custom mock data
providers: [
  { provide: USER_MOCK_DATA, useValue: customMockData },
  provideMockUserApi(),
]

Real API Integration

Configure API Endpoint

// environment.ts
export const environment = {
  production: false,
  apiBaseUrl: 'http://localhost:3000/api',
};

// environment.prod.ts
export const environment = {
  production: true,
  apiBaseUrl: 'https://api.example.com/v1',
};

Provide Real API Service

import { provideUserApi, USER_API_BASE_URL } from '@libs-core/user-management';
import { environment } from './environment';

export const appConfig: ApplicationConfig = {
  providers: [
    { provide: USER_API_BASE_URL, useValue: environment.apiBaseUrl },
    provideUserApi(),  // Real HTTP implementation
    // ...
  ],
};

HTTP Interceptors

The service integrates with Angular HTTP interceptors:

import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { authInterceptor, tenantInterceptor } from '@libs-core/security';

export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(
      withInterceptors([
        authInterceptor,    // Adds JWT token
        tenantInterceptor,  // Adds tenant context
      ])
    ),
    // ...
  ],
};

Custom Headers

Add custom headers to all requests:

import { HttpHeaders } from '@angular/common/http';

export class UserService {
  private userApi = inject(UserApiService);

  createUserWithCustomHeaders(request: CreateUserRequest) {
    const headers = new HttpHeaders({
      'X-Custom-Header': 'value',
      'X-Request-ID': generateRequestId(),
    });

    // Note: UserApiService doesn't expose headers directly
    // Use HttpClient wrapper if needed
  }
}

Multi-Tenant Support

Automatic Tenant Context

The service automatically includes tenant context:

// Request headers automatically include:
X-Tenant-ID: tenant-123

// Injected from ContextService

Tenant-Specific Operations

import { ContextService } from '@libs-core/security';

export class UserService {
  private contextService = inject(ContextService);
  private userApi = inject(UserApiService);

  async getUsersForCurrentTenant() {
    const context = this.contextService.getCurrentContext();
    console.log('Loading users for tenant:', context.tenantId);

    // Tenant ID is automatically added to request
    return firstValueFrom(this.userApi.getUsers());
  }

  async switchTenantAndLoadUsers(newTenantId: string) {
    // Switch tenant context
    this.contextService.setContext({ tenantId: newTenantId });

    // Subsequent API calls use new tenant
    return firstValueFrom(this.userApi.getUsers());
  }
}

Tenant Isolation

All API operations are tenant-isolated:


Performance Optimization

Caching

Implement caching for frequently accessed data:

import { shareReplay } from 'rxjs/operators';

export class UserService {
  private userApi = inject(UserApiService);
  private cache$ = new Map<string, Observable<User>>();

  getUserById(id: string): Observable<User> {
    if (!this.cache$.has(id)) {
      this.cache$.set(
        id,
        this.userApi.getUserById(id).pipe(
          shareReplay({ bufferSize: 1, refCount: true })
        )
      );
    }
    return this.cache$.get(id)!;
  }

  clearCache(id?: string) {
    if (id) {
      this.cache$.delete(id);
    } else {
      this.cache$.clear();
    }
  }
}

Pagination

Always use pagination for large datasets:

export class UserListComponent {
  private userApi = inject(UserApiService);

  currentPage = signal(1);
  pageSize = signal(20);

  users = toSignal(
    toObservable(this.currentPage).pipe(
      switchMap(page =>
        this.userApi.getUsers({
          page: page,
          limit: this.pageSize()
        })
      )
    ),
    { initialValue: { users: [], total: 0 } }
  );

  nextPage() {
    this.currentPage.update(p => p + 1);
  }

  previousPage() {
    this.currentPage.update(p => Math.max(1, p - 1));
  }
}

Request Debouncing

Debounce search requests:

import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators';

export class UserSearchComponent {
  private userApi = inject(UserApiService);

  searchControl = new FormControl('');

  searchResults = toSignal(
    this.searchControl.valueChanges.pipe(
      debounceTime(300),
      distinctUntilChanged(),
      switchMap(query =>
        this.userApi.getUsers({ search: query || undefined })
      )
    ),
    { initialValue: { users: [], total: 0 } }
  );
}

Request Cancellation

Cancel previous requests when new ones are made:

import { switchMap } from 'rxjs/operators';

export class UserListComponent {
  private userApi = inject(UserApiService);
  private filters$ = new BehaviorSubject<UserFilters>({});

  // switchMap cancels previous request when new one is emitted
  users$ = this.filters$.pipe(
    switchMap(filters => this.userApi.getUsers(filters))
  );

  updateFilters(filters: UserFilters) {
    this.filters$.next(filters);
  }
}

Batch Operations

Batch multiple API calls:

import { forkJoin } from 'rxjs';

export class UserBatchService {
  private userApi = inject(UserApiService);

  loadMultipleUsers(userIds: string[]): Observable<User[]> {
    const requests = userIds.map(id => this.userApi.getUserById(id));
    return forkJoin(requests);
  }

  async batchUpdate(updates: UpdateUserRequest[]): Promise<User[]> {
    // Use bulk API instead of individual calls
    const bulkRequest = {
      operation: 'update' as const,
      updates: updates,
    };

    // If your API supports bulk updates
    // return firstValueFrom(this.userApi.bulkUpdate(bulkRequest));

    // Otherwise, sequential updates
    const results: User[] = [];
    for (const update of updates) {
      const result = await firstValueFrom(this.userApi.updateUser(update));
      results.push(result);
    }
    return results;
  }
}

Testing API Integration

Unit Tests with Mock API

import { TestBed } from '@angular/core/testing';
import { UserApiService, provideMockUserApi } from '@libs-core/user-management';

describe('UserService', () => {
  let service: UserApiService;

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [provideMockUserApi()]
    });

    service = TestBed.inject(UserApiService);
  });

  it('should load users', async () => {
    const response = await firstValueFrom(service.getUsers());
    expect(response.users).toBeDefined();
    expect(response.users.length).toBeGreaterThan(0);
  });
});

Integration Tests with HttpTestingController

import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';

describe('UserApiService', () => {
  let service: UserApiService;
  let httpMock: HttpTestingController;

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        provideHttpClient(),
        provideHttpClientTesting(),
        UserApiService,
      ]
    });

    service = TestBed.inject(UserApiService);
    httpMock = TestBed.inject(HttpTestingController);
  });

  it('should call correct endpoint', () => {
    service.getUsers().subscribe();

    const req = httpMock.expectOne('/api/users');
    expect(req.request.method).toBe('GET');

    req.flush({ users: [], total: 0 });
  });
});

API Migration Checklist

When migrating from mock to real API:


API Documentation

For complete backend API documentation, refer to: