Skip to main content
LibreChat is joining ClickHouse to power the open-source Agentic Data Stack 🎉 Learn more
LibreChat

Các tiêu chuẩn và quy ước mã nguồn

Các tiêu chuẩn lập trình, ranh giới không gian làm việc và các quy ước để đóng góp cho LibreChat.

Giới hạn không gian làm việc

LibreChat là một monorepo. Tất cả mã mới nên nhắm mục tiêu vào workspace chính xác:

Không gian làm việcNgôn ngữPhíaMục đích
/apiJS (kế thừa)BackendMáy chủ Express — hạn chế thay đổi tại đây
/packages/apiTypeScriptBackendMã backend mới nằm tại đây (chỉ dùng TS, được /api tiêu thụ)
/packages/data-schemasTypeScriptBackendCác mô hình/lược đồ cơ sở dữ liệu và logic chia sẻ dành riêng cho cơ sở dữ liệu
/packages/data-providerTypeScriptChia sẻCác kiểu API, endpoint, dịch vụ dữ liệu — được sử dụng bởi frontend và backend
/clientTypeScript/ReactFrontendFrontend SPA
/packages/clientTypeScriptFrontendCác tiện ích frontend chia sẻ
  • Tất cả mã backend mới phải là TypeScript trong /packages/api.
  • Giữ các thay đổi đối với /api ở mức tối thiểu tuyệt đối (các trình bao bọc JS mỏng gọi vào /packages/api).
  • Logic dùng chung dành riêng cho cơ sở dữ liệu nằm trong /packages/data-schemas.
  • Logic API dùng chung cho frontend/backend (endpoints, types, data-service) nằm trong /packages/data-provider.
  • Xây dựng tất cả mã đã biên dịch từ thư mục gốc của dự án: npm run build.
  • Xây dựng lại mã data-provider dùng chung sau khi thay đổi API/kiểu dữ liệu: npm run build:data-provider.

Hướng dẫn chung

  • Sử dụng các nguyên tắc "clean code": giữ cho các hàm và mô-đun nhỏ gọn, tuân thủ nguyên tắc trách nhiệm duy nhất (single responsibility principle), và viết mã nguồn dễ hiểu, biểu cảm.
  • Sử dụng tên biến và tên hàm có ý nghĩa và mang tính mô tả.
  • Ưu tiên khả năng đọc và khả năng bảo trì của mã nguồn hơn là sự ngắn gọn.
  • Sử dụng các tệp .eslintrc.prettierrc được cung cấp để đảm bảo định dạng mã nhất quán.
  • Sửa tất cả các lỗi định dạng lint bằng cách sử dụng tính năng tự động sửa khi có sẵn. Tất cả các cảnh báo và lỗi TypeScript/ESLint phải được giải quyết.

Đặt tên và Tổ chức tệp tin

  • Sử dụng tên tệp một từ bất cứ khi nào có thể, chẳng hạn như permissions.ts, capabilities.ts, hoặc service.ts.
  • Khi cần nhiều từ, hãy ưu tiên sử dụng thư mục một từ để cung cấp ngữ cảnh cho tệp, ví dụ như admin/capabilities.ts thay vì adminCapabilities.ts.
  • Hãy để thư mục cung cấp ngữ cảnh. Ưu tiên app/service.ts hơn app/appConfigService.ts.

Cấu trúc mã nguồn

  • Không lồng ghép (Never-nesting): sử dụng early return (trả về sớm), mã nguồn phẳng, giảm thiểu thụt lề. Chia nhỏ các thao tác phức tạp thành các hàm hỗ trợ có tên gọi rõ ràng.
  • Ưu tiên chức năng (Functional first): sử dụng các hàm thuần túy (pure functions), dữ liệu bất biến (immutable data), map/filter/reduce thay vì các vòng lặp mệnh lệnh (imperative loops). Chỉ sử dụng OOP khi nó thực sự cải thiện việc mô hình hóa miền (domain modeling) hoặc đóng gói trạng thái (state encapsulation).
  • Không sử dụng dynamic imports trừ khi thực sự cần thiết.
  • Trích xuất logic lặp lại thành các hàm tiện ích chuyên dụng (DRY). Ưu tiên sử dụng các hàm hỗ trợ có tham số, hằng số, bộ xác thực dùng chung, xử lý lỗi tập trung và các kiểu dữ liệu dùng chung thay vì các triển khai gần như trùng lặp.

Lặp lại và Hiệu suất

  • Giảm thiểu việc lặp — đặc biệt là trên các cấu trúc dữ liệu dùng chung như mảng tin nhắn, vốn được lặp lại thường xuyên. Mỗi lượt duyệt bổ sung đều sẽ tích tụ dần khi mở rộng quy mô.
  • Hợp nhất các thao tác O(n) tuần tự thành một lần duyệt duy nhất bất cứ khi nào có thể; không bao giờ lặp qua cùng một tập hợp hai lần nếu công việc đó có thể được kết hợp.
  • Chọn các cấu trúc dữ liệu giúp giảm nhu cầu lặp (ví dụ: Map/Set để tra cứu thay vì Array.find/Array.includes).
  • Tránh tạo đối tượng không cần thiết; cân nhắc sự đánh đổi giữa không gian và thời gian.
  • Ngăn chặn rò rỉ bộ nhớ: hãy cẩn thận với closures, giải phóng tài nguyên/event listeners, tránh các tham chiếu vòng (circular references).

An toàn kiểu dữ liệu

  • Không bao giờ sử dụng any. Phải có kiểu dữ liệu tường minh cho tất cả các tham số, giá trị trả về và biến.
  • Giới hạn unknown — tránh sử dụng unknown, Record<string, unknown> và các khẳng định as unknown as T. Một Record<string, unknown> gần như luôn là dấu hiệu cho thấy thiếu định nghĩa kiểu dữ liệu tường minh.
  • Đừng trùng lặp các kiểu (types) — hãy kiểm tra xem một kiểu đã tồn tại trong dự án hay chưa (đặc biệt là trong packages/data-provider) trước khi định nghĩa một kiểu mới. Hãy tái sử dụng và mở rộng các kiểu hiện có.
  • Sử dụng union types, generics và interfaces một cách phù hợp.

Ghi chú và Tài liệu

  • Viết mã tự giải thích; không sử dụng chú thích nội dòng để tường thuật chức năng của mã.
  • Chỉ sử dụng JSDoc cho các logic phức tạp/không rõ ràng hoặc để hỗ trợ intellisense trên các public API.
  • Sử dụng JSDoc một dòng cho tài liệu ngắn gọn, nhiều dòng cho các trường hợp phức tạp.
  • Tránh sử dụng các chú thích // độc lập trừ khi thực sự cần thiết.

Thứ tự nhập (Import Order)

Các import được sắp xếp thành ba phần (theo thứ tự):

  1. Nhập gói (Package imports) — được sắp xếp theo độ dài dòng từ ngắn nhất đến dài nhất (react luôn là mục nhập đầu tiên).
  2. import type imports — được sắp xếp từ dài nhất đến ngắn nhất (các kiểu package trước, sau đó đến các kiểu cục bộ; việc sắp xếp theo độ dài được đặt lại giữa các nhóm con).
  3. Local/project imports — được sắp xếp từ dài nhất đến ngắn nhất.
  • Hợp nhất các lệnh import giá trị từ cùng một module nhiều nhất có thể.
  • Luôn sử dụng import type { ... } độc lập cho các kiểu dữ liệu (type imports); không bao giờ sử dụng từ khóa type nội dòng bên trong các import giá trị (ví dụ: import { Foo, type Bar } là sai).

Tùy chọn vòng lặp

  • Hạn chế tối đa việc lặp. Ưu tiên các phép biến đổi một lần (single-pass) và tránh lặp lại trên cùng một dữ liệu.
  • for (let i = 0; ...) cho các thao tác quan trọng về hiệu suất hoặc phụ thuộc vào chỉ mục.
  • for...of cho việc lặp mảng đơn giản.
  • for...in chỉ dành cho việc liệt kê các thuộc tính của đối tượng.

Máy chủ API Node.js

Thiết kế API

  • Tuân thủ các nguyên tắc RESTful khi thiết kế API.
  • Sử dụng các tên có ý nghĩa và mang tính mô tả cho các routes, controllers, services và models.
  • Sử dụng các phương thức HTTP thích hợp (GET, POST, PUT, DELETE) cho mỗi route.
  • Sử dụng các mã trạng thái và cấu trúc phản hồi phù hợp để đảm bảo tính nhất quán cho các phản hồi API (2xx cho thành công, 4xx cho yêu cầu không hợp lệ từ phía client, 5xx cho lỗi máy chủ).
  • Sử dụng các khối try-catch để bắt và xử lý ngoại lệ một cách khéo léo.
  • Triển khai xử lý lỗi đúng cách và luôn trả về các phản hồi lỗi phù hợp.
  • Sử dụng hệ thống ghi nhật ký có sẵn trong thư mục utils để ghi lại các sự kiện và lỗi quan trọng.
  • Thực hiện xác thực không trạng thái (stateless) dựa trên JWT bằng cách sử dụng middleware requireJWTAuth.

Cấu trúc tệp

Mã backend mới được đặt trong /packages/api dưới dạng TypeScript. Thư mục /api cũ tuân theo cấu trúc này:

Các tuyến đường (Routes)

Chỉ định từng phương thức yêu cầu HTTP, bất kỳ middleware nào được sử dụng và hàm controller sẽ được gọi cho mỗi route.

  • Định nghĩa các route bằng Express Router trong các tệp riêng biệt cho từng tài nguyên hoặc nhóm logic.
  • Sử dụng tên route mang tính mô tả và tuân thủ các quy ước RESTful.
  • Giữ cho các route ngắn gọn và tập trung vào một trách nhiệm duy nhất.
  • Thêm tiền tố /api vào tất cả các route.

Các bộ điều khiển (Controllers)

Chứa logic cho từng route, bao gồm việc gọi các hàm dịch vụ thích hợp và trả về mã trạng thái phản hồi cũng như nội dung JSON tương ứng.

  • Tạo một tệp controller riêng cho mỗi route để xử lý logic request/response.
  • Đặt tên các tệp controller theo quy ước PascalCase và thêm hậu tố "Controller" vào tên tệp (ví dụ: UserController.js).
  • Giữ cho các controller gọn nhẹ bằng cách ủy quyền các thao tác phức tạp cho các tệp service hoặc model.

Các dịch vụ

Chứa các logic nghiệp vụ phức tạp hoặc các thao tác được chia sẻ giữa nhiều controller.

  • Đặt tên các tệp dịch vụ theo quy ước PascalCase và thêm hậu tố "Service" vào tên tệp (ví dụ: AuthService.js).
  • Tránh việc gắn kết chặt chẽ các dịch vụ với những model hoặc cơ sở dữ liệu cụ thể để tăng khả năng tái sử dụng.
  • Duy trì nguyên lý trách nhiệm duy nhất (single responsibility principle) trong mỗi service.

Các Model

Định nghĩa các Mongoose model để biểu diễn các thực thể dữ liệu và mối quan hệ giữa chúng.

  • Sử dụng tên ở dạng số ít, PascalCase cho các tệp model và các collection liên quan (ví dụ: User.js và collection users).
  • Chỉ bao gồm các trường, chỉ mục và xác thực cần thiết trong các model.
  • Giữ cho các model độc lập với lớp API bằng cách tránh tham chiếu trực tiếp đến các đối tượng request/response.

Truy cập cơ sở dữ liệu (MongoDB và Mongoose)

  • Sử dụng Mongoose (https://mongoosejs.com) làm ODM cho MongoDB.
  • Tạo các tệp mô hình riêng biệt cho từng thực thể và đảm bảo sự phân tách rõ ràng về các mối quan tâm (separation of concerns).
  • Sử dụng kiểm chứng lược đồ Mongoose để đảm bảo tính toàn vẹn của dữ liệu.
  • Xử lý các kết nối cơ sở dữ liệu một cách hiệu quả và tránh rò rỉ kết nối.
  • Sử dụng các trình xây dựng truy vấn Mongoose để tạo các truy vấn cơ sở dữ liệu ngắn gọn và dễ đọc.

React Client

Các phương pháp hay nhất về TypeScript và React nói chung

  • Sử dụng TypeScript best practices để tận dụng lợi ích từ static typing và cải thiện công cụ hỗ trợ.
  • Nhóm các tệp liên quan lại với nhau trong các thư mục tính năng (ví dụ: SidePanel/Memories/).
  • Đặt tên các thành phần (components) theo quy ước PascalCase.
  • Sử dụng các tên gọi ngắn gọn và mang tính mô tả để phản ánh chính xác mục đích của thành phần đó.
  • Chia các thành phần phức tạp thành các thành phần nhỏ hơn, có thể tái sử dụng khi thích hợp.
  • Giữ logic hiển thị trong các component ở mức tối thiểu.
  • Trích xuất các phần có thể tái sử dụng thành các hàm hoặc hook riêng biệt.
  • Áp dụng các định nghĩa kiểu prop bằng cách sử dụng các kiểu hoặc interface của TypeScript.
  • Sử dụng xác thực biểu mẫu khi phù hợp (chúng tôi sử dụng React Hook Form để xác thực và gửi biểu mẫu).

Bản địa hóa

  • Tất cả văn bản hiển thị cho người dùng phải được bản địa hóa bằng cách sử dụng hook useLocalize().
  • Chỉ cập nhật các khóa tiếng Anh trong client/src/locales/en/translation.json (các ngôn ngữ khác được tự động hóa từ bên ngoài).
  • Sử dụng các tiền tố khóa bản địa hóa ngữ nghĩa: com_ui_, com_assistants_, v.v.
  • Luôn cung cấp văn bản dự phòng (fallback text) có ý nghĩa cho các khóa bản địa hóa mới.

Dịch vụ dữ liệu

  • Tạo các hook cung cấp dữ liệu trong client/src/data-provider/[Feature]/queries.ts.
  • Xuất tất cả các hook từ client/src/data-provider/[Feature]/index.ts.
  • Thêm các export tính năng vào client/src/data-provider/index.ts chính.
  • Sử dụng React Query (@tanstack/react-query) cho tất cả các tương tác API.
  • Triển khai cơ chế vô hiệu hóa truy vấn (query invalidation) phù hợp cho các thao tác thay đổi dữ liệu (mutations).
  • Thêm QueryKeys và MutationKeys vào packages/data-provider/src/keys.ts.

Khi thêm tích hợp API dùng chung, hãy cập nhật:

  • packages/data-provider/src/api-endpoints.ts (các endpoint)
  • packages/data-provider/src/data-service.ts (các hàm dịch vụ dữ liệu)
  • packages/data-provider/src/types/queries.ts (Các kiểu TypeScript)

Hiệu năng

  • Ưu tiên hiệu quả bộ nhớ và tốc độ ở quy mô lớn.
  • Triển khai phân trang con trỏ (cursor pagination) phù hợp cho các tập dữ liệu lớn.
  • Tránh việc render lại không cần thiết bằng cách sử dụng các mảng phụ thuộc (dependency arrays) phù hợp.
  • Tận dụng các tính năng lưu trữ đệm (caching) và tải lại dữ liệu nền (background refetching) của React Query.

Kiểm thử và Tài liệu

  • Viết các bài kiểm thử đơn vị (unit tests) cho tất cả các chức năng quan trọng và phức tạp bằng cách sử dụng Jest.
  • Viết các bài kiểm thử tích hợp cho tất cả các API endpoint bằng cách sử dụng Supertest.
  • Viết các bài kiểm thử end-to-end cho tất cả các chức năng phía client bằng cách sử dụng Playwright.
  • Sử dụng tên hàm và trường hợp kiểm thử mang tính mô tả để thể hiện rõ mục đích của bài kiểm thử.
  • Chạy các bài kiểm thử từ thư mục workspace của chúng: cd api && npx jest <pattern>, cd packages/api && npx jest <pattern>, v.v.
  • Bao gồm các trạng thái tải, thành công và lỗi cho các luồng UI/dữ liệu.
  • Sử dụng test/layout-test-utils để render các component trong các bài kiểm thử frontend.
  • Ưu tiên logic thực tế thay vì các mock. Chỉ mock những gì không thể kiểm soát cục bộ, chẳng hạn như các HTTP API bên ngoài, các dịch vụ bị giới hạn tốc độ (rate-limited) và các lệnh gọi hệ thống không xác định.
  • Sử dụng spies khi bạn cần xác nhận các lệnh gọi mà không thay thế việc triển khai cơ bản.
  • Sử dụng mongodb-memory-server cho các bài kiểm thử dựa trên MongoDB để các truy vấn và xác thực lược đồ (schema validation) thực hiện hành vi cơ sở dữ liệu thực tế.

Hướng dẫn này thế nào?