@libs-ui/services-base-request-abstract
v0.2.357-4
Published
> Abstract base class cung cấp toàn bộ infrastructure cho HTTP requests trong Angular: chuẩn hóa response, xử lý lỗi tự động, caching IndexedDB, upload file với progress tracking và chuyển đổi page 0-based/1-based.
Readme
@libs-ui/services-base-request-abstract
Abstract base class cung cấp toàn bộ infrastructure cho HTTP requests trong Angular: chuẩn hóa response, xử lý lỗi tự động, caching IndexedDB, upload file với progress tracking và chuyển đổi page 0-based/1-based.
Giới thiệu
LibsUiBaseRequestAbstractService là abstract class trung tâm dùng để xây dựng tầng API service trong ứng dụng Angular. Class này đóng gói toàn bộ logic gọi HTTP (GET, POST, PUT, PATCH, DELETE), chuẩn hóa response từ nhiều format backend khác nhau, tự động redirect khi nhận 401 Unauthorized, hỗ trợ caching IndexedDB phân tầng theo user, và upload file native XHR với progress tracking.
Thay vì mỗi service tự viết logic xử lý lỗi và chuẩn hóa response, ta chỉ cần extend class này và implement 3 abstract methods bắt buộc.
Tính năng
- ✅ HTTP methods đầy đủ:
get,post,put,patch,delete - ✅ URL-encoded form:
postUrlEndCode,patchUrlEndCode(Content-Typeapplication/x-www-form-urlencoded) - ✅ File upload với progress tracking qua XHR native (
sendWithFile) - ✅ Response normalization tự động — hỗ trợ REST standard, Spring Boot (
totalElements,totalPages), cursor-based pagination - ✅ Pagination chuẩn hóa từ nhiều backend format về
IPagingthống nhất - ✅ Error handling: 401 → auto redirect login, phân biệt lỗi 4xx vs 5xx
- ✅ Caching với IndexedDB (
cacheIndexDB) — phân tầng theo user ID - ✅ Page zero conversion (
isStartZeroPage) — UI 1-based tự động chuyển sang API 0-based - ✅ URL dynamic segments — thay thế
:id,:userIdtrong URL tự động quareplaceURLByPattern - ✅
observeResponse— chọn mode nhận full HTTP response hoặc chỉ body
Khi nào sử dụng
- Khi cần tạo một API service mới để gọi backend REST API
- Khi muốn thống nhất cách xử lý auth token, error handling và response format toàn project
- Khi cần caching dữ liệu API theo từng user (IndexedDB)
- Khi cần upload file với progress bar (không dùng được XHR native thuần)
- Khi backend trả về nhiều format pagination khác nhau (cần chuẩn hóa về
IPaging)
Cài đặt
npm install @libs-ui/services-base-request-abstractImport
import { LibsUiBaseRequestAbstractService, TYPE_OBSERVE_RESPONSE } from '@libs-ui/services-base-request-abstract';Ví dụ sử dụng
Ví dụ 1 — Tạo ApiService gốc (implement 3 abstract methods bắt buộc)
import { Injectable } from '@angular/core';
import { HttpHeaders, HttpParams } from '@angular/common/http';
import { inject } from '@angular/core';
import { Router } from '@angular/router';
import { LibsUiBaseRequestAbstractService } from '@libs-ui/services-base-request-abstract';
import { UtilsHttpParamsRequest } from '@libs-ui/utils';
@Injectable({ providedIn: 'root' })
export class ApiService extends LibsUiBaseRequestAbstractService {
protected override baseUrl = 'https://api.example.com';
private readonly router = inject(Router);
constructor() {
super();
// Gán user ID để phân tầng cache theo từng user login
this.keyCacheUniqueByIdUserLogin = sessionStorage.getItem('userId') || '';
}
// [1] Inject auth header và content-type cho mọi request
protected getOptions<T>(params: UtilsHttpParamsRequest<T>, contentType?: string) {
const token = sessionStorage.getItem('accessToken');
return {
headers: new HttpHeaders({
'Content-Type': contentType || 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {}),
}),
params: params as unknown as HttpParams,
};
}
// [2] Được gọi tự động khi server trả về 401 Unauthorized
protected redirectToLogin(): void {
this.router.navigate(['/login']);
}
// [3] Thay thế URL dynamic segments — `:id`, `:userId`, `:orgId`...
protected replaceURLByPattern<T>(url: string, params: UtilsHttpParamsRequest<T>) {
let resultUrl = url;
const pattern = /:([\w]+)/g;
let match;
while ((match = pattern.exec(url)) !== null) {
const key = match[1];
if (params.has(key)) {
resultUrl = resultUrl.replace(`:${key}`, params.get(key)!);
params = params.delete(key) as UtilsHttpParamsRequest<T>;
}
}
return { url: resultUrl, params };
}
}Ví dụ 2 — Feature service kế thừa ApiService (CRUD đầy đủ)
import { Injectable } from '@angular/core';
import { UtilsHttpParamsRequest } from '@libs-ui/utils';
import { ApiService } from './api.service';
export type T_user = {
id: string;
name: string;
email: string;
status: string;
};
export type T_user_list_params = {
page?: number;
size?: number;
search?: string;
status?: string;
};
export type T_user_create_body = {
name: string;
email: string;
password: string;
};
export type T_user_update_body = {
name?: string;
email?: string;
};
@Injectable({ providedIn: 'root' })
export class UserApiService extends ApiService {
getList(params: T_user_list_params) {
return this.get<T_user[]>('/users', new UtilsHttpParamsRequest(undefined, { fromObject: params }));
}
getById(id: string) {
return this.get<T_user>('/users/:id', new UtilsHttpParamsRequest(undefined, { fromObject: { id } }));
}
create(body: T_user_create_body) {
return this.post<T_user>('/users', body);
}
update(id: string, body: T_user_update_body) {
return this.put<T_user>('/users/:id', body, new UtilsHttpParamsRequest(undefined, { fromObject: { id } }));
}
partialUpdate(id: string, body: Partial<T_user_update_body>) {
return this.patch<T_user>('/users/:id', body, new UtilsHttpParamsRequest(undefined, { fromObject: { id } }));
}
remove(id: string) {
return this.delete<void>('/users/:id', new UtilsHttpParamsRequest(undefined, { fromObject: { id } }));
}
}Ví dụ 3 — Caching dữ liệu với IndexedDB
import { Injectable } from '@angular/core';
import { lastValueFrom } from 'rxjs';
import { UtilsHttpParamsRequest } from '@libs-ui/utils';
import { ApiService } from './api.service';
import { T_user, T_user_list_params, T_user_create_body } from './user-api.service';
@Injectable({ providedIn: 'root' })
export class UserCachedApiService extends ApiService {
// Cache danh sách 5 phút (300000ms)
getListCached(params: T_user_list_params) {
const observable = this.get<T_user[]>('/users', new UtilsHttpParamsRequest(undefined, { fromObject: params }));
return this.cacheIndexDB(observable, 'users-list', [params], 5 * 60 * 1000);
}
// Xóa cache sau khi tạo mới để lần sau lấy dữ liệu mới nhất
async create(body: T_user_create_body) {
const result = await lastValueFrom(this.post<T_user>('/users', body));
await this.deleteCacheKeyStartWidth('users-list');
return result;
}
// Bắt buộc lấy dữ liệu mới (bỏ qua cache hiện có)
getListForceRefresh(params: T_user_list_params) {
const observable = this.get<T_user[]>('/users', new UtilsHttpParamsRequest(undefined, { fromObject: params }));
return this.cacheIndexDB(observable, 'users-list', [params], 5 * 60 * 1000, true);
}
}Ví dụ 4 — Upload file với progress tracking
import { Injectable } from '@angular/core';
import { UtilsHttpParamsRequest } from '@libs-ui/utils';
import { ApiService } from './api.service';
export type T_upload_result = { url: string; fileName: string };
@Injectable({ providedIn: 'root' })
export class FileUploadApiService extends ApiService {
uploadAvatar(userId: string, file: File, onProgress?: (percent: number) => void) {
return this.sendWithFile<T_upload_result>(
'/users/:userId/avatar',
new UtilsHttpParamsRequest(undefined, { fromObject: { userId } }),
{
method: 'POST',
bodyData: { file },
keepFileOfBody: true,
processUpload: onProgress
? ({ percent }) => onProgress(percent)
: undefined,
}
);
}
uploadMultipleFiles(folderId: string, files: File[], onProgress?: (percent: number) => void) {
return this.sendWithFile<T_upload_result[]>(
'/folders/:folderId/files',
new UtilsHttpParamsRequest(undefined, { fromObject: { folderId } }),
{
method: 'POST',
bodyData: { files },
keepFileOfBody: true,
processUpload: onProgress
? ({ percent }) => onProgress(percent)
: undefined,
}
);
}
}Ví dụ 5 — API 0-based pagination (isStartZeroPage)
Khi backend nhận page bắt đầu từ 0 nhưng UI hiển thị bắt đầu từ 1:
import { Injectable } from '@angular/core';
import { HttpHeaders, HttpParams } from '@angular/common/http';
import { Router } from '@angular/router';
import { inject } from '@angular/core';
import { LibsUiBaseRequestAbstractService } from '@libs-ui/services-base-request-abstract';
import { UtilsHttpParamsRequest } from '@libs-ui/utils';
@Injectable({ providedIn: 'root' })
export class ApiZeroPageService extends LibsUiBaseRequestAbstractService {
protected override baseUrl = 'https://api.spring-boot-backend.com';
// UI gửi page=1 → service tự trừ 1 → backend nhận page=0
protected override isStartZeroPage = true;
private readonly router = inject(Router);
protected getOptions<T>(params: UtilsHttpParamsRequest<T>, contentType?: string) {
return {
headers: new HttpHeaders({ 'Content-Type': contentType || 'application/json' }),
params: params as unknown as HttpParams,
};
}
protected redirectToLogin(): void {
this.router.navigate(['/auth/login']);
}
protected replaceURLByPattern<T>(url: string, params: UtilsHttpParamsRequest<T>) {
return { url, params };
}
}Ví dụ 6 — Form URL-encoded (postUrlEndCode)
Dùng khi backend yêu cầu Content-Type: application/x-www-form-urlencoded:
import { Injectable } from '@angular/core';
import { UtilsHttpParamsRequest } from '@libs-ui/utils';
import { ApiService } from './api.service';
export type T_login_body = { username: string; password: string };
export type T_auth_response = { accessToken: string; refreshToken: string };
@Injectable({ providedIn: 'root' })
export class AuthApiService extends ApiService {
login(body: T_login_body) {
// Gửi form URL-encoded thay vì JSON
return this.postUrlEndCode<T_auth_response>('/auth/login', body);
}
refreshToken(refreshToken: string) {
return this.postUrlEndCode<T_auth_response>('/auth/refresh', { refreshToken });
}
}Methods
Abstract Methods (bắt buộc implement trong subclass)
| Method | Signature | Mô tả |
|---|---|---|
| getOptions | <T>(httpParams: UtilsHttpParamsRequest<T>, contentType?: string) => { headers: HttpHeaders; params: HttpParams } | Inject auth token, Content-Type vào mọi request |
| redirectToLogin | () => void | Được gọi tự động khi server trả về 401 Unauthorized |
| replaceURLByPattern | <T>(url: string, params: UtilsHttpParamsRequest<T>) => { url: string; params: UtilsHttpParamsRequest<T> } | Thay thế URL dynamic segments (:id, :userId) từ params |
HTTP Methods
| Method | Signature | Trả về | Mô tả |
|---|---|---|---|
| get | <TypeResponse, TypeParams>(path, params?) | Observable<IHttpResponse<TypeResponse>> | HTTP GET |
| post | <TypeResponse, TypeParams, TypeBody>(path, body, params?) | Observable<IHttpResponse<TypeResponse>> | HTTP POST JSON |
| postUrlEndCode | <TypeResponse, TypeParams, TypeBody>(path, body, params?) | Observable<IHttpResponse<TypeResponse>> | POST application/x-www-form-urlencoded |
| put | <TypeResponse, TypeParams, TypeBody>(path, body, params?) | Observable<IHttpResponse<TypeResponse>> | HTTP PUT JSON |
| patch | <TypeResponse, TypeParams>(path, body, params?) | Observable<IHttpResponse<TypeResponse>> | HTTP PATCH JSON |
| patchUrlEndCode | <TypeResponse, TypeParams>(path, body, params?) | Observable<IHttpResponse<TypeResponse>> | PATCH application/x-www-form-urlencoded |
| delete | <TypeResponse, TypeParams>(path, params?) | Observable<IHttpResponse<TypeResponse>> | HTTP DELETE |
| sendWithFile | <TypeResponse, TypeParams, TypeBody>(path, params, args) | Observable<IHttpResponse<TypeResponse>> | Upload file qua XHR native với progress tracking |
Cache Methods
| Method | Signature | Trả về | Mô tả |
|---|---|---|---|
| cacheIndexDB | <T>(observable, key, params, timeCache?, reloadCache?) | Observable<IHttpResponse<T>> | Cache response vào IndexedDB, tự động phân tầng theo user |
| deleteCacheKeyStartWidth | (key: string) | Promise<void> | Xóa toàn bộ cache có key bắt đầu với prefix cho trước |
| cacheResponseData | <T>(observable, key, params, timeCache?, reloadCache?) | Observable<IHttpResponse<T>> | Deprecated — dùng cacheIndexDB thay thế |
Utility Methods
| Method | Signature | Trả về | Mô tả |
|---|---|---|---|
| getResponseURL | (xhr: any) | string | Lấy URL thực tế sau redirect từ XHR object |
Protected Properties (override trong subclass)
| Property | Type | Default | Mô tả |
|---|---|---|---|
| baseUrl | string | '' | Base URL của API (bắt buộc set) |
| keyCacheUniqueByIdUserLogin | string | '' | User ID suffix cho cache key — tránh conflict giữa các user |
| ignoreRedirect401 | boolean \| undefined | undefined | Set true để tắt auto redirect khi nhận 401 |
| isStartZeroPage | boolean | false | Set true khi backend dùng page 0-based, UI dùng 1-based |
| ignoreAutoIncrementPageIsStartZeroPage | boolean | false | Set true để tắt auto increment page trong response khi isStartZeroPage = true |
| observeResponse | 'response' \| 'body' | 'body' | Mode nhận HTTP response: 'body' chỉ nhận data, 'response' nhận full HTTP response |
Tham số của sendWithFile
| Tham số | Type | Bắt buộc | Mô tả |
|---|---|---|---|
| path | string | ✅ | Đường dẫn API (có thể chứa dynamic segments) |
| params | UtilsHttpParamsRequest<TypeParams> | ✅ | Query params và URL path variables |
| args.method | 'POST' \| 'PATCH' \| 'PUT' | ✅ | HTTP method cho upload |
| args.bodyData | TypeBody \| FormData | ✅ | Dữ liệu body — plain object hoặc FormData có sẵn |
| args.keepFileOfBody | boolean | ❌ | Set true để giữ nguyên File/Blob thay vì JSON.stringify |
| args.keepArrayValue | boolean | ❌ | Set true để serialize array thành JSON string thay vì append từng phần tử |
| args.processUpload | (process: IHttpProcessUpload) => void | ❌ | Callback nhận tiến độ upload |
Tham số của cacheIndexDB
| Tham số | Type | Bắt buộc | Mô tả |
|---|---|---|---|
| observable | Observable<IHttpResponse<T>> | ✅ | Observable HTTP call cần cache |
| key | string | ✅ | Cache key prefix (không md5 trước) |
| params | Array<any> | ✅ | Mảng tham số để tạo unique key (page, size, filter...) |
| timeCache | number | ❌ | Thời gian cache tính bằng ms (mặc định: UtilsCache.CACHE_EXPIRE_TIME_DEFAULT) |
| reloadCache | boolean | ❌ | Set true để xóa cache cũ và lấy dữ liệu mới nhất |
Types & Interfaces
import { IHttpResponse, IPaging, IHttpProcessUpload } from '@libs-ui/interfaces-types';
import { UtilsHttpParamsRequest, TYPE_OBSERVE_RESPONSE } from '@libs-ui/services-base-request-abstract';// Response chuẩn hóa từ mọi HTTP method
type IHttpResponse<T = any, P = IPaging> = {
code?: number;
feCloneCode?: any; // code thực từ backend khi HTTP trả 200 nhưng business code khác
message?: string;
data?: T;
paging?: P;
[key: string]: unknown;
};
// Pagination chuẩn hóa từ nhiều backend format
interface IPaging {
page?: number;
per_page?: number;
total_items?: number;
total_pages?: number;
before?: string; // cursor-based pagination
after?: string; // cursor-based pagination
previous?: string;
next?: string;
fake?: boolean;
}
// Progress upload
interface IHttpProcessUpload {
loaded: number; // bytes đã upload
total: number; // tổng bytes
percent: number; // phần trăm (0-100)
}
// Mode nhận HTTP response
type TYPE_OBSERVE_RESPONSE = 'response' | 'body';Lưu ý quan trọng
⚠️ Class là abstract — không inject trực tiếp: Phải extend LibsUiBaseRequestAbstractService và implement đầy đủ 3 abstract methods: getOptions, redirectToLogin, replaceURLByPattern.
⚠️ 3 abstract methods là bắt buộc: Thiếu bất kỳ method nào sẽ gây lỗi TypeScript compile-time. Không thể dùng super() để bỏ qua.
⚠️ cacheIndexDB thay thế cacheResponseData: Method cacheResponseData đã deprecated (dùng Promise internally). Luôn dùng cacheIndexDB cho code mới — Observable-first, không block event loop.
⚠️ keyCacheUniqueByIdUserLogin phải set trước khi dùng cache: Nếu bỏ trống, cache không phân tầng theo user — user A sẽ thấy data của user B sau khi login. Gán user ID trong constructor của ApiService gốc.
⚠️ deleteCacheKeyStartWidth — không md5 key trước khi truyền: Method này tự md5 key nội bộ. Truyền vào key thô (cùng string đã dùng khi gọi cacheIndexDB).
⚠️ sendWithFile dùng XHR native: Không phải Angular HttpClient — để có onprogress event. Interceptor của Angular không áp dụng ở đây. Headers phải được set thủ công từ getOptions.
⚠️ isStartZeroPage và response paging: Khi bật isStartZeroPage, service tự trừ 1 trước khi gửi lên API và tự cộng 1 trong response trả về. Component chỉ cần làm việc với page 1-based nhất quán.
⚠️ postUrlEndCode / patchUrlEndCode: Tự động encode body thành URL-encoded format. Không truyền JSON body vào — truyền plain object, class sẽ tự encode.
