Chuyển đến nội dung chính

Xây dựng Trình quản lý Công việc Đơn giản theo Danh mục bằng Node.js và MongoDB

Tìm hiểu cách tổ chức công việc theo danh mục bằng Node.js và MongoDB. Xây dựng REST API với các endpoint tạo, liệt kê, lấy, cập nhật, xóa và bộ lọc theo danh mục.
Đã cập nhật 10 thg 9, 2026  · 10 phút đọc

Khám phá với AI

ChatGPTClaudePerplexity

Một danh sách việc cần làm đơn giản hoạt động rất ổn lúc ban đầu: bạn thêm vài nhiệm vụ và đánh dấu hoàn thành khi xong. Nhưng khi số lượng nhiệm vụ tăng lên, việc sử dụng trở nên bất tiện. Một cách khắc phục hiệu quả là thêm phân loại, cho phép mỗi nhiệm vụ thuộc các danh mục như Work, Study, Personal, v.v., và bạn có thể lọc danh sách theo danh mục phù hợp.

Trong hướng dẫn này, bạn sẽ học cách xây dựng một REST API cho phép tổ chức nhiệm vụ theo danh mục bằng Node.js và MongoDB. Kết thúc bài, bạn sẽ có một máy chủ Node.js hoạt động với năm endpoint: tạo, liệt kê, lấy, cập nhật và xóa, kèm theo bộ lọc danh mục. Bạn cũng sẽ thấy cách áp đặt kiểm tra hợp lệ cho danh mục bằng trình điều khiển gốc của MongoDB, mà không cần phụ thuộc vào một ODM như Mongoose.

Bạn sẽ học được gì

  • Cách xây dựng REST API với Node.js, Express và MongoDB, bao gồm đầy đủ các endpoint CRUD
  • Cách kiểm tra đầu vào, áp đặt danh mục và xử lý an toàn các giá trị ObjectId một cách hiệu quả
  • Cách cấu trúc và kiểm thử API, sau đó kết nối nó với frontend

Bạn có thể tìm thấy toàn bộ mã của hướng dẫn này trên GitHub nếu muốn clone về và đọc theo khi thực hành.

Yêu cầu tiên quyết

Trước khi bắt đầu, bạn cần:

  • Cài đặt Node.js 18+ (npm được bao gồm mặc định)
  • MongoDB chạy cục bộ, hoặc một cụm MongoDB Atlas miễn phí
  • Postman hoặc bất kỳ HTTP client nào
  • Kiến thức cơ bản về JavaScript và các khái niệm REST

Bước 1: Thiết lập dự án

Hãy bắt đầu xây dựng trình quản lý công việc đơn giản của chúng ta. Việc đầu tiên là tạo một thư mục mới và khởi tạo dự án Node bằng cách chạy các lệnh dưới đây trong terminal:

mkdir task-manager-categories
cd task-manager-categories
npm init -y

Tiếp theo, chúng ta sẽ cài bốn phụ thuộc runtime mà ứng dụng cần để chạy: express để định tuyến, trình điều khiển MongoDB chính thức, dotenv để nạp biến môi trường, và cuối cùng là helmet cho các header bảo mật.

npm install express mongodb dotenv helmet

Trong quá trình phát triển, cài đặt nodemon như một dev dependency bằng lệnh dưới đây để máy chủ tự động tải lại khi tệp thay đổi:

npm install --save-dev nodemon

Tiếp theo, mở thư mục dự án trong IDE và cập nhật phần scripts trong package.json để bao gồm nội dung sau. Điều này cho phép bạn khởi động máy chủ bằng npm start hoặc chạy ở chế độ phát triển với npm run dev:

"scripts": {
  "start": "node server.js",
  "dev": "nodemon server.js"
}

Đây là cấu trúc chúng ta sẽ xây dựng dần. Mỗi thư mục có vai trò rõ ràng: db/ xử lý kết nối cơ sở dữ liệu, lib/ chứa các hàm trợ giúp, middleware/ dành cho middleware của Express, và routes/ định nghĩa các trình xử lý yêu cầu. Thư mục public/ sẽ chứa một frontend nhỏ chúng ta sẽ thêm sau. Ngay cả với dự án nhỏ, cấu trúc này giúp dễ theo dõi và rõ ràng nơi cần thêm mã mới. Bạn có thể tạo sẵn bố cục thư mục và điền dần khi làm, hoặc bỏ qua và thêm từng tệp khi đi qua các bước:

task-manager-categories/
├── db/
│ └── connect.js
├── lib/
│ └── taskDocument.js
├── middleware/
│ └── parseObjectId.js
├── routes/
│ └── tasks.js
├── public/
│ ├── index.html
│ ├── styles.css
│ └── app.js
├── .env
├── .env.example
├── .gitignore
├── package.json
└── server.js

Bước 2: Kết nối tới MongoDB

Để lưu trữ và truy xuất nhiệm vụ, trước hết chúng ta cần kết nối ứng dụng với MongoDB. Việc này thực hiện bằng một chuỗi kết nối, cho biết cho trình điều khiển MongoDB cách kết nối tới cơ sở dữ liệu của bạn. Thay vì viết cứng trong mã, tốt hơn là đặt nó trong tệp môi trường. Cách này giữ giá trị nhạy cảm ngoài codebase và giúp dễ chuyển đổi giữa các môi trường.

Tạo tệp .env.example để ghi chú các biến cần thiết, và một tệp .env cho giá trị thực tế của bạn, như sau:

# Copy this file to `.env` and fill in real values.

# --- Local MongoDB ---
# Use this if you're running MongoDB locally
MONGO_URI=mongodb://127.0.0.1:27017

# --- MongoDB Atlas ---
# Replace <username>, <password>, and <cluster-url> with your actual values
# Example: mongodb+srv://user:pass@cluster0.abcde.mongodb.net/
# MONGO_URI=mongodb+srv://<username>:<password>@<cluster-url>/?retryWrites=true&w=majority

DB_NAME=taskmanager
PORT=3000

Giờ chúng ta đã có chuỗi kết nối, hãy dùng nó để thiết lập kết nối MongoDB. Tạo tệp db/connect.js. Đây là nơi khởi tạo MongoDB client và cung cấp nó cho phần còn lại của ứng dụng:

const { MongoClient } = require("mongodb");

let client;
let db;

async function connectDB() {
  if (!process.env.MONGO_URI) {
    throw new Error("MONGO_URI is not set. Check your .env file.");
  }
  client = new MongoClient(process.env.MONGO_URI, {
    appName: "devrel-tutorial-javascript-crud-geeksforgeeks",
  });
  await client.connect();
  db = client.db(process.env.DB_NAME || "taskmanager");
  await db.collection("tasks").createIndex({ category: 1 });

  console.log(`MongoDB connected (db: ${db.databaseName})`);
}

function getTasksCollection() {
  if (!db) {
    throw new Error("Database not initialized. Call connectDB() first.");
  }
  return db.collection("tasks");
}

async function closeDB() {
  if (client) await client.close();
}

module.exports = { connectDB, getTasksCollection, closeDB };

Tệp này thiết lập một MongoDB client dùng chung cho toàn ứng dụng. Bạn chỉ nên tạo một lần, vì trình điều khiển đã xử lý pooling kết nối ở tầng dưới. Tạo client mới cho mỗi yêu cầu nghe có vẻ ổn ban đầu, nhưng sẽ gây vấn đề hiệu năng rất nhanh.

Bước 3: Định nghĩa cấu trúc và kiểm tra hợp lệ cho Task

Tại thời điểm này, ứng dụng có thể kết nối tới MongoDB. Giờ chúng ta cần định nghĩa một task thực sự trông như thế nào trước khi bắt đầu lưu dữ liệu.

Đây là lúc dùng trình điều khiển gốc của MongoDB sẽ có chút khác biệt. Không có tệp schema như với Mongoose. Thay vào đó, “schema” đơn giản là hình dạng đối tượng bạn chèn vào cơ sở dữ liệu. Ban đầu có thể thấy hơi lỏng lẻo, nhưng thực ra lại hữu ích vì bạn bám sát cách MongoDB thực sự hoạt động, không có lớp trừu tượng che giấu.

Dù vậy, chúng ta vẫn cần kiểm tra hợp lệ. Thay vì rải logic này khắp các route, chúng ta sẽ gom vào một nơi để mọi thứ tuân theo cùng một quy tắc. Hãy tạo tệp có tên lib/taskDocument.js và thêm đoạn mã dưới đây:

class ValidationError extends Error {
  constructor(message) {
    super(message);
    this.name = 'ValidationError';
  }
}

const ALLOWED_CATEGORIES = ['Work', 'Personal', 'Study', 'Other'];

function assertValidCategory(category) {
  if (!ALLOWED_CATEGORIES.includes(category)) {
    throw new ValidationError(
      `category must be one of: ${ALLOWED_CATEGORIES.join(', ')}`
    );
  }
}

function buildTaskDocument(body = {}) {
  if (!body.title || typeof body.title !== 'string' || !body.title.trim()) {
    throw new ValidationError('title is required and must be a non-empty string');
  }
  if (body.category != null) {
    assertValidCategory(body.category);
  }
  const now = new Date();
  return {
    title: body.title.trim(),
    description: typeof body.description === 'string' ? body.description.trim() : '',
    category: body.category != null ? body.category : 'Other',
    completed: Boolean(body.completed),
    createdAt: now,
    updatedAt: now
  };
}

function buildTaskUpdate(body = {}) {
  const updates = {};
  if (typeof body.title === 'string' && body.title.trim()) {
    updates.title = body.title.trim();
  }
  if (typeof body.description === 'string') {
    updates.description = body.description.trim();
  }
  if (body.category != null) {
    assertValidCategory(body.category);
    updates.category = body.category;
  }
  if (body.completed != null) {
    updates.completed = Boolean(body.completed);
  }
  if (Object.keys(updates).length === 0) {
    throw new ValidationError('no valid fields provided for update');
  }
  updates.updatedAt = new Date();
  return updates;
}

module.exports = {
  ALLOWED_CATEGORIES,
  buildTaskDocument,
  buildTaskUpdate,
  ValidationError
};

Tệp này đóng vai trò “gác cổng” cho mọi thứ đi vào cơ sở dữ liệu. Mọi yêu cầu tạo hoặc cập nhật đều đi qua đây, nên có một nơi duy nhất áp đặt quy tắc.

Nó đảm bảo nhiệm vụ luôn có hình dạng đúng, giữ danh mục nhất quán và tránh bất ngờ khi bạn bắt đầu truy vấn dữ liệu. ValidationError tùy chỉnh cũng cho bạn cách gọn gàng để phân biệt đầu vào không hợp lệ với lỗi máy chủ thực sự, giúp API phản hồi phù hợp. Khi có phần này, phần còn lại của ứng dụng có thể đơn giản. Mỗi route chỉ cần tập trung vào công việc của nó, vì dữ liệu nhận được đã hợp lệ. Tiếp theo, chúng ta sẽ nối các route và bắt đầu lưu nhiệm vụ vào MongoDB.

Bước 4: Xử lý ObjectId và kiểm tra hợp lệ yêu cầu

Giờ chúng ta biết một task trông như thế nào, hãy xử lý cách tham chiếu nó. Bất cứ khi nào một route bao gồm tham số :id, nó đến dưới dạng chuỗi thuần. Nhưng MongoDB mong đợi một ObjectId. Nếu chuỗi đó sai định dạng, trình điều khiển sẽ ném ra lỗi khá khó hiểu. Thay vì xử lý ở mọi route, chúng ta sẽ tập trung hóa bằng middleware để mọi endpoint có hành vi đồng nhất.

Để làm điều này, hãy tạo tệp tên là middleware/parseObjectId.js và thêm nội dung sau:

const { ObjectId } = require('mongodb');

function parseObjectId(req, res, next) {
  const { id } = req.params;
  if (!ObjectId.isValid(id)) {
    return res.status(400).json({ error: 'invalid task id' });
  }
  req.taskId = new ObjectId(id);
  next();
}

module.exports = parseObjectId;

Middleware này chạy trước trình xử lý route. Nó kiểm tra id có hợp lệ không, chuyển nó thành ObjectId, và gắn vào req.taskId. Nhờ đó, khi logic route chạy, bạn luôn làm việc với một ObjectId đúng, và đầu vào xấu bị từ chối sớm với phản hồi 400 rõ ràng. Tiếp theo, chúng ta sẽ cắm phần này vào các route và kết nối mọi thứ lại với nhau.

Bước 5: Xây dựng các route cho Task (CRUD + Lọc)

Đến đây, phần việc nặng đã hoàn thành phần lớn. Chúng ta đã định nghĩa một task hợp lệ trông như thế nào, và đã xử lý cách phân tích và kiểm tra hợp lệ ID. Điều đó có nghĩa là các trình xử lý route có thể tập trung vào một việc: giao tiếp với cơ sở dữ liệu.

Hãy xây dựng các route API thực tế bằng cách tạo tệp routes/tasks.js và thêm nội dung sau:

const express = require('express');
const { getTasksCollection } = require('../db/connect');
const {
  ALLOWED_CATEGORIES,
  buildTaskDocument,
  buildTaskUpdate,
  ValidationError
} = require('../lib/taskDocument');
const parseObjectId = require('../middleware/parseObjectId');

const router = express.Router();

// POST /tasks
router.post('/', async (req, res, next) => {
  try {
    const doc = buildTaskDocument(req.body);
    const result = await getTasksCollection().insertOne(doc);
    res.status(201).json({ _id: result.insertedId, ...doc });
  } catch (err) {
    if (err instanceof ValidationError) {
      return res.status(400).json({ error: err.message });
    }
    next(err);
  }
});

// GET /tasks (optionally ?category=Work)
router.get('/', async (req, res, next) => {
  try {
    const { category } = req.query;
    if (category && !ALLOWED_CATEGORIES.includes(category)) {
      return res.status(400).json({
        error: `category must be one of: ${ALLOWED_CATEGORIES.join(', ')}`
      });
    }
    const filter = category ? { category } : {};
    const tasks = await getTasksCollection()
      .find(filter)
      .sort({ createdAt: -1 })
      .toArray();
    res.json(tasks);
  } catch (err) { next(err); }
});

// GET /tasks/:id
router.get('/:id', parseObjectId, async (req, res, next) => {
  try {
    const task = await getTasksCollection().findOne({ _id: req.taskId });
    if (!task) return res.status(404).json({ error: 'task not found' });
    res.json(task);
  } catch (err) { next(err); }
});

// PUT /tasks/:id
router.put('/:id', parseObjectId, async (req, res, next) => {
  try {
    const updates = buildTaskUpdate(req.body);
    const result = await getTasksCollection().findOneAndUpdate(
      { _id: req.taskId },
      { $set: updates },
      { returnDocument: 'after' }
    );
    if (!result) return res.status(404).json({ error: 'task not found' });
    res.json(result);
  } catch (err) {
    if (err instanceof ValidationError) {
      return res.status(400).json({ error: err.message });
    }
    next(err);
  }
});

// DELETE /tasks/:id
router.delete('/:id', parseObjectId, async (req, res, next) => {
  try {
    const result = await getTasksCollection().deleteOne({ _id: req.taskId });
    if (result.deletedCount === 0) {
      return res.status(404).json({ error: 'task not found' });
    }
    res.status(204).end();
  } catch (err) { next(err); }
});

module.exports = router;

Mỗi route trong đoạn mã trên đều theo cùng một mẫu: nhận đầu vào từ yêu cầu, truyền qua các hàm trợ giúp đã xây dựng, gọi MongoDB, rồi trả về phản hồi. Với việc kiểm tra hợp lệ và phân tích ID đã được xử lý, mã ở đây ngắn gọn và dễ dự đoán. Trên thực tế, nó diễn ra như sau:

  • Route POST xây dựng một nhiệm vụ mới bằng buildTaskDocument trước khi chèn
  • Các route GET có thể lọc theo danh mục và trả kết quả sắp xếp mới nhất trước
  • Route PUT dùng buildTaskUpdate, nên cập nhật một phần an toàn và nhất quán
  • Route DELETE xóa nhiệm vụ theo ObjectId đã được phân tích sẵn

Cũng có một số chi tiết khác trong mã này đáng lưu ý:

  • find() trả về một con trỏ, không phải mảng, nên chúng ta nối thêm .toArray() sau khi sắp xếp để lấy kết quả thực sự
  • findOneAndUpdate với returnDocument: 'after' cho bạn tài liệu đã cập nhật ngay lập tức
  • Chúng ta trả về mã trạng thái HTTP phù hợp: 201 khi tạo, 204 khi xóa, và 400 hoặc 404 khi phù hợp
  • Mọi lỗi không mong đợi đều được chuyển tới next(err), để chúng có thể được xử lý tập trung thay vì trong từng route

Bước 6: Kết nối mọi thứ trong máy chủ

Đến đây, mọi mảnh ghép đã sẵn sàng. Chúng ta có kiểm tra hợp lệ, các route gọn gàng và kết nối cơ sở dữ liệu hoạt động. Giờ chỉ cần nối mọi thứ lại và thực sự khởi động máy chủ. Tạo tệp server.js. Đây là điểm vào của ứng dụng, nơi mọi thứ hợp nhất:

require('dotenv').config();

const path = require('path');
const express = require('express');
const helmet = require('helmet');
const { connectDB, closeDB } = require('./db/connect');
const tasksRouter = require('./routes/tasks');

if (!process.env.MONGO_URI) {
  console.error('Fatal: MONGO_URI is not set. Copy .env.example to .env and fill it in.');
  process.exit(1);
}

const PORT = Number(process.env.PORT) || 3000;

const app = express();

app.use(helmet());
app.use(express.json({ limit: '100kb' }));
app.use(express.static(path.join(__dirname, 'public')));

app.get('/health', (req, res) => {
  res.json({ status: 'ok', service: 'task-manager-with-categories' });
});
app.use('/tasks', tasksRouter);

app.use((req, res) => {
  res.status(404).json({ error: 'not found' });
});

app.use((err, req, res, next) => {
  if (err.type === 'entity.too.large') {
    return res.status(413).json({ error: 'payload too large' });
  }
  if (err.type === 'entity.parse.failed') {
    return res.status(400).json({ error: 'invalid JSON body' });
  }
  console.error(err);
  res.status(500).json({ error: 'internal server error' });
});

async function start() {
  try {
    await connectDB();
    const server = app.listen(PORT, () => {
      console.log(`Server running on http://localhost:${PORT}`);
    });

    const shutdown = () => {
      console.log('\nShutting down gracefully...');
      server.close(async () => {
        await closeDB();
        process.exit(0);
      });
      setTimeout(() => process.exit(1), 10_000).unref();
    };
    process.on('SIGINT', shutdown);
    process.on('SIGTERM', shutdown);
  } catch (err) {
    console.error('Failed to start server:', err);
    process.exit(1);
  }
}

if (require.main === module) {
  start();
}

module.exports = { app, start };

Tệp này kết nối mọi thứ lại một cách gọn gàng. Nó kiểm tra biến môi trường được thiết lập trước khi chạy bất cứ thứ gì, áp dụng bảo mật cơ bản với helmet, và giới hạn JSON đầu vào để ứng dụng không vô tình chấp nhận payload quá lớn. Nó cũng phục vụ thư mục public/, để frontend của bạn có thể chạy trong cùng ứng dụng mà không cần máy chủ riêng.

Tất cả route của bạn được gắn dưới /tasks, và bất cứ thứ gì không khớp sẽ trả về 404 gọn gàng. Lỗi được xử lý tập trung thay vì lặp lại trong từng route, giúp nhất quán và dễ bảo trì hơn. Kết nối cơ sở dữ liệu được thiết lập trước khi máy chủ bắt đầu lắng nghe, nên bạn không nhận yêu cầu khi chưa sẵn sàng, và việc tắt được xử lý trơn tru để đóng kết nối đúng cách.

Giờ hãy khởi động và đảm bảo mọi thứ hoạt động bằng cách chạy npm run dev. Nếu mọi thứ được nối đúng, bạn sẽ thấy như sau:

MongoDB connected (db: taskmanager)
Server running on http://localhost:3000

Tại thời điểm này, mọi thứ đã được nối và chạy! Chúng ta có một API hoạt động hoàn chỉnh, nên hãy kiểm thử nó.

Bước 7: Kiểm thử các endpoint của API

Trước khi xây dựng UI, đáng để xác nhận rằng mỗi endpoint hoạt động độc lập. Điều này giúp gỡ lỗi dễ hơn, vì bạn có thể nhanh chóng xác định vấn đề đến từ API hay frontend. Chúng ta sẽ dùng Postman (hoặc bất kỳ HTTP client nào) và chạy một vài yêu cầu theo thứ tự. Mỗi yêu cầu dựa trên dữ liệu tạo ở bước trước.

Tạo một nhiệm vụ: Gửi một yêu cầu POST với tiêu đề, mô tả và danh mục. Nếu mọi thứ hoạt động, API trả về 201 Created cùng nhiệm vụ mới và _id được tạo.

POST http://localhost:3000/tasks
Content-Type: application/json

{
  "title": "Write GeeksForGeeks article",
  "description": "First draft by Friday",
  "category": "Work"
}

Tạo thêm vài cái nữa: Chạy cùng yêu cầu POST với nội dung khác nhau để có đủ dữ liệu kiểm thử liệt kê và lọc. Sau đó, bạn sẽ có một tập dữ liệu nhỏ để truy vấn.

{ "title": "Go for a run", "category": "Personal" }
{ "title": "Read MongoDB docs", "category": "Study" }
{ "title": "Buy groceries" } // category defaults to "Other"

Liệt kê tất cả nhiệm vụ: Trả về tất cả nhiệm vụ, sắp xếp mới nhất trước.

GET http://localhost:3000/tasks

Lọc theo danh mục: Trả về chỉ các nhiệm vụ Work. Chỉ mục bạn đã thêm trước đó giúp truy vấn này vẫn nhanh khi dữ liệu tăng.

GET http://localhost:3000/tasks?category=Work

Cập nhật một nhiệm vụ: Dùng _id của một nhiệm vụ bạn vừa tạo (bạn có thể sao chép từ phản hồi POST hoặc endpoint danh sách). Phản hồi trả về nhiệm vụ đã cập nhật. Bạn chỉ cần gửi các trường muốn thay đổi, nên cập nhật một phần rất đơn giản.

PUT http://localhost:3000/tasks/<paste-task-id-here>
Content-Type: application/json

{ "completed": true }

Xóa một nhiệm vụ: Dùng _id của nhiệm vụ bạn muốn xóa. Phản hồi trả về 204 No Content. Nếu bạn thử lấy lại nhiệm vụ đó, sẽ nhận 404 với "task not found".

DELETE http://localhost:3000/tasks/<paste-task-id-here>

Tới đây, bạn đã xác nhận rằng tất cả route hoạt động như mong đợi. API đã hoàn thành nhiệm vụ của nó, giờ chúng ta có thể chuyển sang xây dựng UI phía trên.

Bước 8: Thêm frontend để tương tác với API

Postman rất hữu ích để xác minh API, nhưng sẽ tuyệt hơn nếu xem nó hoạt động trên một trang web thực tế, nên hãy thêm một chút mã frontend vào ứng dụng.

Thay vì dán các tệp HTML và CSS lớn ở đây, tôi đã thêm mã vào repo GitHub này. Hãy tới đó và sao chép nội dung của index.html, styles.css, và favicon.svg vào thư mục public/ ở thư mục gốc dự án của bạn. Lưu lại và truy cập localhost:3000. Sau đó dán mã dưới đây vào tệp app.js:

const form = document.getElementById('task-form');
const titleEl = document.getElementById('title');
const descEl = document.getElementById('description');
const catEl = document.getElementById('category');
const errEl = document.getElementById('form-error');
const listEl = document.getElementById('task-list');
const emptyEl = document.getElementById('empty');
const filterEl = document.getElementById('filter');
const countEl = document.getElementById('task-count');

async function api(method, path, body) {
  const res = await fetch(path, {
    method,
    headers: body ? { 'Content-Type': 'application/json' } : {},
    body: body ? JSON.stringify(body) : undefined
  });

  if (res.status === 204) return null;

  const data = await res.json();
  if (!res.ok) throw new Error(data.error || `Request failed (${res.status})`);
  return data;
}

const fetchTasks = (category) =>
  api('GET', '/tasks' + (category ? `?category=${encodeURIComponent(category)}` : ''));

const createTask = (body) => api('POST', '/tasks', body);
const updateTask = (id, body) => api('PUT', `/tasks/${id}`, body);
const deleteTask = (id) => api('DELETE', `/tasks/${id}`);

function updateCount(tasks) {
  const active = tasks.filter((t) => !t.completed).length;
  const total = tasks.length;

  if (total === 0) {
    countEl.textContent = '';
    return;
  }

  const scope = filterEl.value ? ` in ${filterEl.value}` : '';
  countEl.textContent = `${total} tasks${scope} · ${active} active`;
}

function renderTask(task) {
  const li = document.createElement('li');
  li.className = 'task' + (task.completed ? ' completed' : '');

  const checkbox = document.createElement('input');
  checkbox.type = 'checkbox';
  checkbox.checked = task.completed;
  checkbox.addEventListener('change', () =>
    updateTask(task._id, { completed: checkbox.checked }).then(refresh)
  );

  const title = document.createElement('div');
  title.textContent = task.title;

  const badge = document.createElement('span');
  badge.textContent = task.category;

  const del = document.createElement('button');
  del.textContent = 'Delete';
  del.onclick = () => deleteTask(task._id).then(refresh);

  li.append(checkbox, title, badge, del);
  return li;
}

function render(tasks) {
  listEl.innerHTML = '';
  emptyEl.classList.toggle('hidden', tasks.length > 0);

  for (const task of tasks) {
    listEl.appendChild(renderTask(task));
  }

  updateCount(tasks);
}

async function refresh() {
  try {
    const tasks = await fetchTasks(filterEl.value);
    render(tasks);
  } catch (e) {
    errEl.textContent = e.message;
  }
}

form.addEventListener('submit', async (e) => {
  e.preventDefault();
  errEl.textContent = '';

  const title = titleEl.value.trim();
  if (!title) {
    errEl.textContent = 'Title is required.';
    return;
  }

  try {
    await createTask({
      title,
      description: descEl.value.trim(),
      category: catEl.value
    });

    form.reset();
    await refresh();
  } catch (e) {
    errEl.textContent = e.message;
  }
});

filterEl.addEventListener('change', refresh);

refresh();

Mã JavaScript là nơi mọi thứ kết nối lại với API bạn đã xây dựng. Mỗi hàm ánh xạ tới một route, giúp frontend dễ theo dõi. Tất cả yêu cầu cũng đi qua một hàm trợ giúp api() duy nhất, nên bạn chỉ xử lý lỗi ở một nơi thay vì lặp lại logic ở khắp nơi.

Bộ lọc hoạt động bằng cách truyền query string như ?category=Work, gắn liền với logic backend bạn đã thêm. Sau khi tạo, cập nhật hoặc xóa một nhiệm vụ, ứng dụng sẽ lấy lại danh sách mới nhất để UI luôn đồng bộ. Bộ đếm nhiệm vụ là một chi tiết nhỏ, nhưng giúp ứng dụng sinh động hơn khi bạn sử dụng.

Giờ hãy khởi động lại máy chủ với npm run dev, rồi mở http://localhost:3000 để thử Task Manager. Thêm vài nhiệm vụ, cập nhật chúng, chuyển giữa các danh mục và xóa một số. Lúc này, frontend và backend đã kết nối đầy đủ và hoạt động xuyên suốt end-to-end.

Tổng kết

Chúc mừng! Bạn đã xây dựng thành công một API quản lý nhiệm vụ hoàn chỉnh từ đầu bằng Node.js, Express và MongoDB. Bạn đã xử lý kiểm tra hợp lệ, giữ cho các route gọn nhẹ và kết nối mọi thứ thành một hệ thống hoạt động. Quan trọng hơn, bạn đã bám sát cách MongoDB thực sự vận hành thay vì ẩn sau các lớp trừu tượng. Từ đây, bạn có thể tiếp tục phát triển ứng dụng để đưa vào sản xuất.

Điểm chính rút ra

  • Sử dụng middleware và tập trung hóa kiểm tra hợp lệ giúp mã sạch và dễ dự đoán.
  • Trình điều khiển gốc của MongoDB cho bạn quyền kiểm soát mà không cần trừu tượng không cần thiết.
  • Một API có cấu trúc tốt rất dễ mở rộng thành ứng dụng sẵn sàng cho sản xuất.

FAQs

Tôi có cần Mongoose để xây dựng một API MongoDB không?

Không. Hướng dẫn này sử dụng trình điều khiển gốc của MongoDB, mang lại nhiều kiểm soát hơn và giữ mọi thứ nhẹ nhàng. Bạn có thể thêm Mongoose sau nếu muốn có lớp trừu tượng schema.

Làm thế nào để kiểm tra dữ liệu mà không cần schema?

Bạn có thể tập trung hóa kiểm tra hợp lệ trong các hàm trợ giúp (chẳng hạn như buildTaskDocument), đảm bảo mọi route đều áp dụng cùng một quy tắc trước khi ghi vào cơ sở dữ liệu.

Điều gì xảy ra nếu tôi truyền một ObjectId không hợp lệ?

Middleware sẽ từ chối sớm với phản hồi 400, ngăn MongoDB ném ra các lỗi gây bối rối.

Chủ đề
MongoDB

Các khóa học DataCamp hàng đầu

Courses

Nhập môn MongoDB với Python

3 giờ
24.2K
Xem chi tiếtRight Arrow
Bắt Đầu Khóa Học
Xem thêmRight Arrow
Có liên quan

blogs

Claude Opus 4.6: Tính năng, Điểm chuẩn, Bài kiểm tra thực hành và hơn thế nữa

Mô hình mới nhất của Anthropic dẫn đầu ở mã hóa tác tử và lập luận phức tạp. Thêm vào đó, nó có cửa sổ ngữ cảnh 1M.
Matt Crabtree's photo

Matt Crabtree

10 phút

Xem ThêmXem Thêm