0

Có thể bạn chưa biết cách viết API như thế này với Express

Khi tôi lần đầu "đào sâu" vào mã nguồn của n8n trong quá trình làm việc với LiveAPI, tôi đã kỳ vọng một cấu trúc Express quen thuộc:

  • Một file routes chính import các module khác,
  • Mỗi route sẽ gọi đến một controller,
  • Controller sẽ đi qua một trình xác thực (validator),
  • Và cuối cùng đến service để xử lý logic.

Đây là cách thiết lập tiêu chuẩn trong hầu hết các backend dùng Node.js – như trong các dự án FlowiseAI hay NodeBB.

Nhưng còn n8n? Nó hoàn toàn đảo ngược kỳ vọng của tôi.

Thay vì luồng route → controller → service thông thường, n8n sử dụng cách tiếp cận đậm chất TypeScript, dựa vào decorator – khiến nó trông giống NestJS hơn là Express truyền thống.

Đây thực sự là một bất ngờ thú vị và tôi đã nghĩ ngay: “Phải chia sẻ điều này thôi.”

File Server.ts: Một thiết lập Express tùy chỉnh

Trái tim của backend trong n8n nằm ở class Server, mở rộng từ AbstractServer, thiết lập một API dựa trên Express.

Nhưng không chỉ là một app Express đơn giản với vài route:

  • Tải controller động: Dựa vào cấu hình môi trường, các controller cho xác thực (LDAP, SAML), quản lý mã nguồn, xác thực đa yếu tố (MFA), và nhiều hơn nữa được nạp khi runtime.
  • Xử lý webhook & sự kiện thời gian thực: WebSocket, giám sát bằng Prometheus, và bộ nhớ đệm được tích hợp.
  • Sử dụng middleware thông minh: Helmet, CSP, phân tích cookie, và giới hạn tốc độ đều được cấu hình động.
  • Phục vụ cả SPA và tài nguyên frontend: Route API và frontend cùng tồn tại.

Decorator – Cách định nghĩa API mới mẻ

n8n loại bỏ thư mục routes truyền thống và thay bằng các decorator TypeScript để định nghĩa endpoint API.

Đây chính là lúc bạn bắt đầu cảm thấy nó rất giống với NestJS.

Hãy xem ví dụ trong route.ts:

Không còn route file truyền thống – Chỉ dùng Decorator

Các decorator định nghĩa phương thức API một cách động:

import type { RequestHandler } from 'express';

import { getRouteMetadata } from './controller.registry';
import type { Controller, Method, RateLimit } from './types';

const RouteFactory =
    (method: Method) =>
    (path: `/${string}`, options: RouteOptions = {}): MethodDecorator =>
    (target, handlerName) => {
        const routeMetadata = getRouteMetadata(target.constructor as Controller, String(handlerName));
        routeMetadata.method = method;
        routeMetadata.path = path;
        routeMetadata.middlewares = options.middlewares ?? [];
    };

export const Get = RouteFactory('get');
export const Post = RouteFactory('post');
export const Put = RouteFactory('put');
export const Patch = RouteFactory('patch');
export const Delete = RouteFactory('delete');

Cách tiếp cận này giúp:

  • Không cần khai báo route thủ công,
  • Mỗi method trong controller chỉ cần gắn @Get(), @Post(), v.v.,
  • Và n8n sẽ xử lý routing phía sau hậu trường.

Phép màu của @RestController

Thay vì phải tự định nghĩa router trong Express theo cách thủ công, decorator @RestController sẽ tự động đăng ký toàn bộ controller:

import { Service } from '@n8n/di';

import { getControllerMetadata } from './controller.registry';
import type { Controller } from './types';

export const RestController =
    (basePath: `/${string}` = '/'): ClassDecorator =>
    (target) => {
        const metadata = getControllerMetadata(target as unknown as Controller);
        metadata.basePath = basePath;
        return Service()(target);
    };

Decorator này tự động đăng ký controller và ánh xạ các route tương ứng với các phương thức bên trong class.

Ví dụ: Active Workflows API

Controller ActiveWorkflowsController (nguồn code) dùng để xử lý các API liên quan đến workflow đang hoạt động:

import { Get, RestController } from '@/decorators';
import { ActiveWorkflowRequest } from '@/requests';
import { ActiveWorkflowsService } from '@/services/active-workflows.service';

@RestController('/active-workflows')
export class ActiveWorkflowsController {
    constructor(private readonly activeWorkflowsService: ActiveWorkflowsService) {}

    @Get('/')
    async getActiveWorkflows(req: ActiveWorkflowRequest.GetAllActive) {
        return await this.activeWorkflowsService.getAllActiveIdsFor(req.user);
    }

    @Get('/error/:id')
    async getActivationError(req: ActiveWorkflowRequest.GetActivationError) {
        const { user, params: { id: workflowId } } = req;
        return await this.activeWorkflowsService.getActivationError(workflowId, user);
    }
}

Mỗi phương thức API chỉ là một hàm bên trong class, và decorators lo hết phần còn lại – từ ánh xạ route, xác thực, kiểm tra quyền, cho đến xác thực request.

Tầng Service: Dependency Injection & truy cập cơ sở dữ liệu

n8n sử dụng dependency injection (thông qua @n8n/di) để quản lý các service.

Ví dụ, ActiveWorkflowsService xử lý các thao tác liên quan đến workflow:

@Service()
export class ActiveWorkflowsService {
    constructor(
        private readonly logger: Logger,
        private readonly workflowRepository: WorkflowRepository,
        private readonly sharedWorkflowRepository: SharedWorkflowRepository,
        private readonly activationErrorsService: ActivationErrorsService,
    ) {}

    async getAllActiveIdsFor(user: User) {
        const activationErrors = await this.activationErrorsService.getAll();
        const activeWorkflowIds = await this.workflowRepository.getActiveIds();

        const hasFullAccess = user.hasGlobalScope('workflow:list');
        if (hasFullAccess) {
            return activeWorkflowIds.filter((workflowId) => !activationErrors[workflowId]);
        }

        const sharedWorkflowIds =
            await this.sharedWorkflowRepository.getSharedWorkflowIds(activeWorkflowIds);
        return sharedWorkflowIds.filter((workflowId) => !activationErrors[workflowId]);
    }
}

Lợi ích:

  • Dễ dàng thay thế các thành phần mà không ảnh hưởng đến phần còn lại của hệ thống.
  • Dễ test: Có thể mock các dependency trong các unit test.
  • Code trở nên module hóa và dễ bảo trì hơn rất nhiều.

Kết luận

Nếu bạn xuất phát từ nền tảng Express.js truyền thống, cách tổ chức API của n8n có thể khiến bạn cảm thấy lạ lẫm lúc đầu.

Nhưng một khi bạn vượt qua cảm giác bối rối kiểu "các route đâu hết rồi?", và hiểu được rằng các decorator xử lý routing, xác thực, và middleware như thế nào, bạn sẽ thấy nó thực sự hợp lý.

Thông qua việc sử dụng decorator và dependency injection, n8n đã làm được điều mà nhiều hệ thống lớn mong muốn:

  • API được module hóa,
  • Dễ bảo trì,
  • Và rất dễ mở rộng.

Vậy nên, lần tới khi bạn xây dựng một backend với Express, hãy thử tích hợp một vài decorator xem sao. Biết đâu bạn lại thích cách tiếp cận này đấy!

Nếu bạn đang phát triển một ứng dụng phụ thuộc nhiều vào API, thì cấu trúc này rất đáng để đưa vào danh sách lựa chọn của bạn.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí