Xây Dựng API với Elysia.js và Bun
Trong thế giới backend với JavaScript, có một câu hỏi lớn: làm thế nào để tận dụng năng suất và tính linh hoạt của ngôn ngữ mà không phải hy sinh hiệu suất và an toàn kiểu? Câu trả lời có thể nằm trong một framework tương đối mới nhưng đầy tiềm năng: Elysia.js.
Chạy trên Bun - một runtime siêu nhanh, Elysia không chỉ là “một framework” nữa. Nó hứa hẹn kết hợp những điều tốt nhất của hai thế giới:
- Hiệu suất vượt trội hơn so với các framework nổi tiếng như Fastify và Gin.
- Đảm bảo an toàn kiểu hoàn toàn, từ request đến response.
- Hệ sinh thái plugin sẵn sàng sử dụng.
Có vẻ tốt đến mức khó tin? Hãy cùng nhau thử nghiệm bằng cách xây dựng một API thực tế với Elysia.
Bắt Đầu
Trước tiên, chúng ta cần cài đặt Bun. Nếu chưa có, bạn có thể cài đặt qua npm:
bash
npm install -g bun
Sau khi đã cài đặt Bun, chúng ta sẽ tạo ứng dụng mới với template chính thức của Elysia:
bash
bun create elysia app
cd app
Bây giờ chỉ cần chạy:
bash
bun dev
Và truy cập vào http://localhost:3000
. Nếu mọi thứ diễn ra thuận lợi, bạn sẽ thấy dòng chữ Hello Elysia.
Tạo Endpoint Đầu Tiên
Bắt đầu đơn giản để hiểu cấu trúc cơ bản. Theo mặc định, bạn đã có một route /
. Hãy thêm một route mới để liệt kê các "To-Dos":
javascript
import { Elysia } from "elysia";
const app = new Elysia()
.get("/", () => "Hello Elysia")
.get("/todo", () => "my todos") // Endpoint mới
.listen(3000);
console.log(
`🦊 Elysia đang chạy tại ${app.server?.hostname}:${app.server?.port}`
);
Dễ dàng đúng không? Nhưng chúng ta biết rằng một API thực sự cần phải xác thực dữ liệu vào. Đây là lúc Elysia tỏa sáng.
Theo mặc định, nó sử dụng thư viện có tên là TypeBox, nhưng hệ sinh thái rất linh hoạt. Như nhiều người trong chúng ta đã sử dụng và yêu thích Zod, hãy tích hợp nó vào dự án.
Đầu tiên, cài đặt Zod:
bash
bun add zod
Bây giờ, hãy tạo một route nhận id
như tham số và sử dụng Zod để đảm bảo rằng nó có kiểu đúng.
javascript
import { Elysia } from "elysia";
import { z } from "zod"; // Nhập Zod
const app = new Elysia()
.get("/", () => "Hello Elysia")
.get("/todo", () => "my todos")
.get(
"/todo/:id",
({ params }) => {
// `params.id` ở đây đã được xác định kiểu!
return {
id: params.id,
};
},
{
// Định nghĩa schema xác thực cho các tham số URL
params: z.object({
id: z.string(),
}),
}
)
.listen(3000);
console.log(
`🦊 Elysia đang chạy tại ${app.server?.hostname}:${app.server?.port}`
);
Với cùng một nguyên tắc, chúng ta sẽ tạo một endpoint POST để tạo một "To-Do" mới, xác thực body của request:
javascript
.post("/todo", ({ body }) => {
// `body` cũng đã được xác định kiểu, nhờ vào Zod.
return {
title: body.title,
description: body.description,
}
},{
// Xác thực body của request
body: z.object({
title: z.string(),
description: z.string(),
}),
})
Thấy không, nó rất sạch sẽ và dễ hiểu? Việc xác thực gắn liền với route, giúp mã dễ đọc và bảo trì.
Tạo Tài Liệu Tự Động
Một lỗi phổ biến của nhiều nhà phát triển là để tài liệu sang một bên (tức là để đó mãi mãi). Với Elysia, việc tạo tài liệu chuyên nghiệp theo tiêu chuẩn OpenAPI (Swagger) là cực kỳ đơn giản.
Đầu tiên, cài đặt plugin:
bash
@elysiajs/openapi
Bây giờ hãy thêm vào mã của chúng ta. Chỉ cần thêm đoạn sau:
javascript
.use(openapi({
mapJsonSchema: {
zod: z.toJSONSchema,
},
}))
Bây giờ, chỉ cần sử dụng plugin trong phiên bản Elysia của chúng ta. Vì chúng ta đang sử dụng Zod, chúng ta cần một cấu hình nhỏ để nó biết cách "dịch" các schema của chúng ta.
javascript
.use(openapi({
mapJsonSchema: {
zod: z.toJSONSchema,
},
}))
Mẹo: Nếu bạn đang sử dụng TypeBox (mặc định của Elysia), bạn sẽ không cần mapJsonSchema
. Nó sẽ hoạt động ngay lập tức.
Xem cách mã của chúng ta đã hoàn thiện cho đến bây giờ:
javascript
import { openapi } from "@elysiajs/openapi";
import { Elysia } from "elysia";
import { z } from "zod";
const app = new Elysia()
.use(openapi({
mapJsonSchema: {
zod: z.toJSONSchema,
},
}))
.get("/", () => "Hello Elysia")
.get("/todo", () => "my todos")
.get(
"/todo/:id",
({ params }) => {
return {
id: params.id,
};
},
{
params: z.object({
id: z.string(),
}),
}
)
.post(
"/todo",
({ body }) => {
return {
title: body.title,
description: body.description,
};
},
{
body: z.object({
title: z.string(),
description: z.string(),
}),
}
)
.listen(3000);
console.log(
`🦊 Elysia đang chạy tại ${app.server?.hostname}:${app.server?.port}`
);
Chạy lại dự án và truy cập http://localhost:3000/openapi
. Bạn sẽ thấy một giao diện của Scalar với tất cả các endpoint đã được tài liệu hóa. Thật kỳ diệu!
Làm Cho API Chức Năng
Cho đến bây giờ, các route của chúng ta chưa làm được gì nhiều. Hãy thêm một số logic để mô phỏng CRUD cho "To-Dos", đồng thời làm phong phú thêm tài liệu của chúng ta.
javascript
import { openapi } from "@elysiajs/openapi";
import { randomUUIDv7 } from "bun";
import { Elysia, NotFoundError } from "elysia";
import { z } from "zod";
// Định nghĩa kiểu cho "To-Do"
type Todo = {
id: string;
title: string;
description: string;
};
// Giả lập một "database" trong bộ nhớ
const todos: Todo[] = [];
const app = new Elysia()
.use(
openapi({
mapJsonSchema: {
zod: z.toJSONSchema,
},
})
)
.get(
"/todo",
() => {
return todos;
},
{
// Thêm thông tin chi tiết cho tài liệu
detail: {
summary: "Lấy tất cả todos",
},
// Định nghĩa kiểu cho phản hồi của request
response: {
200: z.array(
z.object({
id: z.string(),
title: z.string(),
description: z.string(),
})
),
},
}
)
.get(
"/todo/:id",
({ params }) => {
const todo = todos.find((todo) => todo.id === params.id);
if (!todo) {
// Ném ra lỗi NotFound của Elysia
throw new NotFoundError("Không tìm thấy Todo");
}
return todo;
},
{
params: z.object({
id: z.string(),
}),
detail: {
summary: "Lấy todo theo id",
},
response: {
// Bây giờ tài liệu của chúng ta biết mong đợi gì trong từng tình huống
200: z.object({
id: z.string(),
title: z.string(),
description: z.string(),
}),
404: z.string(),
},
}
)
.post(
"/todo",
({ body, set }) => {
const todo = {
id: randomUUIDv7(),
title: body.title,
description: body.description,
};
todos.push(todo);
// Định nghĩa mã trạng thái cho phản hồi
set.status = 201;
return todo;
},
{
body: z.object({
title: z.string(),
description: z.string(),
}),
detail: {
summary: "Tạo todo",
},
response: {
201: z.object({
id: z.string(),
title: z.string(),
description: z.string(),
}),
},
}
)
.listen(3000);
console.log(
`🦊 Elysia đang chạy tại ${app.server?.hostname}:${app.server?.port}`
);
Chúng ta đã làm gì ở đây?
- Giả lập Database: Tạo một mảng todos trong bộ nhớ để lưu trữ dữ liệu và một kiểu Todo để giữ cho mọi thứ được tổ chức.
- Tài liệu phong phú: Sử dụng các trường detail và response để mô tả những gì mỗi endpoint làm và quan trọng hơn, để định kiểu chính xác những gì chúng trả về trong mỗi mã trạng thái (200, 201, 404). Điều này không chỉ làm tài liệu Swagger đẹp mà còn đảm bảo TypeScript sẽ cảnh báo nếu bạn cố gắng trả về điều gì đó khác với những gì đã hứa.
- Logic Kinh Doanh: Thực hiện tìm kiếm và tạo "To-Dos", sử dụng NotFoundError có sẵn của Elysia để xử lý các trường hợp mà item không được tìm thấy.
Xử Lý Lỗi Tập Trung
Trong endpoint /todo/:id
, khi một "To-Do" không được tìm thấy, chúng ta ném ra một NotFoundError. Phản hồi mặc định có thể không phải là tốt nhất cho API của bạn. Vậy tại sao không tùy chỉnh điều này?
Elysia cho phép bạn tạo một global error handler. Thêm đoạn mã sau vào cuối chuỗi phương thức của bạn:
javascript
.onError(({ error }) => {
if (error instanceof NotFoundError) {
return {
status: 404,
body: { message: error.message },
};
}
})
Bằng cách này, bất cứ khi nào một NotFoundError
được ném ra ở bất kỳ đâu trong ứng dụng của bạn, nó sẽ được bắt và định dạng theo cách mà bạn đã xác định. Điều này rất tuyệt để chuẩn hóa các phản hồi lỗi của API của bạn.
Bổ Sung: Tạo Executable
Một trong những tính năng tuyệt vời nhất của Bun là khả năng biên dịch dự án TypeScript của bạn thành một executable duy nhất. Không còn node_modules
trong môi trường sản xuất!
Để làm điều này, hãy chạy lệnh sau trong terminal:
bash
bun build ./src/index.ts --target=bun --minify --compile --outfile app
Điều này sẽ tạo ra một tệp app
(hoặc app.exe
trên Windows). Bây giờ, để chạy API của bạn, chỉ cần thực hiện tệp này:
bash
./app
Và xong! Ứng dụng của bạn, hoàn chỉnh và tối ưu, chạy từ một binary duy nhất.
Kết Luận
Trong bài viết này, chúng ta chỉ mới chạm đến bề mặt của Elysia và Bun. Chúng ta đã thấy cách đơn giản để tạo một API mạnh mẽ, với xác thực, tài liệu tự động và hiệu suất cao, tất cả đều với cú pháp thanh lịch và trực quan.
Hệ sinh thái vẫn còn nhiều điều để cung cấp, như middlewares, xác thực JWT, WebSockets và nhiều hơn nữa.
Hy vọng rằng hướng dẫn này đã khuyến khích bạn khám phá những công cụ này. Nếu bạn thích, hãy để lại phản hồi! Ai biết, có thể chúng ta sẽ biến điều này thành một chuỗi, xây dựng một ứng dụng hoàn chỉnh và khám phá những thực hành tốt nhất từ đầu đến cuối. Cảm ơn bạn!