Courses
ลิสต์งานแบบพื้นฐานใช้งานได้ดีในช่วงแรก: เพิ่มงานไม่กี่งาน แล้วปิดงานเมื่อทำเสร็จ แต่พองานเพิ่มขึ้นเรื่อย ๆ การใช้งานก็เริ่มน่าอึดอัด วิธีหนึ่งที่ช่วยได้คือเพิ่มการจัดหมวดหมู่ เพื่อให้งานแต่ละงานอยู่ในหมวดอย่าง 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 โยนข้อผิดพลาดที่สับสน