@gennext/lb-infra
@gennext/lb-infra
Gennex Technology - LoopBack 4 infrastructure framework.
@gennext/lb-infra cung cấp các lớp cơ sở (base classes), components, helpers, datasources, model mixins, repositories và các controller generator dùng chung để xây dựng các dịch vụ backend chất lượng cao trên nền tảng LoopBack 4.
1. Requirements
- Node.js:
>= 18 - Bun:
>= 1.3.2 - TypeScript
Cài đặt dependencies từ thư mục gốc của repository:
bun installCác lệnh chạy trong package:
bun run --filter "@gennext/lb-infra" build
bun run --filter "@gennext/lb-infra" clean
bun run --filter "@gennext/lb-infra" rebuild
bun run --filter "@gennext/lb-infra" lint
bun run --filter "@gennext/lb-infra" testLệnh tắt từ root:
bun run rebuild:lb-infra2. CLI
Sau khi cài đặt package, binary lb-infra có thể được sử dụng để khởi tạo cấu trúc thư mục mặc định cho dự án consumer-app:
lb-infra initHoặc trỏ tới một thư mục cụ thể:
lb-infra init ./apps/apiCấu trúc thư mục được sinh ra:
src
├── components
├── controllers
├── datasources
├── interceptors
├── middlewares
├── migrations
├── models
├── protos
├── repositories
├── services
└── types
public
Các file placeholder index.ts và .gitkeep sẽ được tự động tạo. Sử dụng cờ --force để ghi đè các file đã có:
lb-infra init ./apps/api --force3. Main Imports & Re-exports
Sử dụng trực tiếp các thành phần chính được cung cấp bởi framework:
import {
BaseApplication,
DefaultRestApplication,
defineCrudController,
defineKVController,
TzCrudRepository,
SearchableTzCrudRepository,
} from '@gennext/lb-infra';Sử dụng các alias re-export của LoopBack để đảm bảo đồng bộ phiên bản:
import {inject, injectable, BindingScope} from '@gennext/lb-infra/lb-core';
import {api, get, post, param, requestBody} from '@gennext/lb-infra/lb-rest';
import {model, property, repository} from '@gennext/lb-infra/lb-repo';
import {authenticate, TokenServiceBindings} from '@gennext/lb-infra/lb-auth';Các tính năng tùy chọn:
import {SocketIOComponent} from '@gennext/lb-infra/socket-io';
import {GrpcServerComponent} from '@gennext/lb-infra/grpc';4. Cấu trúc thư mục (Project Layout)
src/
├── base/ # Các base class cốt lõi (Application, Sequence, Models, Repositories, Controllers, Services)
├── common/ # Định nghĩa constants, kiểu dữ liệu dùng chung (Types), keys và environments
├── components/ # Các LoopBack 4 Components cắm ngoài (Auth, Authorize, Mail, Health, v.v.)
├── datasources/ # Nguồn cấp dữ liệu (Postgres, Redis, In-memory KV)
├── helpers/ # Các wrapper helper: logger, hàng đợi queue, cryptography, testing, cronjob
├── interceptors/ # Global Interceptors (ví dụ: Content-Range header)
├── middlewares/ # Express Middlewares (Request body parser, Request spy)
├── migrations/ # Script chạy migration cơ sở dữ liệu
├── mixins/ # TypeScript mixins dùng cho LoopBack Models
└── utilities/ # Hàm tiện ích (parse data, query helper, format, error handler)
5. Kiến trúc ứng dụng & Vòng đời (Architecture & Lifecycle)
Ứng dụng kế thừa từ BaseApplication hoặc DefaultRestApplication tuân theo thứ tự khởi động và nạp cấu hình nghiêm ngặt:
New Application Instance
│
▼
staticConfigure()
│
▼
initialize()
│
▼
validateEnv()
│
▼
declareModels()
│
▼
preConfigure()
│
▼
Loopback Bootstrapping
│
▼
postConfigure()
│
▼
Application Ready
Chi tiết các lifecycle hooks:
staticConfigure(): Được gọi đầu tiên trong constructor củaBaseApplication.- Khi nào dùng: Thực hiện bind trực tiếp các giá trị tĩnh, cấu hình component, datasources tĩnh trước khi nạp sequence hay cấu hình động.
validateEnv(): Được gọi khi initialize.- Khi nào dùng: Thực hiện kiểm tra các biến môi trường thông qua
validateEnvironment()theo schema định sẵn. Nếu trả vềresult: false, ứng dụng sẽ dừng boot ngay lập tức kèm thông tin lỗi chi tiết.
- Khi nào dùng: Thực hiện kiểm tra các biến môi trường thông qua
declareModels(): Được gọi ngay sau kiểm tra môi trường.- Khi nào dùng: Đăng ký danh sách các model của consumer app với metadata schema.
preConfigure(): Chạy trước khi LoopBack thực hiện boot process quét thư mục.- Khi nào dùng: Bind cấu hình dynamic, đăng ký interceptors, khai báo global middleware, thiết lập kết nối datasource.
postConfigure(): Chạy sau khi ứng dụng hoàn tất quá trình boot.- Khi nào dùng: Thực hiện các tác vụ khởi tạo muộn, kiểm tra tính sẵn sàng của database hoặc thiết lập lắng nghe hàng đợi (worker).
6. Quản lý phụ thuộc (Dependency Graph & DI Binding Keys)
Framework chuẩn hóa cách đặt tên binding keys để tránh sử dụng các chuỗi string thô không an toàn thông qua lớp static BindingKeys:
- Datasource bindings:
datasources.postgres,datasources.redis - Repository bindings:
repositories.XxxRepository(được tạo tự động thông quaBindingKeys.repositoryFor('XxxRepository')) - Service bindings:
services.XxxService(được tạo tự động thông quaBindingKeys.serviceFor('XxxService'))
Ví dụ Wire Controller - Service - Repository thông qua @inject:
import {BindingKeys} from '@gennext/lb-infra';
import {inject, injectable, BindingScope} from '@gennext/lb-infra/lb-core';
@injectable({scope: BindingScope.SINGLETON})
export class ProductRepository extends TzCrudRepository<Product> {
constructor(
@inject('datasources.postgres') dataSource: PostgresDataSource
) {
super(Product, dataSource);
}
}
@injectable({scope: BindingScope.TRANSIENT})
export class ProductService extends BaseCrudService<Product> {
constructor(
@inject(BindingKeys.repositoryFor('ProductRepository'))
protected productRepository: ProductRepository
) {
super({ scope: ProductService.name, repository: productRepository });
}
}7. Hệ thống Models & Mixins
Framework cung cấp sẵn các base class tích hợp mixin nhằm tái sử dụng thuộc tính dữ liệu:
| Base Model Class | Các Mixin tích hợp sẵn | Mục đích / Khi nào nên dùng |
|---|---|---|
BaseEntity |
Không | Model cơ bản nhất không có ID và audit log |
BaseKVEntity |
Không | Dành cho các cặp dữ liệu dạng Key-Value (chứa trường payload) |
BaseTzEntity |
TzMixin |
Tự động tạo và cập nhật createdAt, modifiedAt |
BaseUserAuditTzEntity |
TzMixin, UserAuditMixin |
Tự động cập nhật timestamp cùng thông tin user tạo/sửa (createdBy, modifiedBy) |
BaseTextSearchTzEntity |
TzMixin, TextSearchMixin |
Tự động build trường text search textSearch |
BaseObjectSearchTzEntity |
TzMixin, ObjectSearchMixin |
Tự động build trường search JSONB objectSearch |
BaseSearchableTzEntity |
TzMixin, TextSearchMixin, ObjectSearchMixin |
Model tìm kiếm tổng hợp (cả text và object search) |
BaseSoftDeleteTzEntity |
TzMixin, SoftDeleteModelMixin |
Hỗ trợ xóa mềm (đánh dấu cờ isDeleted và deletedAt) |
BaseVectorEntity |
TzMixin, VectorMixin |
Hỗ trợ lưu trữ vector embedding (cho tìm kiếm AI / pgvector) |
8. Repository nâng cao
Framework cung cấp lớp TzCrudRepository và SearchableTzCrudRepository kế thừa từ LoopBack repository gốc nhưng được bổ sung cơ chế xử lý timezone và audit user tự động:
import {PostgresDataSource, TzCrudRepository} from '@gennext/lb-infra';
import {inject} from '@gennext/lb-infra/lb-core';
export class ProductRepository extends TzCrudRepository<Product> {
constructor(@inject('datasources.postgres') dataSource: PostgresDataSource) {
super(Product, dataSource);
}
// Ví dụ custom query phức tạp (sử dụng Knex query builder của PostgresDataSource)
async findActiveProductsWithMinPrice(minPrice: number): Promise<Product[]> {
const table = this.modelClass.definition.settings.postgresql.table;
const query = this.dataSource
.connector!.execute(`SELECT * FROM ${table} WHERE price >= $1 AND is_deleted = false`, [minPrice]);
return query;
}
}9. Bộ tạo Controller tự động (Controller Generator)
Framework cho phép sinh nhanh các route REST chuẩn RESTful chỉ qua cấu hình tùy chọn:
1. defineCrudController
Sinh ra các endpoints: GET /, POST /, PATCH /, GET /count, GET /find-one, GET /{id}, PATCH /{id}, PUT /{id}, DELETE /{id}.
import {defineCrudController} from '@gennext/lb-infra';
import {api} from '@gennext/lb-infra/lb-rest';
const BaseController = defineCrudController<Product>({
entity: Product,
repository: {name: 'ProductRepository'},
controller: {
basePath: '/products',
readonly: false, // Nếu true, chỉ sinh ra các route GET/count/find-one
defaultLimit: 100
},
doInjectCurrentUser: true, // Tự động inject context user vào audit log
doDeleteWithReturn: true // delete trả về object {id}
});
@api({basePath: '/products'})
export class ProductController extends BaseController {
// Bạn có thể kế thừa và viết đè hoặc bổ sung custom method tại đây
}2. defineKVController
Dành cho mô hình Key-Value lưu vào memory hoặc Redis:
- Endpoints sinh ra:
GET /{key},POST /{key},DELETE /{key}.
3. defineRelationCrudController / defineRelationViewController
Dành cho quan hệ (HasMany, BelongsTo, HasManyThrough). Sinh ra các REST API quản lý mối quan hệ giữa Source và Target model (ví dụ: POST /users/{id}/posts, GET /users/{id}/posts).
10. Hướng dẫn thiết kế Custom Controller - Service - Repository
Framework tuân thủ nghiêm ngặt quy tắc phân tách trách nhiệm (Separation of Concerns):
- Model: Định nghĩa cấu trúc dữ liệu và các mixin.
- Repository: Thực hiện truy vấn DB qua SQL/Knex.
- Service: Chứa logic nghiệp vụ, tính toán, gọi API ngoài, điều phối nghiệp vụ.
- Controller: Xử lý HTTP routing, nhận request, kiểm tra định dạng và gọi Service.
Bước 1: Khai báo Model
import {BaseUserAuditTzEntity} from '@gennext/lb-infra';
import {model, property} from '@gennext/lb-infra/lb-repo';
@model({
settings: {
postgresql: {
schema: 'public',
table: 'order',
},
strict: true,
},
})
export class Order extends BaseUserAuditTzEntity {
@property({type: 'string', required: true})
code: string;
@property({type: 'number', required: true})
amount: number;
}Bước 2: Tạo Repository
import {PostgresDataSource, TzCrudRepository} from '@gennext/lb-infra';
import {inject} from '@gennext/lb-infra/lb-core';
export class OrderRepository extends TzCrudRepository<Order> {
constructor(
@inject('datasources.postgres') dataSource: PostgresDataSource
) {
super(Order, dataSource);
}
}Bước 3: Tạo Service chứa Business Logic
import {BaseCrudService, BindingKeys} from '@gennext/lb-infra';
import {inject} from '@gennext/lb-infra/lb-core';
import {OrderRepository} from '../repositories/order.repository';
import {Order} from '../models/order.model';
export class OrderService extends BaseCrudService<Order> {
constructor(
@inject(BindingKeys.repositoryFor('OrderRepository'))
private orderRepository: OrderRepository
) {
// Gọi constructor của BaseCrudService nhận 1 object { scope, repository }
super({ scope: OrderService.name, repository: orderRepository });
}
// Phương thức chứa logic nghiệp vụ đặc thù
async createOrderWithValidation(data: Order): Promise<Order> {
if (data.amount <= 0) {
throw new Error('Order amount must be positive');
}
// Sử dụng context trống nếu không truyền currentUser
return this.orderRepository.create(data, {});
}
}Bước 4: Tạo Custom Controller
import {BaseController, BindingKeys} from '@gennext/lb-infra';
import {api, post, requestBody} from '@gennext/lb-infra/lb-rest';
import {inject} from '@gennext/lb-infra/lb-core';
import {OrderService} from '../services/order.service';
import {Order} from '../models/order.model';
@api({basePath: '/orders'})
export class OrderController extends BaseController {
constructor(
@inject(BindingKeys.serviceFor('OrderService'))
private orderService: OrderService
) {
super({scope: OrderController.name});
}
@post('/')
async create(@requestBody() data: Order): Promise<Order> {
try {
return await this.orderService.createOrderWithValidation(data);
} catch (err) {
this.logger.error('Failed to create order: %s', err.message);
throw err;
}
}
}11. Các Components tích hợp sẵn
Framework tích hợp sẵn các LoopBack 4 Components hỗ trợ bootstrap nhanh:
AuthenticateComponent- Mục đích: Quản lý phiên đăng nhập và định danh (JWT, Basic, OAuth2).
- Cấu hình: Bind cấu hình qua
AuthenticateKeys.TOKEN_OPTIONSvàAuthenticateKeys.REST_OPTIONS.
AuthorizeComponent- Mục đích: Phân quyền chi tiết người dùng sử dụng Casbin authorization adapter.
MigrationComponent- Mục đích: Quản lý chạy tự động các tệp tin schema migration SQL/TS lúc boot ứng dụng.
HealthCheckComponent- Mục đích: Endpoint
/healthcung cấp thông tin trạng thái hoạt động của hệ thống.
- Mục đích: Endpoint
MailComponent- Mục đích: Quản lý gửi mail tích hợp BullMQ để gửi bất đồng bộ qua SMTP / Mailgun.
SocketIOComponent- Mục đích: Quản lý Socket.IO server tích hợp Redis adapter phục vụ phân tán.
GrpcServerComponent- Mục đích: Khởi tạo và khởi chạy gRPC server của hệ thống.
CrashReportComponent- Mục đích: Ghi lại dấu vết lỗi hệ thống sang các bên thứ 3 (Sentry / MT service).
StaticAssetComponent- Mục đích: Hỗ trợ upload, quản lý file và tích hợp với Object Storage (MinIO/S3).
12. Hệ thống Helpers dùng chung
Các helper cung cấp giao diện API đồng bộ và dễ sử dụng:
Logger Helper:
import {LoggerFactory} from '@gennext/lb-infra'; const logger = LoggerFactory.getLogger('CustomScope'); logger.info('Message log: %s', 'value');Redis Helper:
import {RedisHelper} from '@gennext/lb-infra'; const redis = new RedisHelper({host: '127.0.0.1', port: 6379}); await redis.set('key', 'value');Queue Helpers:
// 1. QueueHelper (Hàng đợi trong bộ nhớ) import {QueueHelper} from '@gennext/lb-infra'; const inMemoryQueue = new QueueHelper<string>({ identifier: 'my-in-memory-queue', onMessage: async ({ queueElement }) => { console.log('Processing:', queueElement.payload); } }); await inMemoryQueue.enqueue('task-payload'); // 2. BullMQHelper (Hàng đợi Redis-backed) import {BullMQHelper} from '@gennext/lb-infra'; import Redis from 'ioredis'; const connection = new Redis({ host: '127.0.0.1', port: 6379 }); // Client Role: queue (để đẩy job vào hàng đợi) const bullQueue = new BullMQHelper<string>({ queueName: 'email-tasks', identifier: 'email-queue-client', role: 'queue', connection, }); await bullQueue.queue.add('send-email', 'payload'); // Server Role: worker (để xử lý job) const bullWorker = new BullMQHelper<string>({ queueName: 'email-tasks', identifier: 'email-worker-server', role: 'worker', connection, onWorkerData: async (job) => { console.log('Processing job data:', job.data); } });Crypto Helper:
import {AES} from '@gennext/lb-infra'; const aes = AES.withAlgorithm('aes-256-cbc'); const encrypted = aes.encrypt('plain-text', 'secret-key');Storage Helper (MinIO):
import {MinioHelper} from '@gennext/lb-infra'; const storage = new MinioHelper({ endPoint: '127.0.0.1', port: 9000, useSSL: false, accessKey: 'minioadmin', secretKey: 'minioadmin', }); const files = [{ originalname: 'file.txt', mimetype: 'text/plain', buffer: Buffer.from('hello world'), size: 11, encoding: 'utf-8', }]; const result = await storage.upload({ bucket: 'bucket-name', files, });
13. Cơ chế mở rộng (Extension Point)
Consumer Application có thể mở rộng các chức năng của framework mà không cần sửa mã nguồn gốc bằng các cách sau:
- Custom Mixins: Tạo các mixin riêng bằng cách bọc quanh
BaseEntityhoặc các model hiện có và truyền vào cấu hình của mình. - Custom Components: Viết các LoopBack component tuân thủ giao diện
Componentcủa Loopback và cắm vào thông quathis.component(MyComponent). - Custom Datasource: Đăng ký các Connector mới bằng cách viết các lớp kế thừa
juggler.DataSource.
14. Best Practices trong phát triển dự án
- Sử dụng Package Aliases: Luôn dùng
@gennext/lb-infra/lb-core,@gennext/lb-infra/lb-rest,@gennext/lb-infra/lb-repođể tránh xung đột phiên bản của các thư viện LoopBack. - Quản lý Binding Keys đúng cách: Phân biệt rõ hai loại binding key để tránh lỗi runtime khi gọi
@inject:- Với repository/service NỘI BỘ của framework: Sử dụng các key tĩnh định nghĩa sẵn như
BindingKeys.REPOSITORIES.User,BindingKeys.SERVICES.JWTToken. - Với repository/service do ứng dụng TỰ TẠO: Sử dụng các static helper dynamic để sinh key như
BindingKeys.repositoryFor('ProductRepository')hoặcBindingKeys.serviceFor('ProductService').
- Với repository/service NỘI BỘ của framework: Sử dụng các key tĩnh định nghĩa sẵn như
- Phân tách tầng logic: Luôn tuân thủ quy tắc: HTTP validation / Response formatting nằm ở Controller -> Logic xử lý nghiệp vụ ở Service -> Query Database / Transaction ở Repository.
- Không đổi schema đa ngôn ngữ (i18n): Lưu trữ các trường đa ngôn ngữ trực tiếp dưới dạng literal text indexable thay vì dynamic path-lookup để tối ưu hiệu năng tìm kiếm Postgres.
- Clamp limit truy vấn: Luôn sử dụng helper
applyLimitđể đảm bảo hệ thống tự động clamp các limit quá lớn từ client vềApp.MAX_QUERY_LIMIT(giá trị thực tế là1000) nhằm tránh lỗi quá tải bộ nhớ.
15. Hướng dẫn sử dụng gRPC (5 bước)
Module gRPC được xuất từ @gennext/lb-infra/grpc. Quy trình triển khai dịch vụ gRPC gồm 5 bước:
Bước 1: Tạo file định nghĩa Proto
Tệp: src/protos/user.proto
syntax = "proto3";
package demo;
service UserService {
rpc GetUser (GetUserRequest) returns (GetUserResponse);
}
message GetUserRequest {
int32 id = 1;
}
message GetUserResponse {
int32 id = 1;
string email = 2;
}
Bước 2: Tạo gRPC Controller
import {BaseGrpcController, grpcController, grpcMethod} from '@gennext/lb-infra/grpc';
@grpcController()
export class UserGrpcController extends BaseGrpcController {
constructor() {
super({scope: UserGrpcController.name});
}
@grpcMethod({
proto: 'user.proto',
service: 'demo.UserService',
method: 'GetUser',
})
async getUser(request: {id: number}): Promise<{id: number; email: string}> {
return {
id: request.id,
email: `user-${request.id}@example.com`,
};
}
}Bước 3: Cấu hình gRPC Options & Register Component
import {BaseApplication} from '@gennext/lb-infra';
import {GrpcServerComponent, GrpcServerKeys, IGrpcServerOptions} from '@gennext/lb-infra/grpc';
import {ServerCredentials} from '@grpc/grpc-js';
import {join} from 'node:path';
import {UserGrpcController} from './controllers/user-grpc.controller';
export class MyApplication extends BaseApplication {
staticConfigure(): void {
this.bind<IGrpcServerOptions>(GrpcServerKeys.GRPC_OPTIONS).to({
identifier: 'my-grpc-server',
protoFolder: join(__dirname, 'protos'),
address: '0.0.0.0:50051',
credentials: ServerCredentials.createInsecure(),
});
this.grpcController(UserGrpcController);
this.component(GrpcServerComponent);
}
}Bước 4: Gọi gRPC Client thông thường
import {initializeGrpcClient} from '@gennext/lb-infra/grpc';
import * as grpc from '@grpc/grpc-js';
const userClient = initializeGrpcClient<any>({
serviceClass: UserService, // UserService loaded from proto
address: '127.0.0.1:50051',
credentials: grpc.ChannelCredentials.createInsecure(),
});Bước 5: Gọi gRPC dưới dạng Repository-Style
import {GrpcDataSource, GrpcRepository} from '@gennext/lb-infra/grpc';
import * as grpc from '@grpc/grpc-js';
const dataSource = new GrpcDataSource<any>({
dsConfig: {
host: '127.0.0.1',
port: 50051,
credentials: grpc.ChannelCredentials.createInsecure(),
serviceClassResolver: () => UserService,
},
});
class UserGrpcRepository extends GrpcRepository<any> {
constructor() {
super({dataSource, scope: UserGrpcRepository.name});
}
async getUser(id: number): Promise<any> {
return new Promise((resolve, reject) => {
this.getServiceClient().GetUser({id}, (error: any, response: any) => {
if (error) return reject(error);
resolve(response);
});
});
}
}