ข้ามไปยังเนื้อหาหลัก

สร้างตัวจัดการงานอย่างง่ายพร้อมหมวดหมู่ด้วย Node.js และ MongoDB

เรียนรู้วิธีจัดระเบียบงานตามหมวดหมู่ด้วย Node.js และ MongoDB สร้าง REST API พร้อมเอ็นด์พอยต์ create, list, get, update, delete และตัวกรองตามหมวดหมู่
อัปเดตแล้ว 10 ก.ย. 2569  · 10 นาที อ่าน

สำรวจด้วย AI

ChatGPTClaudePerplexity

ลิสต์งานแบบพื้นฐานใช้งานได้ดีในช่วงแรก: เพิ่มงานไม่กี่งาน แล้วปิดงานเมื่อทำเสร็จ แต่พองานเพิ่มขึ้นเรื่อย ๆ การใช้งานก็เริ่มน่าอึดอัด วิธีหนึ่งที่ช่วยได้คือเพิ่มการจัดหมวดหมู่ เพื่อให้งานแต่ละงานอยู่ในหมวดอย่าง Work, Study, Personal เป็นต้น และสามารถกรองลิสต์ตามหมวดหมู่ที่ต้องการได้

ในบทช่วยสอนนี้ จะอธิบายวิธีสร้าง REST API สำหรับจัดงานตามหมวดหมู่ด้วย Node.js และ MongoDB เมื่อจบแล้วจะมีเซิร์ฟเวอร์ Node.js ที่ใช้งานได้จริงพร้อม 5 เอ็นด์พอยต์: create, list, get, update และ delete พร้อมตัวกรองตามหมวดหมู่ อีกทั้งยังเห็นวิธีบังคับใช้การตรวจสอบหมวดหมู่ด้วย MongoDB ไดรเวอร์แบบเนทีฟ โดยไม่ต้องพึ่ง ODM อย่าง Mongoose

สิ่งที่จะได้เรียนรู้

  • วิธีสร้าง REST API ด้วย Node.js, Express และ MongoDB ครบทั้งเอ็นด์พอยต์ CRUD
  • วิธีตรวจสอบอินพุต บังคับใช้หมวดหมู่ และจัดการค่า ObjectId อย่างปลอดภัยและมีประสิทธิภาพ
  • วิธีจัดโครงสร้างและทดสอบ API จากนั้นเชื่อมต่อไปยังฝั่งเฟรันต์เอนด์

สามารถดูโค้ดฉบับเต็มของบทช่วยสอนนี้ได้บน GitHub หากต้องการโคลนแล้วอ่านไปพร้อมกัน

ข้อกำหนดเบื้องต้น

ก่อนเริ่ม ควรมีสิ่งต่อไปนี้:

  • ติดตั้ง Node.js 18+ แล้ว (npm มากับตัว)
  • MongoDB ที่รันบนเครื่อง หรือคลัสเตอร์ MongoDB Atlas ฟรี
  • Postman หรือไคลเอนต์ HTTP ใด ๆ
  • ความรู้พื้นฐานเกี่ยวกับ JavaScript และแนวคิด REST

ขั้นตอนที่ 1: ตั้งค่าโปรเจกต์

มาเริ่มสร้างตัวจัดการงานแบบง่ายกัน สิ่งแรกคือสร้างโฟลเดอร์ใหม่และเริ่มต้นโปรเจกต์ Node โดยรันคำสั่งด้านล่างในเทอร์มินัล:

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

ถัดไป ติดตั้งไลบรารีรันไทม์สี่ตัวที่แอปต้องใช้: express สำหรับระบบเราต์ ไดรเวอร์ MongoDB อย่างเป็นทางการ dotenv สำหรับโหลดตัวแปรสภาพแวดล้อม และสุดท้าย helmet สำหรับตั้งค่าเฮดเดอร์ด้านความปลอดภัย

npm install express mongodb dotenv helmet

สำหรับการพัฒนา ให้ติดตั้ง nodemon เป็น dev dependency ด้วยคำสั่งด้านล่าง เพื่อให้เซิร์ฟเวอร์รีโหลดอัตโนมัติเมื่อไฟล์มีการเปลี่ยนแปลง:

npm install --save-dev nodemon

จากนั้น เปิดโฟลเดอร์โปรเจกต์ใน IDE แล้วอัปเดตส่วน scripts ใน package.json ให้มีดังนี้ ซึ่งจะช่วยให้เริ่มเซิร์ฟเวอร์ด้วย npm start หรือรันโหมดพัฒนาด้วย npm run dev:

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

โครงสร้างที่กำลังจะสร้างมีดังนี้ แต่ละโฟลเดอร์มีหน้าที่ชัดเจน: db/ สำหรับการเชื่อมต่อฐานข้อมูล lib/ สำหรับฟังก์ชันช่วยเหลือ middleware/ สำหรับ Express middleware และ routes/ สำหรับกำหนดตัวจัดการคำขอ โฟลเดอร์ public/ จะเก็บเฟรันต์เอนด์เล็ก ๆ ที่จะเพิ่มภายหลัง แม้โปรเจกต์เล็ก โครงสร้างนี้ช่วยให้ง่ายต่อการติดตาม และเห็นชัดว่าควรใส่โค้ดใหม่ตรงไหน จะสร้างเลย์เอาต์โฟลเดอร์ตอนนี้แล้วค่อยเติมไฟล์ภายหลังก็ได้ หรือจะข้ามไปแล้วเพิ่มไฟล์ทีละส่วนตามขั้นตอนก็ได้:

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

ขั้นตอนที่ 2: เชื่อมต่อกับ MongoDB

เพื่อบันทึกและดึงข้อมูลงาน ขั้นแรกต้องเชื่อมต่อแอปกับ MongoDB โดยใช้สตริงการเชื่อมต่อ ซึ่งบอกไดรเวอร์ MongoDB ว่าจะเชื่อมต่อฐานข้อมูลอย่างไร แทนที่จะฮาร์ดโค้ดไว้ในโค้ด ควรเก็บไว้ในไฟล์สภาพแวดล้อม เพื่อป้องกันข้อมูลอ่อนไหวอยู่ในซอร์สโค้ด และสลับสภาพแวดล้อมได้ง่าย

สร้างไฟล์ .env.example เพื่ออธิบายตัวแปรที่ต้องใช้ และไฟล์ .env สำหรับค่าจริงของคุณ ดังนี้:

# 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

เมื่อมีสตริงการเชื่อมต่อแล้ว ก็ใช้ตั้งค่าการเชื่อมต่อ MongoDB ได้ สร้างไฟล์ db/connect.js ซึ่งจะเป็นที่เริ่มต้น MongoDB client และให้ส่วนอื่นของแอปเรียกใช้ได้:

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 };

ไฟล์นี้ตั้งค่า MongoDB client ตัวเดียวให้ส่วนอื่นของแอปใช้ซ้ำ ควรสร้างเพียงครั้งเดียว เพราะไดรเวอร์จัดการ connection pooling ให้แล้ว การสร้างไคลเอนต์ใหม่ทุกคำขออาจดูไม่เป็นไรช่วงแรก แต่จะทำให้ประสิทธิภาพแย่ลงอย่างรวดเร็ว

ขั้นตอนที่ 3: กำหนดโครงสร้างและการตรวจสอบของ Task

ถึงตอนนี้ แอปเชื่อมต่อ MongoDB ได้แล้ว ต่อไปต้องกำหนดว่ารูปแบบของงาน (task) ควรเป็นอย่างไรก่อนจะเริ่มบันทึกข้อมูล

ตรงนี้การใช้ MongoDB ไดรเวอร์แบบเนทีฟจะรู้สึกต่างเล็กน้อย เพราะไม่มีไฟล์สคีมาแบบที่มีใน Mongoose แทนที่จะเป็นสคีมา ไดรเวอร์จะรับอ็อบเจ็กต์ตามรูปร่างที่เราอินเสิร์ตเข้าไปในฐานข้อมูล ซึ่งอาจดูหลวมในตอนแรก แต่จริง ๆ มีประโยชน์เพราะเราใกล้เคียงกับสิ่งที่ MongoDB ทำจริง ๆ และไม่มีอะไรซ่อนอยู่หลังเลเยอร์นามธรรม

อย่างไรก็ตาม เรายังคงต้องการการตรวจสอบความถูกต้องของข้อมูล ดังนั้นแทนที่จะกระจายตรรกะนี้ไปตามเราต์ต่าง ๆ จะรวบไว้ที่เดียวเพื่อให้ทุกอย่างยึดกฎเดียวกัน สร้างไฟล์ชื่อ lib/taskDocument.js แล้วใส่โค้ดด้านล่าง:

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
};

ไฟล์นี้ทำหน้าที่เป็นด่านก่อนเข้าฐานข้อมูล ทุกคำขอ create หรือ update จะผ่านส่วนนี้ ทำให้กำหนดกฎไว้ที่เดียว

ช่วยให้รูปแบบงานถูกต้องเสมอ รักษาค่าหมวดหมู่ให้สม่ำเสมอ และลดปัญหายามคิวรีข้อมูลในภายหลัง คลาส ValidationError แบบกำหนดเองยังช่วยแยกแยะอินพุตที่ไม่ดีออกจากปัญหาเซิร์ฟเวอร์จริง ทำให้ API ตอบสนองได้เหมาะสม เมื่อมีส่วนนี้แล้ว ที่เหลือของแอปก็ยังคงเรียบง่าย เราต์แต่ละตัวโฟกัสงานของตัวเองได้ เพราะมั่นใจว่าข้อมูลที่ได้รับถูกต้องแล้ว ต่อไปจะเชื่อมต่อเราต์และเริ่มบันทึกงานลง MongoDB

ขั้นตอนที่ 4: จัดการ ObjectId และการตรวจสอบคำขอ

เมื่อรู้แล้วว่างานหน้าตาเป็นอย่างไร ต่อไปจัดการวิธีอ้างอิงงาน เวลาเราต์มีพารามิเตอร์ :id ค่านั้นจะมาเป็นสตริงธรรมดา แต่ MongoDB คาดหวังเป็น ObjectId หากสตริงนั้นรูปแบบไม่ถูกต้อง ไดรเวอร์จะโยนข้อผิดพลาดที่ไม่ค่อยช่วยอะไร แทนที่จะต้องจัดการในทุกเราต์ จะรวมไว้ด้วย middleware เพื่อให้ทุกเอ็นด์พอยต์มีพฤติกรรมเดียวกัน

ให้สร้างไฟล์ชื่อ middleware/parseObjectId.js แล้วเพิ่มโค้ดต่อไปนี้:

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;

มิดเดิลแวร์นี้จะรันก่อนตัวจัดการเราต์ ตรวจสอบว่า id ถูกต้อง แปลงให้เป็น ObjectId และผูกไว้ที่ req.taskId พอถึงขั้นตอนเราต์ทำงาน ก็จะได้ทำงานกับ ObjectId ที่ถูกต้องเสมอ และอินพุตที่ไม่ดีจะถูกปฏิเสธตั้งแต่แรกด้วยสถานะ 400 ที่ชัดเจน ต่อไปจะนำไปใช้กับเราต์และเชื่อมทุกอย่างเข้าด้วยกัน

ขั้นตอนที่ 5: สร้างเราต์ของ Task (CRUD + การกรอง)

ถึงตอนนี้ งานหนักส่วนใหญ่ทำเสร็จแล้ว เรากำหนดรูปแบบงานที่ถูกต้องไว้ และจัดการการพาร์สและตรวจสอบ ID แล้ว นั่นทำให้โค้ดตัวจัดการเราต์โฟกัสเพียงอย่างเดียวคือคุยกับฐานข้อมูล

มาสร้างเราต์ API จริงโดยสร้างไฟล์ชื่อ routes/tasks.js แล้วเพิ่มโค้ดต่อไปนี้:

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;

แต่ละเราต์ด้านบนทำตามแพตเทิร์นเดียวกัน: รับอินพุตจากคำขอ ส่งผ่านฮีลเปอร์ที่สร้างไว้ก่อนหน้า เรียก MongoDB แล้วส่งผลลัพธ์กลับ ด้วยการตรวจสอบและการพาร์ส ID ที่จัดการไว้แล้ว โค้ดส่วนนี้จึงสั้นและคาดเดาได้ง่าย โดยสรุปคือ:

  • เราต์ POST สร้างงานใหม่ด้วย buildTaskDocument ก่อนอินเสิร์ต
  • เราต์ GET เลือกกรองตามหมวดหมู่ได้ และส่งผลลัพธ์เรียงจากรายการใหม่สุดก่อน
  • เราต์ PUT ใช้ buildTaskUpdate จึงอัปเดตบางส่วนได้อย่างปลอดภัยและสม่ำเสมอ
  • เราต์ DELETE ลบงานโดยใช้ ObjectId ที่พาร์สไว้แล้ว

ยังมีรายละเอียดอื่น ๆ ที่น่าสนใจในโค้ดนี้ด้วย:

  • find() ส่งกลับ cursor ไม่ใช่อาเรย์ จึงเชน .toArray() หลังการ sort เพื่อให้ได้ผลลัพธ์จริง
  • findOneAndUpdate พร้อมตัวเลือก returnDocument: 'after' จะได้เอกสารที่อัปเดตแล้วทันที
  • ส่งคืนรหัสสถานะ HTTP ที่เหมาะสม: 201 สำหรับการสร้าง 204 สำหรับการลบ และ 400 หรือ 404 ตามความเหมาะสม
  • ข้อผิดพลาดที่ไม่คาดคิดจะถูกส่งต่อไปที่ next(err) เพื่อให้จัดการรวมไว้ที่เดียว แทนที่จะทำซ้ำในทุกเราต์

ขั้นตอนที่ 6: เชื่อมทุกอย่างเข้าด้วยกันในเซิร์ฟเวอร์

ตอนนี้ทุกส่วนพร้อมแล้ว มีการตรวจสอบที่ดี เราต์ที่สะอาด และการเชื่อมต่อฐานข้อมูลที่ทำงานได้ ต่อไปคือเชื่อมทั้งหมดเข้าด้วยกันและเริ่มเซิร์ฟเวอร์ สร้างไฟล์ server.js ซึ่งเป็นจุดเริ่มต้นของแอปที่รวมทุกอย่างไว้:

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 };

ไฟล์นี้เชื่อมทุกอย่างเข้าด้วยกันอย่างเป็นระเบียบ ตรวจสอบตัวแปรสภาพแวดล้อมก่อนรัน ใช้ helmet เพื่อความปลอดภัยพื้นฐาน และจำกัดขนาด JSON ที่เข้ามาเพื่อป้องกันรับเพย์โหลดใหญ่โดยไม่ตั้งใจ อีกทั้งยังเสิร์ฟโฟลเดอร์ public/ ทำให้เฟรันต์เอนด์อยู่ในแอปเดียวกันโดยไม่ต้องมีเซิร์ฟเวอร์แยก

เราต์ทั้งหมดถูกเมานต์ไว้ใต้ /tasks และคำขอที่ไม่ตรงใด ๆ จะได้กลับมาเป็น 404 ที่ชัดเจน จัดการข้อผิดพลาดไว้ที่จุดเดียวแทนการทำซ้ำในทุกเราต์ ช่วยให้สม่ำเสมอและดูแลง่าย การเชื่อมต่อฐานข้อมูลถูกตั้งค่าก่อนเซิร์ฟเวอร์เริ่มฟังคำขอ จึงไม่รับคำขอก่อนพร้อมใช้งาน และปิดตัวแบบนุ่มนวลเพื่อปิดการเชื่อมต่ออย่างถูกต้อง

ตอนนี้รันและตรวจสอบว่าทุกอย่างทำงาน โดยใช้คำสั่ง npm run dev หากเชื่อมต่อทุกอย่างถูกต้อง จะเห็นสิ่งที่คล้ายกับนี้:

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

ถึงตอนนี้ ทุกอย่างเชื่อมต่อและรันแล้ว! เรามี API ที่ทำงานได้ครบถ้วน มาเริ่มทดสอบกัน

ขั้นตอนที่ 7: ทดสอบ API Endpoints

ก่อนสร้าง UI ควรยืนยันว่าแต่ละเอ็นด์พอยต์ทำงานได้ด้วยตัวเอง จะช่วยให้ดีบักง่ายขึ้น เพราะจะรู้ได้เร็วว่าปัญหามาจาก API หรือฝั่งเฟรันต์เอนด์ จะใช้ Postman (หรือไคลเอนต์ HTTP ใด ๆ) แล้วรันคำขอสักชุดตามลำดับ แต่ละคำขอจะต่อยอดจากข้อมูลที่สร้างในขั้นก่อนหน้า

สร้างงาน: ส่งคำขอ POST พร้อม title, description และ category หากทุกอย่างเรียบร้อย API จะส่งกลับ 201 Created พร้อมงานใหม่และค่า _id ที่สร้างขึ้น

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

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

สร้างเพิ่มอีกไม่กี่งาน: รันคำขอ POST แบบเดิมด้วยบอดีต่างกัน เพื่อให้มีข้อมูลพอสำหรับทดสอบการลิสต์และการกรอง หลังจากนี้จะมีชุดข้อมูลเล็ก ๆ ให้คิวรี

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

ลิสต์งานทั้งหมด: ส่งคืนงานทั้งหมด เรียงจากใหม่สุดก่อน

GET http://localhost:3000/tasks

กรองตามหมวดหมู่: ส่งคืนเฉพาะงานหมวด Work อินเด็กซ์ที่เพิ่มไว้ก่อนหน้าช่วยให้คิวรีนี้เร็วแม้ข้อมูลจะโตขึ้น

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

อัปเดตงาน: ใช้ค่า _id จากงานที่เพิ่งสร้าง (คัดลอกจากการตอบกลับของ POST หรือจากเอ็นด์พอยต์ลิสต์) จะได้งานที่อัปเดตกลับมา ส่งเฉพาะฟิลด์ที่ต้องการเปลี่ยน ทำให้อัปเดตบางส่วนง่าย

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

{ "completed": true }

ลบงาน: ใช้ค่า _id ของงานที่ต้องการลบ เอ็นด์พอยต์จะส่งกลับ 204 No Content หากลองดึงงานเดิมอีกครั้ง จะได้สถานะ 404 พร้อมข้อความ "task not found"

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

ตอนนี้ยืนยันได้แล้วว่าเราต์ทั้งหมดทำงานตามคาด API ทำหน้าที่ของมันแล้ว ต่อไปสามารถสร้าง UI มาครอบทับ

ขั้นตอนที่ 8: เพิ่มเฟรันต์เอนด์เพื่อใช้งาน API

Postman ดีสำหรับการยืนยัน API แต่จะดีกว่าหากได้เห็นการทำงานบนเว็บไซต์จริง ๆ มาลองเพิ่มโค้ดฝั่งเฟรันต์เอนด์ให้กับแอปกัน

แทนการวางไฟล์ HTML และ CSS ขนาดใหญ่ไว้ที่นี่ โค้ดถูกเพิ่มไว้ใน GitHub repo แล้ว ไปคัดลอกเนื้อหาในไฟล์ index.html, styles.css และ favicon.svg ไปไว้ในโฟลเดอร์ public/ ของโปรเจกต์ บันทึกการเปลี่ยนแปลงแล้วไปที่ localhost:3000 จากนั้นวางโค้ดด้านล่างลงในไฟล์ 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();

โค้ด JavaScript นี้เป็นจุดที่ทุกอย่างเชื่อมกลับไปยัง API ที่สร้างไว้ก่อนหน้า แต่ละฟังก์ชันแมปกับเราต์หนึ่งตัว ทำให้ฝั่งเฟรันต์เอนด์อ่านง่าย คำขอทั้งหมดผ่านฮีลเปอร์ api() เพียงตัวเดียวด้วย จึงจัดการข้อผิดพลาดไว้ที่เดียว ไม่ต้องทำซ้ำ

การกรองทำงานด้วย query string อย่างเช่น ?category=Work ซึ่งผูกกับลอจิกฝั่งแบ็กเอนด์ที่เพิ่มไว้แล้ว หลังการสร้าง อัปเดต หรือลบงาน แอปจะดึงลิสต์ล่าสุดอีกครั้งเพื่อให้ UI ซิงก์เสมอ ตัวนับงานเป็นรายละเอียดเล็ก ๆ แต่ช่วยให้แอปรู้สึกมีชีวิตมากขึ้นระหว่างใช้งาน

ตอนนี้รีสตาร์ทเซิร์ฟเวอร์ด้วย npm run dev แล้วเปิด http://localhost:3000 เพื่อทดลองใช้ Task Manager เพิ่มงานไม่กี่งาน อัปเดต สลับหมวดหมู่ และลบบางงาน ตอนนี้เฟรันต์เอนด์และแบ็กเอนด์เชื่อมต่อทำงานร่วมกันแบบ end-to-end แล้ว

สรุป

ขอแสดงความยินดี คุณได้สร้าง API ตัวจัดการงานครบถ้วนตั้งแต่ศูนย์ด้วย Node.js, Express และ MongoDB จัดการการตรวจสอบข้อมูล รักษาเราต์ให้กระชับ และเชื่อมทุกอย่างเข้าด้วยกันจนใช้งานได้ ที่สำคัญคือคงรูปแบบการทำงานตามจริงของ MongoDB แทนการซ่อนหลังเลเยอร์นามธรรม จากตรงนี้สามารถพัฒนาแอปต่อให้พร้อมใช้งานจริงได้

สาระสำคัญ

  • การใช้มิดเดิลแวร์และรวมตรรกะการตรวจสอบไว้ที่เดียว ช่วยให้โค้ดสะอาดและคาดเดาได้
  • ไดรเวอร์ MongoDB แบบเนทีฟให้การควบคุมโดยไม่ต้องมีนามธรรมที่ไม่จำเป็น
  • API ที่มีโครงสร้างดีจะขยายต่อไปสู่แอปพร้อมใช้งานจริงได้ง่าย

FAQs

จำเป็นต้องใช้ Mongoose เพื่อสร้าง MongoDB API หรือไม่?

ไม่จำเป็น บทช่วยสอนนี้ใช้ไดรเวอร์ MongoDB แบบเนทีฟ ซึ่งให้การควบคุมมากกว่าและเบากว่า หากต้องการชั้นนามธรรมของสคีมา ค่อยเพิ่ม Mongoose ภายหลังก็ได้

จะตรวจสอบข้อมูลโดยไม่มีสคีมาได้อย่างไร?

สามารถรวมตรรกะการตรวจสอบไว้ในฟังก์ชันช่วยเหลือ (เช่น buildTaskDocument) เพื่อให้ทุกเราต์บังคับใช้กฎเดียวกันก่อนเขียนลงฐานข้อมูล

จะเกิดอะไรขึ้นถ้าส่ง ObjectId ไม่ถูกต้อง?

มิดเดิลแวร์จะปฏิเสธตั้งแต่ต้นด้วยการตอบกลับสถานะ 400 ป้องกันไม่ให้ MongoDB โยนข้อผิดพลาดที่สับสน

หัวข้อ
MongoDB

Top DataCamp Courses

Courses

MongoDB เบื้องต้นใน Python

3 ชม.
24.2K
เรียนรู้การจัดการและวิเคราะห์ข้อมูลที่มีโครงสร้างยืดหยุ่นด้วย MongoDB
ดูรายละเอียดRight Arrow
เริ่มหลักสูตร
ดูเพิ่มเติมRight Arrow