# API Integration Patterns Guide for integrating the frontend with the B12 SIS backend API. ## API Base Configuration ### Environment Setup ```typescript // environments/environment.ts export const environment = { production: false, apiUrl: 'http://localhost:8080/api' }; // environments/environment.prod.ts export const environment = { production: true, apiUrl: 'https://api.school.edu/api' }; ``` ### ApiService The core HTTP wrapper for all API calls: ```typescript import { Injectable } from '@angular/core'; import { HttpClient, HttpParams } from '@angular/common/http'; import { environment } from 'environments/environment'; @Injectable({ providedIn: 'root' }) export class ApiService { private baseUrl = environment.apiUrl; constructor(private http: HttpClient) {} get(endpoint: string, params?: HttpParams) { return this.http.get(`${this.baseUrl}/${endpoint}`, { params }); } post(endpoint: string, body?: any, queryParams?: Record) { let url = `${this.baseUrl}/${endpoint}`; if (queryParams) { url += `?${new URLSearchParams(queryParams).toString()}`; } return this.http.post(url, body); } put(endpoint: string, body?: any) { return this.http.put(`${this.baseUrl}/${endpoint}`, body); } delete(endpoint: string) { return this.http.delete(`${this.baseUrl}/${endpoint}`); } } ``` ## Standard CRUD Operations ### List with Pagination and Filters ```typescript // Request POST /api/students/list // Body { "page": 1, "page_size": 25, "sort_by": "created_at", "sort_order": "desc", "filters": [ { "field": "status", "operator": "eq", "value": "active" } ] } ``` **Frontend Implementation:** ```typescript interface ListRequest { page: number; page_size: number; sort_by?: string; sort_order?: 'asc' | 'desc'; filters?: Filter[]; } interface Filter { field: string; operator: 'eq' | 'ne' | 'like' | 'gt' | 'gte' | 'lt' | 'lte' | 'in' | 'not_in'; value: any; } // Service listStudents(request: ListRequest) { return this.api.post>('students/list', request); } // Component loadStudents() { this.studentService.listStudents({ page: this.currentPage, page_size: 25, sort_by: 'last_name', sort_order: 'asc', filters: [ { field: 'status', operator: 'eq', value: 'active' } ] }).subscribe(response => { this.students = response.data; this.totalRecords = response.metadata.total; }); } ``` ### Response Format All API responses follow this structure: ```typescript interface ApiResponse { data: T; message?: string; metadata?: { page: number; page_size: number; total: number; total_pages: number; }; } ``` ### Create ```typescript // Request POST /api/students { "first_name": "John", "last_name": "Doe", "email": "john.doe@school.edu", "date_of_birth": "2010-05-15" } // Response { "data": { "id": "uuid-here", "first_name": "John", "last_name": "Doe", ... }, "message": "Student created successfully" } ``` **Frontend Implementation:** ```typescript createStudent(student: StudentInput) { return this.api.post>('students', student); } // Component onSubmit() { this.studentService.createStudent(this.form.value) .subscribe({ next: (response) => { this.notification.showSuccess('Student created'); this.router.navigate(['/module/students', response.data.id]); }, error: (error) => { this.notification.showError(error.message); } }); } ``` ### Read (Get by ID) ```typescript // Request GET /api/students/:id // Response { "data": { "id": "uuid-here", "first_name": "John", ... } } ``` **Frontend Implementation:** ```typescript getStudent(id: string) { return this.api.get>(`students/${id}`); } // Component ngOnInit() { const id = this.route.snapshot.paramMap.get('id'); this.studentService.getStudent(id).subscribe(response => { this.student = response.data; }); } ``` ### Update ```typescript // Request PUT /api/students/:id { "first_name": "Jane", "last_name": "Doe" } // Response { "data": { ... }, "message": "Student updated successfully" } ``` **Frontend Implementation:** ```typescript updateStudent(id: string, data: Partial) { return this.api.put>(`students/${id}`, data); } // Component onSave() { this.studentService.updateStudent(this.student.id, this.form.value) .subscribe({ next: () => { this.notification.showSuccess('Student updated'); } }); } ``` ### Delete ```typescript // Request DELETE /api/students/:id // Response { "message": "Student deleted successfully" } ``` **Frontend Implementation:** ```typescript deleteStudent(id: string) { return this.api.delete>(`students/${id}`); } // Component with confirmation onDelete(student: Student) { if (confirm(`Delete ${student.first_name} ${student.last_name}?`)) { this.studentService.deleteStudent(student.id) .subscribe(() => { this.notification.showSuccess('Student deleted'); this.refreshList(); }); } } ``` ## Authentication ### Login Flow ```typescript // AuthService login(email: string, password: string) { return this.api.post('auth/login', { email, password }).pipe( tap(user => { this.currentUserSubject.next(user); }), concatMap(() => this.api.get<{data: {permissions: Permission[], user: any}}>('auth/me')), tap(({data}) => { this.currentUserValue.permissions = data.permissions; this.currentUserValue.user = data.user; localStorage.setItem('currentUser', JSON.stringify(this.currentUserValue)); }) ); } // Component onLogin() { this.authService.login(this.email, this.password).subscribe({ next: () => { this.router.navigate(['/module/dashboard']); }, error: (error) => { this.errorMessage = 'Invalid email or password'; } }); } ``` ### JWT Token Handling The `JwtInterceptor` automatically adds the token: ```typescript @Injectable() export class JwtInterceptor implements HttpInterceptor { constructor(private authService: AuthService) {} intercept(request: HttpRequest, next: HttpHandler) { const currentUser = this.authService.currentUserValue; if (currentUser?.token?.access_token) { request = request.clone({ setHeaders: { Authorization: `Bearer ${currentUser.token.access_token}` } }); } return next.handle(request); } } ``` ### Token Refresh ```typescript // Backend: POST /api/auth/refresh // Body: { "refresh_token": "..." } // Response: { "access_token": "...", "refresh_token": "..." } refreshToken() { const refreshToken = this.currentUserValue.token.refresh_token; return this.api.post<{access_token: string, refresh_token: string}>('auth/refresh', { refresh_token: refreshToken }).pipe( tap(tokens => { this.currentUserValue.token = tokens; this.currentUserSubject.next(this.currentUserValue); localStorage.setItem('currentUser', JSON.stringify(this.currentUserValue)); }) ); } ``` ## Multi-Tenancy ### Team Header Injection The `TeamInterceptor` adds the team context: ```typescript @Injectable() export class TeamInterceptor implements HttpInterceptor { constructor(private authService: AuthService) {} intercept(request: HttpRequest, next: HttpHandler) { const teamId = this.authService.currentUserValue.active_team_id; if (teamId) { request = request.clone({ setHeaders: { 'X-Team-ID': teamId } }); } return next.handle(request); } } ``` ### Switching Teams ```typescript // Service setActiveTeam(teamId: string) { this.currentUserValue.active_team_id = teamId; this.currentUserSubject.next(this.currentUserValue); localStorage.setItem('currentUser', JSON.stringify(this.currentUserValue)); return of(this.currentUserValue); } // Component (team selector) onTeamChange(teamId: string) { this.authService.setActiveTeam(teamId).subscribe(() => { // Refresh current view to load team-specific data window.location.reload(); }); } ``` ## Advanced Filtering ### Complex Filter Queries ```typescript // AND filters (default) { "filters": [ { "field": "status", "operator": "eq", "value": "active" }, { "field": "grade_level", "operator": "eq", "value": "10" } ] } // OR filters { "filters": [ { "field": "status", "operator": "in", "value": ["active", "pending"] } ] } // Date range { "filters": [ { "field": "created_at", "operator": "gte", "value": "2024-01-01" }, { "field": "created_at", "operator": "lte", "value": "2024-12-31" } ] } // LIKE search { "filters": [ { "field": "first_name", "operator": "like", "value": "John" } ] } ``` ### Relation Filters Filter by related entity fields: ```typescript // Filter students by class name { "filters": [ { "field": "current_class_id", "operator": "relate", "relate_filters": [ { "field": "name", "operator": "like", "value": "Grade 10" } ] } ] } ``` ## File Upload ### Upload to S3 ```typescript // Get presigned URL POST /api/file_uploads/presigned { "file_name": "document.pdf", "content_type": "application/pdf", "module": "Student" } // Response { "data": { "upload_url": "https://s3.../presigned-url", "file_url": "https://s3.../final-url" } } ``` **Frontend Implementation:** ```typescript uploadFile(file: File, module: string) { // Step 1: Get presigned URL return this.api.post<{data: {upload_url: string, file_url: string}}>('file_uploads/presigned', { file_name: file.name, content_type: file.type, module }).pipe( // Step 2: Upload to S3 concatMap(response => { return this.http.put(response.data.upload_url, file, { headers: { 'Content-Type': file.type } }).pipe( map(() => response.data.file_url) ); }) ); } // Component onFileSelected(event: Event) { const file = (event.target as HTMLInputElement).files[0]; this.fileService.uploadFile(file, 'Student').subscribe(fileUrl => { this.form.patchValue({ document_url: fileUrl }); }); } ``` ## Error Handling ### ErrorInterceptor ```typescript @Injectable() export class ErrorInterceptor implements HttpInterceptor { constructor( private authService: AuthService, private router: Router, private notification: NotificationService ) {} intercept(request: HttpRequest, next: HttpHandler) { return next.handle(request).pipe( catchError((error: HttpErrorResponse) => { switch (error.status) { case 401: // Unauthorized - redirect to login this.authService.logout(); this.router.navigate(['/authentication/signin']); break; case 403: // Forbidden this.notification.showError('You do not have permission'); break; case 404: this.notification.showError('Resource not found'); break; case 422: // Validation error const message = error.error?.message || 'Validation failed'; this.notification.showError(message); break; case 500: this.notification.showError('Server error. Please try again.'); break; } return throwError(() => error); }) ); } } ``` ### Validation Errors Backend returns validation errors: ```json { "message": "Validation failed", "errors": { "email": ["Email is required", "Email format is invalid"], "first_name": ["First name is required"] } } ``` **Frontend Handling:** ```typescript onSubmit() { this.studentService.createStudent(this.form.value).subscribe({ error: (error) => { if (error.error?.errors) { // Map backend errors to form controls Object.keys(error.error.errors).forEach(field => { const control = this.form.get(field); if (control) { control.setErrors({ serverError: error.error.errors[field][0] }); } }); } } }); } ``` ## Real-Time Updates ### Polling Pattern ```typescript // Poll for updates every 30 seconds pollForUpdates() { return interval(30000).pipe( startWith(0), switchMap(() => this.api.get('notifications/unread')), takeUntil(this.destroy$) ); } ``` ### WebSocket (if implemented) ```typescript // BroadcastService for cross-component communication @Injectable({ providedIn: 'root' }) export class BroadcastService { private messageSource = new Subject(); message$ = this.messageSource.asObservable(); send(message: BroadcastMessage) { this.messageSource.next(message); } } ``` ## Batch Operations ### Bulk Delete ```typescript // Request POST /api/students/bulk-delete { "ids": ["uuid-1", "uuid-2", "uuid-3"] } // Response { "message": "3 students deleted successfully" } ``` **Frontend Implementation:** ```typescript bulkDelete(ids: string[]) { return this.api.post>('students/bulk-delete', { ids }); } // Component onBulkDelete() { const selectedIds = this.selectedStudents.map(s => s.id); this.studentService.bulkDelete(selectedIds).subscribe(() => { this.notification.showSuccess(`${selectedIds.length} students deleted`); this.clearSelection(); this.refreshList(); }); } ``` ## Export/Import ### Export to Excel ```typescript // Request POST /api/students/export { "filters": [...], "columns": ["first_name", "last_name", "email", "status"] } // Response: File download ``` **Frontend Implementation:** ```typescript exportStudents(filters: Filter[], columns: string[]) { return this.http.post(`${this.baseUrl}/students/export`, { filters, columns }, { responseType: 'blob' } ).pipe( tap(blob => { const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'students.xlsx'; a.click(); window.URL.revokeObjectURL(url); }) ); } ``` ### Import from Excel See [Import System Guide](import-system.md) for detailed import documentation. ## Caching Strategies ### Service-Level Caching ```typescript @Injectable({ providedIn: 'root' }) export class GradeLevelService { private cache$ = new ReplaySubject(1); private loaded = false; getGradeLevels() { if (!this.loaded) { this.api.get>('grade_levels/list').pipe( map(response => response.data) ).subscribe(data => { this.cache$.next(data); this.loaded = true; }); } return this.cache$.asObservable(); } invalidateCache() { this.loaded = false; } } ``` ### HTTP Caching Headers Backend returns appropriate cache headers: - `Cache-Control: no-cache` for dynamic data - `Cache-Control: max-age=3600` for static data ## Performance Tips 1. **Use pagination** - Never load all records 2. **Lazy load modules** - Reduce initial bundle 3. **Cancel pending requests** - Use `takeUntil` or `switchMap` 4. **Debounce search** - Prevent excessive API calls 5. **Cache reference data** - Grade levels, subjects, etc. ```typescript // Debounced search searchControl.valueChanges.pipe( debounceTime(300), distinctUntilChanged(), switchMap(term => this.search(term)), takeUntil(this.destroy$) ).subscribe(results => { this.searchResults = results; }); ```