0
0
Lập trình
NM

Elysia: Khám Phá Web Framework Mới Cho API

Đăng vào 4 ngày trước

• 11 phút đọc

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 Copy
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 Copy
bun create elysia app
cd app

Bây giờ chỉ cần chạy:

bash Copy
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 Copy
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 Copy
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 Copy
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 Copy
  .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 Copy
@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 Copy
.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 Copy
.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 Copy
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 Copy
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?

  1. 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.
  2. 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.
  3. 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 Copy
.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 Copy
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 Copy
./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!

Gợi ý câu hỏi phỏng vấn
Không có dữ liệu

Không có dữ liệu

Bài viết được đề xuất
Bài viết cùng tác giả

Bình luận

Chưa có bình luận nào

Chưa có bình luận nào