Khi ứng dụng web phát triển lớn hơn, độ phức tạp trong việc phát triển và duy trì hệ thống cũng tăng lên. Một cách phổ biến để giải quyết vấn đề này là sử dụng kiến trúc microservice, trong đó các nhà phát triển chia nhỏ hệ thống thành các thành phần nhỏ được quản lý tốt, có thể được quản lý và mở rộng riêng lẻ.
Để làm điều này một cách hiệu quả, việc sử dụng một framework microservice thường rất hữu ích. Nhưng việc lựa chọn framework phù hợp hỗ trợ native microservices có thể là một thách thức. Trong bài viết này, chúng ta sẽ xem xét Encore.ts và Nest.js như hai lựa chọn thay thế phù hợp, vì cả hai đều hỗ trợ native kiến trúc microservices và TypeScript.
Encore.ts là một framework mã nguồn mở mới nổi bật với hiệu suất cao, tính an toàn về kiểu dữ liệu và các tính năng quan sát. Mặt khác, Nest.js dẫn đầu framework TypeScript để xây dựng các ứng dụng Microservices. Mỗi framework đều có những điểm mạnh riêng, vì vậy chúng tôi sẽ kiểm tra từng framework về mặt kiến trúc, hiệu suất và khả năng mở rộng, đồng thời giải thích cách xác định framework nào có thể phù hợp nhất với bạn.
Trước khi bắt đầu, chúng ta hãy xem dữ liệu điểm chuẩn trong hình ảnh dưới đây:
Dữ liệu điểm chuẩn cho thấy Encore.ts có thể xử lý 121.005 yêu cầu mỗi giây mà không cần xác thực và 107.018 với xác thực lược đồ. Điều đó nhanh hơn đáng kể so với các framework truyền thống. Ví dụ, Express.js với Zod chỉ đạt khoảng 15.707 yêu cầu mỗi giây mà không cần xác thực và 11.878 với xác thực. Vì vậy, Encore.ts nhanh hơn khoảng 9 lần so với Express, framework mà Nestjs được xây dựng dựa trên.
Tổng quan về Encore.ts và NestJS: Sự khác biệt trong cách tiếp cận
Khi bạn bắt đầu một dự án, bạn muốn một framework không chỉ mạnh mẽ mà còn dễ sử dụng cho các nhà phát triển. Encore.ts và NestJS nổi bật khi nói đến các framework Microservice có hỗ trợ tích hợp cho Typescript, nhưng chúng hoạt động theo những cách riêng biệt.
Encore.ts là một framework mã nguồn mở, cloud-native được thiết kế cho phát triển backend với tự động hóa cơ sở hạ tầng tích hợp. Nó cho phép bạn xây dựng các hệ thống phân tán dạng module bằng cách sử dụng các thư viện cơ sở hạ tầng khai báo.
Encore.ts hoạt động trên Rust runtime tích hợp với Node.js thông qua napi để có hiệu suất vượt trội trong việc xử lý I/O và đa luồng trong khi vẫn cho phép bạn viết logic bằng TypeScript.
Dưới đây là một ví dụ đơn giản về cách bạn có thể định nghĩa một service trong Encore.ts:
import { Service } from "encore.dev/service"; export default new Service("hello");
Khi service hello này được tạo, Encore.ts sẽ tự động coi toàn bộ thư mục là một phần của service—không cần cấu hình thêm.
Mặt khác, NestJS có phong cách riêng. Nó là một framework TypeScript linh hoạt cho phép bạn kiểm soát hoàn toàn cách bạn xây dựng ứng dụng, cho bạn tự do cấu trúc mọi thứ theo cách của mình.
Mặc dù không xử lý tự động hóa cơ sở hạ tầng, NestJS giúp dễ dàng tích hợp với hầu hết mọi thư viện của bên thứ ba, điều này mở ra rất nhiều khả năng cho các dự án khác nhau.
Dưới đây là cách bạn có thể định nghĩa một service tương tự trong NestJS:
import { Controller, Get } from '@nestjs/common'; @Controller('hello')
export class HelloWorldController { @Get() sayHello(): string { return 'Hello, World!'; }
}
NestJS cung cấp cho bạn nhiều tính linh hoạt hơn nhưng không có tự động hóa tích hợp như trong Encore.ts.
Kiến trúc, thiết kế và tính năng nổi bật
Kiến trúc của một framework quyết định cách ứng dụng của bạn được xây dựng và duy trì theo thời gian. Cả Encore.ts và NestJS đều mạnh mẽ, nhưng triết lý cốt lõi của chúng khác nhau.
Encore.ts thiên về cloud-first, khiến nó trở nên lý tưởng cho các hệ thống phân tán lớn, an toàn về kiểu dữ liệu với nhiều microservices. Một trong những tính năng nổi bật của nó là hỗ trợ native cho Pub/Sub, cho phép kiến trúc hướng sự kiện liền mạch.
Dưới đây là cách bạn có thể định nghĩa một service hướng sự kiện trong Encore.ts bằng cách sử dụng Pub/Sub:
import { Topic, Subscription } from "encore.dev/pubsub"; // Define the event type for order creation
export interface OrderCreatedEvent { orderId: string;
} // Create a topic for order creation events
export const orders = new Topic<OrderCreatedEvent>("orders", { deliveryGuarantee: "at-least-once",
}); // Create a subscription to listen for the order creation event
export const _ = new Subscription(orders, "process-order", { handler: async (event: OrderCreatedEvent) => { console.log('Order created:', event.orderId); },
});
NestJS, mặc dù có khả năng hỗ trợ microservices và kiến trúc hướng sự kiện, nhưng lại cung cấp một cách tiếp cận module hơn. Cốt lõi của nó tuân theo mô hình MVC và nó cho phép các nhà phát triển xây dựng hệ thống theo cách của họ bằng cách cung cấp quyền kiểm soát nhiều hơn đối với các cấu hình.
Ví dụ, đây là cách bạn có thể định nghĩa các service và sự kiện trong NestJS với cách tiếp cận module hơn:
// order.event.ts
export class OrderCreatedEvent { constructor(public readonly order: Order) {}
} // order.repository.ts
@Injectable()
export class OrderRepository { async save(order: Order): Promise<Order> { // Persistence logic }
} // order.service.ts
@Injectable()
export class OrderService { constructor( private readonly orderRepository: OrderRepository, private readonly eventEmitter: EventEmitter2 ) {} async createOrder(orderDto: CreateOrderDto): Promise<Order> { const order = new Order(orderDto); const savedOrder = await this.orderRepository.save(order); this.eventEmitter.emit( 'order.created', new OrderCreatedEvent(savedOrder) ); return savedOrder; }
} // order.listener.ts
@Injectable()
export class OrderEventListener { @OnEvent('order.created') async handleOrderCreatedEvent(event: OrderCreatedEvent) { // Handle event logic separately }
} // order.module.ts
@Module({ imports: [EventEmitterModule.forRoot()], providers: [ OrderService, OrderRepository, OrderEventListener ], exports: [OrderService]
})
export class OrderModule {}
Theo thiết kế, NestJS cấp rất nhiều quyền kiểm soát đối với cách các thành phần sẽ tương tác, nhưng nhược điểm là nhiều boilerplate hơn và bạn cũng sẽ phải tự quản lý cấu hình cơ sở hạ tầng.
Các tính năng tích hợp và khả năng mở rộng
Trong quá trình phát triển các hệ thống phân tán, các tính năng được cung cấp bởi framework thường sẽ tạo điều kiện thuận lợi cho việc phát triển với nguy cơ gây ra sự phức tạp quá mức. Tính năng nổi bật của Encore.ts là nó cung cấp các cách tự động hóa việc cung cấp cơ sở hạ tầng, cả trong phát triển cục bộ và trong môi trường đám mây.
Điều này bao gồm cơ sở dữ liệu, Pub/Sub, cron job, v.v. Encore.ts cũng cung cấp bảng điều khiển phát triển cục bộ tự động tạo tài liệu API, sơ đồ kiến trúc và theo dõi phân tán. Nó cũng tạo ra các client frontend, bao gồm hỗ trợ đặc tả OpenAPI cho REST API, có thể tiết kiệm rất nhiều thời gian cho nhà phát triển.
Dưới đây là một ví dụ về cách định nghĩa REST API trong Encore.ts, đồng thời tự động tạo tài liệu OpenAPI:
import { api } from "encore.dev/api"; interface CreateOrderParams { productId: string; quantity: number;
} interface OrderResponse { orderId: string; message: string;
} export const createOrder = api( { method: "POST", path: "/orders", expose: true, auth: true }, async (params: CreateOrderParams): Promise<OrderResponse> => { const orderId = "order123"; return { orderId, message: `Order created for product ${params.productId} with quantity ${params.quantity}.`, }; }
);
Với Encore.ts, ngay khi bạn định nghĩa service của mình, tài liệu và sơ đồ sẽ tự động có sẵn mà không cần thiết lập thêm.
NestJS đã trở nên phổ biến nhờ tính linh hoạt của nó. Ngay từ ngày đầu, nó đã hỗ trợ REST, GraphQL và WebSocket một cách dễ dàng, nhưng điều chính đằng sau sự phổ biến của nó là nó dễ dàng kết nối với các thư viện của bên thứ ba. Ví dụ: nếu bạn muốn thêm hỗ trợ GraphQL, đó là một quy trình đơn giản.
import { Resolver, Query, Mutation, Args } from '@nestjs/graphql'; // Define a simple model for an order (this will be your GraphQL type)
class Order { id: string; product: string; quantity: number;
} // Resolver to handle GraphQL queries and mutations
@Resolver(() => Order)
export class OrderResolver { // Array to store orders (simulating a database) private orders: Order[] = []; // Query to get all orders @Query(() => [Order], { name: 'getAllOrders' }) getAllOrders(): Order[] { return this.orders; } // Mutation to create an order @Mutation(() => Order) createOrder( @Args('product') product: string, @Args('quantity') quantity: number, ): Order { const newOrder = { id: Date.now().toString(), product, quantity }; this.orders.push(newOrder); return newOrder; }
}
NestJS giúp việc xây dựng trên các tính năng cốt lõi của nó trở nên đơn giản, nhưng nó không cung cấp cùng mức độ cơ sở hạ tầng tự động và các tính năng như Encore.ts.
Hiệu suất, khả năng nở rộng và triển khai
Hiệu suất rất quan trọng khi xây dựng các hệ thống phân tán, đặc biệt là ở quy mô lớn. Encore.ts được xây dựng để có hiệu suất cao với Rust runtime của nó, giúp xử lý các hoạt động I/O và đa luồng một cách hiệu quả. Tốc độ và tính an toàn bộ nhớ của Rust mang lại cho Encore.ts một lợi thế đáng kể so với các framework hoàn toàn dựa trên Node.js.
Về khả năng mở rộng, Encore.ts là cloud-native và có thể tự động mở rộng bằng cách sử dụng kiến trúc không máy chủ hoặc Kubernetes, tùy thuộc vào chiến lược triển khai của bạn.
NestJS, mặt khác, truyền thống hơn trong cách xử lý hiệu suất và khả năng mở rộng. Vì NestJS hoàn toàn dựa trên TypeScript và JavaScript, nên nó dựa vào các tối ưu hóa hiệu suất mà bạn áp dụng trong quá trình thiết lập. Việc mở rộng ứng dụng NestJS thường liên quan đến việc tự cấu hình Kubernetes, Docker hoặc các nền tảng không máy chủ như AWS Lambda.
Mặc dù NestJS cung cấp tính linh hoạt trong cách bạn mở rộng quy mô, nhưng cấu hình yêu cầu nhiều nỗ lực thủ công hơn so với tự động hóa tích hợp của Encore.ts.
Chúng ta hãy hiểu sự khác biệt về hiệu suất giữa encore.ts và Nest.js từ dữ liệu điểm chuẩn trong hình ảnh dưới đây:
Từ dữ liệu điểm chuẩn, encore.ts nổi bật về hiệu suất, với thời gian khởi động chỉ 8,3 mili giây, trong khi NestJS mất khoảng 143,7 mili giây, khiến nó nhanh hơn gần chín lần so với các framework truyền thống.
Cách bạn triển khai ứng dụng của mình là một yếu tố quan trọng cần cân nhắc đối với bất kỳ dự án nào, đặc biệt là khi nghĩ đến môi trường đám mây. Encore.ts cung cấp một lộ trình dễ dàng để triển khai thông qua các công cụ mã nguồn mở của nó hoặc Encore Cloud Platform.
Sử dụng phiên bản mã nguồn mở, bạn có thể sử dụng encore build để xây dựng dự án của mình và tạo image Docker, sau đó có thể được triển khai ở bất kỳ đâu hỗ trợ Docker:
encore build docker --services=service1,service2 --gateways=api-gateway MY-IMAGE:TAG
Điều này tạo ra một image Docker có thể được triển khai ở bất kỳ đâu. Ngoài ra, nếu bạn chọn sử dụng Encore Cloud Platform, nó sẽ tự động hóa toàn bộ quy trình CI/CD, triển khai trực tiếp lên đám mây của riêng bạn trên AWS hoặc GCP với các tùy chọn không máy chủ hoặc Kubernetes.
Ngược lại, NestJS yêu cầu thiết lập thủ công để triển khai. Thông thường, các nhà phát triển sử dụng Docker để đóng gói các ứng dụng NestJS và triển khai chúng lên nhà cung cấp đám mây mà họ lựa chọn.
Mặc dù điều này cho phép bạn kiểm soát chiến lược triển khai của mình, nhưng nó yêu cầu nhiều cấu hình hơn—ngay cả đối với một ứng dụng đơn giản, bạn cũng cần thực hiện nhiều bước:
- Tạo Dockerfile:
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["npm", "run", "start:prod"]
- Tạo tệp docker-compose.yml: (xem mã ví dụ trong bài gốc)
version: '3.8'
services: api: build: . ports: - "3000:3000" environment: - NODE_ENV=production - DATABASE_URL=postgresql://user:pass@db:5432/myapp depends_on: - db db: image: postgres:13 environment: - POSTGRES_USER=user - POSTGRES_PASSWORD=pass - POSTGRES_DB=myapp
- Tạo quy trình làm việc GitHub Actions cho NestJS: (xem mã ví dụ trong bài gốc)
name: Deploy NestJS
on: push: branches: [ main ]
jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Build and push Docker image run: | docker build -t myapp . docker push myregistry/myapp:latest - name: Deploy to cloud run: | # Add your cloud deployment commands here, e.g., kubectl apply, AWS ECS deploy, etc.
Ứng dụng của bạn càng lớn và bạn càng cần nhiều môi trường dàn dựng và thử nghiệm, thì cách tiếp cận cấu hình thủ công này càng trở nên gánh nặng—liên tục tăng về thời gian dành cho việc bảo trì.
Cân nhắc trường hợp nào nên dùng
Khi lựa chọn giữa Encore.ts và NestJS, quyết định nên dựa trên nhu cầu cụ thể của dự án của bạn. Encore.ts hoàn hảo cho các ứng dụng cloud-first và các hệ thống phân tán lớn được hưởng lợi từ tự động hóa tích hợp. Runtime được hỗ trợ bởi Rust và quản lý cơ sở hạ tầng của nó làm cho nó trở nên lý tưởng cho các kiến trúc hướng sự kiện, microservices và các ứng dụng hiệu suất cao. Cộng đồng đang phát triển nhanh chóng của Encore là một nguồn hỗ trợ đáng tin cậy và tìm cách tích hợp các công cụ của bên thứ ba.
Mặt khác, NestJS tỏa sáng khi cần tính linh hoạt và tùy chỉnh. Nó phù hợp với các ứng dụng doanh nghiệp yêu cầu kiểm soát chi tiết đối với mọi khía cạnh và nơi việc dành thời gian cho cấu hình thủ công là có thể chấp nhận được. Hệ sinh thái tương đối rộng lớn và hỗ trợ cộng đồng của NestJS giúp dễ dàng tìm thấy tài nguyên và các công cụ của bên thứ ba.
Kết luận
Việc lựa chọn giữa Encore.ts và NestJS phụ thuộc vào nhu cầu cụ thể của dự án của bạn. Nếu bạn đang tìm kiếm một framework cloud-native đơn giản, hiệu suất cao với tự động hóa tích hợp, Encore.ts là một lựa chọn tuyệt vời. Nó hợp lý hóa việc phát triển các hệ thống phân tán bằng cách tự động quản lý cơ sở hạ tầng và hiệu suất được hỗ trợ bởi Rust của nó rất khó bị đánh bại.
Tuy nhiên, nếu bạn cần một framework module rất linh hoạt, cho phép bạn kiểm soát mọi khía cạnh nhỏ, thì NestJS có lẽ là con đường nên đi. Khả năng mở rộng và hệ sinh thái lớn của nó làm cho nó trở thành một lựa chọn vững chắc cho các giải pháp doanh nghiệp tùy chỉnh. Cả hai framework đều mạnh mẽ theo cách riêng của chúng và lựa chọn tốt nhất phụ thuộc vào việc bạn coi trọng hiệu suất và sự đơn giản hay tính linh hoạt và kiểm soát hoàn toàn.