Facebook Pixel

Lập trình REST API TODO List với Typescript Express

21 Aug, 2024

Tran Thuy Vy

Frontend Developer

Trong bài viết này, mình sẽ hướng dẫn các bạn xây dựng REST API với Typescript, Express và MySQL.

Lập trình REST API TODO List với Typescript Express

Mục Lục

Trong bài viết này, mình sẽ hướng dẫn các bạn xây dựng REST API với Typescript, Express và MySQL. Nếu bạn chưa biết về các syntax cơ bản, các bạn có thể đọc tại đây.

1. Phân tích và thiết lập User Story cơ bản

Bài viết này mình sẽ tập trung vào phần backend, nên mình sẽ để hình phần giao diện đơn giản về TODO List phía dưới các bạn có thể tham khảo qua nhé.

Giao diện todo list

Mình khuyên các bạn nên xây dựng User Story chi tiết, vì nó sẽ giúp bạn tránh được việc sẽ bị sót các tính năng, kiểm soát các tính năng cần cho dự án.

  1. Service không đăng nhập phân biệt người dùng với nhau.
  2. Người dùng có thể tạo mới một TODO Item.
  3. Người dùng có thể xem được toàn bộ các TODO Item (thường sẽ có phân trang), hiển thị tổng TODO Item đã hoàn thành / tổng TODO Item.
  4. Người dùng có thể edit mark done bất kỳ một TODO Item.
  5. Người dùng có thể xoá một TODO Item.

Các bạn theo mảng backend sẽ cảm thấy hơi lạ với việc xây dựng User Story như thế này. Vì các backend developer trong các công ty không cần phải làm việc này. Mình nêu đến để các bạn hiểu rõ quy trình từ phân tích đến triển khai.

2. Thiết kế cơ sở dữ liệu từ User Story

Dựa trên User Story phía trên mình vừa xây dựng, mình có 3 danh từ được in đậm: "Service", "Người dùng", "TODO Item". Và vì trong ví dụ này, không yêu cầu đăng nhập nên "Service" và "Người dùng" có thể không cần quan tâm đến.

Với danh từ "TODO Item" mình sẽ cần chứa title và status. Dựa vào đó ta sẽ xây dựng cơ sở dữ liệu như sau:

  • Id (Primary Key, Auto Increment): định danh (Identifier) cho từng TODO Item, vì là PK nên sẽ không trùng lặp, không thể NULL.
  • Title: tiêu đề cho TODO Item. Cột này chắc chắn sẽ chứa nội dung. Cụ thể trong MySQL thì nó là varchar.
  • Status: trạng thái của TODO Item. Vì chỉ có 2 giá trị các bạn có thể dùng 01. Tuy nhiên mình vẫn thích dùng kiểu Enum để rõ ràng và dễ mở rộng về sau hơn.
  • Created At: Thời gian Item được tạo trên hệ thống. Cột này chỉ là một tuỳ chọn thêm. Theo mình mỗi table nên có cột này để tiện quản lý về sau.
  • Updated At: Thời gian Item được update lần cuối trên hệ thống. Cột này cũng dùng để quản lý thêm thôi.

Phần code tạo bảng trong MySQL như sau:

Sql
CREATE TABLE `todo_items` (
  `id` int NOT NULL AUTO_INCREMENT,
  `title` varchar(150) CHARACTER SET utf8 NOT NULL,
  `status` enum('Doing','Finished') DEFAULT 'Doing',
  `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `updated_at` timestamp NOT NULL ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

Sau khi chạy thì database của mình sẽ như sau:

Database Todo list

3. Thiết kế các REST API cho TODO List service

Mình thấy đây là bước cực kỳ quan trọng, nhưng mình thấy nhiều bạn thường bỏ qua. Nếu bạn chưa hiểu rõ về REST API nên được thiết kế thế nào thì xem lại bài này:

REST API là gì? Cách thiết kế RESTful API bạn chưa biết
REST API là gì? Làm thế nào để thiết kế RESTful API hiệu quả? Cập nhật những thông tin mới nhất về REST API nhé!

Mình sẽ thiết kế CRUD (Create-Read-Update-Delete) API như vầy:

  1. POST /v1/items tạo mới TODO Item với dữ liệu chỉ cần có title là đủ. Thuộc tính status nên để mặc định là "Doing". API này sẽ trả về ID của TODO Item sau khi tạo thành công. Ràng buộc đơn giản là "title không rỗng hoặc chỉ chứa toàn khoảng trắng" là ok.
  2. GET /v1/items lấy danh sách các TODO Items. Nếu có phân trang thì có thể dùng thêm query string ?page=1&limit=10. Một trang sẽ hiển thị tối đa 10 items. Mặc định page1limit10.
  3. PUT /v1/items/:id update tiêu đề hoặc trạng thái của một Item thông qua ID của nó. Vì API này chúng ta có thể truyền lên cả 2 thông tin hoặc chỉ một trong 2 nên các bạn có thể dùng method PATCH sẽ chuẩn chỉ hơn. Vì PUT thông dụng hơn cho các API update nên mình chọn trong ví dụ này.
  4. DELETE /v1/items/:id xoá một TODO Item thông qua ID của nó. Trong ví dụ này mình sẽ xoá luôn trong table. Trong thực tế, hầu hết tất cả trường hợp là không nên xoá mà chỉ chuyển đổi trạng thái deleted thôi.
  5. GET /v1/items/:id lấy toàn bộ thông tin chi tiết của một TODO Item thông qua ID của nó. Theo giao diện demo thì chúng ta không cần API này, tuy nhiên 200Lab để vào cho đủ bộ CRUD nha.

4. Xây dựng REST API với Typescript Express

Mình đã hoàn tất phần chuẩn bị ở các mục phía trên, tiến hành code thôi nào.

Bash
npm init -y
npm install express typescript ts-node mysql2 @types/node @types/express --save-dev
npx tsc --init

Đây là tsconfig.json

JSON
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "CommonJS",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "outDir": "./dist"
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

Nếu bạn chưa có MySQL thì bạn có thể dùng Docker để chạy container MySQL với câu lệnh sau:

Bash
docker run -d --name demo-mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=my-root-pass -e MYSQL_DATABASE=todo_db mysql:8.3.1

Để dễ dàng hơn mình sẽ để code trong cùng một file để các bạn dễ theo dõi
src/index.ts

Typescript
import express, { Request, Response } from 'express';
import mysql, { RowDataPacket, ResultSetHeader } from 'mysql2';

interface TodoItem extends RowDataPacket {
  id: number;
  title: string;
  status: 'Doing' | 'Finished';
  created_at: string;
  updated_at: string;
}

interface CreateTodoItem {
  title: string;
}

interface UpdateTodoItem {
  title?: string;
  status?: 'Doing' | 'Finished';
}

const db = mysql.createConnection({
  host: 'localhost',
  user: process.env.USER,
  password: process.env.PASS,
  database: process.env.DATABASE,
});

db.connect((err) => {
  if (err) {
    console.error('Error connect:', err.message);
  } else {
    console.log('Connected to MySQL');
  }
});

const app = express();
app.use(express.json());

app.post('/v1/items', (req: Request<{}, {}, CreateTodoItem>, res: Response) => {
  const { title } = req.body;

  if (!title || !title.trim()) {
    return res.status(400).json({ message: 'Title cannot be empty' });
  }

  const sql = 'INSERT INTO todo_items (title) VALUES (?)';
  db.query<ResultSetHeader>(sql, [title], (err, result) => {
    if (err) {
      return res.status(500).json({ message: 'Database error', error: err });
    }
    return res.status(201).json({ id: result.insertId });
  });
});

app.get('/v1/items', (req: Request, res: Response) => {
  const page = parseInt(req.query.page as string) || 1;
  const limit = parseInt(req.query.limit as string) || 10;
  const offset = (page - 1) * limit;

  const sql = 'SELECT * FROM todo_items LIMIT ? OFFSET ?';
  db.query<TodoItem[]>(sql, [limit, offset], (err, results) => {
    if (err) {
      return res.status(500).json({ message: 'Database error', error: err });
    }
    return res.status(200).json(results);
  });
});

app.put(
  '/v1/items/:id',
  (req: Request<{ id: string }, {}, UpdateTodoItem>, res: Response) => {
    const { id } = req.params;
    const { title, status } = req.body;

    if (!title && !status) {
      return res.status(400).json({ message: 'Nothing to update' });
    }

    const updates: any[] = [];
    let sql = 'UPDATE todo_items SET ';

    if (title) {
      sql += 'title = ? ';
      updates.push(title);
    }

    if (status) {
      if (updates.length > 0) {
        sql += ', ';
      }
      sql += 'status = ? ';
      updates.push(status);
    }

    sql += 'WHERE id = ?';
    updates.push(id);

    db.query<ResultSetHeader>(sql, updates, (err, result) => {
      if (err) {
        return res.status(500).json({ message: 'Database error', error: err });
      }

      if (result.affectedRows === 0) {
        return res.status(404).json({ message: 'Item not found' });
      }

      return res.status(200).json({ message: 'Item updated successfully' });
    });
  }
);

app.delete('/v1/items/:id', (req: Request<{ id: string }>, res: Response) => {
  const { id } = req.params;

  const sql = 'DELETE FROM todo_items WHERE id = ?';
  db.query<ResultSetHeader>(sql, [id], (err, result) => {
    if (err) {
      return res.status(500).json({ message: 'Database error', error: err });
    }

    if (result.affectedRows === 0) {
      // Không tìm thấy item với ID đã cung cấp
      return res.status(404).json({ message: 'Item not found' });
    }

    return res.status(200).json({ message: 'Item deleted successfully' });
  });
});

app.get('/v1/items/:id', (req: Request<{ id: string }>, res: Response) => {
  const { id } = req.params;

  const sql = 'SELECT * FROM todo_items WHERE id = ?';
  db.query<TodoItem[]>(sql, [id], (err, result) => {
    if (err) {
      return res.status(500).json({ message: 'Database error', error: err });
    }
    if (result.length === 0) {
      return res.status(404).json({ message: 'Item not found' });
    }
    return res.status(200).json(result[0]);
  });
});

const PORT = 8080;
app.listen(PORT, () => {
  console.log(`Server is running on http://localhost:${PORT}`);
});

Cuối cùng, mình sẽ dùng POSTMAN để test API nhé!

  • POST /v1/items
Tạo mới todo item
  • GET /v1/items
Get todo items list
  • PUT /v1/items/:id
update todo item
  • DELETE /v1/items/:id
Delete todo item
  • GET /v1/items/:id
Get todo items

5. Kết luận

Qua bài viết này, bạn sẽ hiểu rõ và có thể tự mình hoàn tất được một REST API TODO List đơn giản với Typescript, vì đây là một ví dụ nên việc để code chỉ trong file index.ts không phải là best practice trong thực tế.

Nếu các bạn chưa tự tin, cảm thấy khó khăn khi học các kiến thức nâng cao với Typescript thì có thể tham khảo khóa học Typescript tại 200Lab nhé.

Khoá Học Lập Trình Microservices Với Typescript Và Express
Khoá học lập trình microservices với typescript và express giúp bạn có một nền tảng vững chắc + có project để build portfolio trở thành dev backend.

Một số bài có thể bạn sẽ quan tâm:

Bài viết liên quan

Lập trình backend expressjs

xây dựng hệ thống microservices
  • Kiến trúc Hexagonal và ứng dụngal font-
  • TypeScript: OOP và nguyên lý SOLIDal font-
  • Event-Driven Architecture, Queue & PubSubal font-
  • Basic scalable System Designal font-

Đăng ký nhận thông báo

Đừng bỏ lỡ những bài viết thú vị từ 200Lab