ลิสต์งานแบบพื้นฐานใช้งานได้ดีในช่วงแรก: เพิ่มงานไม่กี่งาน แล้วปิดงานเมื่อทำเสร็จ แต่พองานเพิ่มขึ้นเรื่อย ๆ การใช้งานก็เริ่มน่าอึดอัด วิธีหนึ่งที่ช่วยได้คือเพิ่มการจัดหมวดหมู่ เพื่อให้งานแต่ละงานอยู่ในหมวดอย่าง 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 สำหรับค่าจริงของคุณ ดังนี้:
MONGO_URI = mongodb://127.0.0.1:27017
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 ( ) ;
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) ;
}
} ) ;
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) ; }
} ) ;
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) ; }
} ) ;
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) ;
}
} ) ;
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" }
ขับเคลื่อนโดย
ลิสต์งานทั้งหมด: ส่งคืนงานทั้งหมด เรียงจากใหม่สุดก่อน
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 ที่มีโครงสร้างดีจะขยายต่อไปสู่แอปพร้อมใช้งานจริงได้ง่าย
ลิสต์งานแบบพื้นฐานใช้งานได้ดีในช่วงแรก: เพิ่มงานไม่กี่งาน แล้วปิดงานเมื่อทำเสร็จ แต่พองานเพิ่มขึ้นเรื่อย ๆ การใช้งานก็เริ่มน่าอึดอัด วิธีหนึ่งที่ช่วยได้คือเพิ่มการจัดหมวดหมู่ เพื่อให้งานแต่ละงานอยู่ในหมวดอย่าง Work, Study, Personal เป็นต้น และสามารถกรองลิสต์ตามหมวดหมู่ที่ต้องการได้
ในบทช่วยสอนนี้ จะอธิบายวิธีสร้าง REST API สำหรับจัดงานตามหมวดหมู่ด้วย Node.js และ MongoDB เมื่อจบแล้วจะมีเซิร์ฟเวอร์ Node.js ที่ใช้งานได้จริงพร้อม 5 เอ็นด์พอยต์: create, list, get, update และ delete พร้อมตัวกรองตามหมวดหมู่ อีกทั้งยังเห็นวิธีบังคับใช้การตรวจสอบหมวดหมู่ด้วย MongoDB ไดรเวอร์แบบเนทีฟ โดยไม่ต้องพึ่ง ODM อย่าง Mongoose
สิ่งที่จะได้เรียนรู้
สามารถดูโค้ดฉบับเต็มของบทช่วยสอนนี้ได้บน GitHub หากต้องการโคลนแล้วอ่านไปพร้อมกัน
ข้อกำหนดเบื้องต้น
ก่อนเริ่ม ควรมีสิ่งต่อไปนี้:
ขั้นตอนที่ 1: ตั้งค่าโปรเจกต์
มาเริ่มสร้างตัวจัดการงานแบบง่ายกัน สิ่งแรกคือสร้างโฟลเดอร์ใหม่และเริ่มต้นโปรเจกต์ Node โดยรันคำสั่งด้านล่างในเทอร์มินัล:
ถัดไป ติดตั้งไลบรารีรันไทม์สี่ตัวที่แอปต้องใช้: express สำหรับระบบเราต์ ไดรเวอร์ MongoDB อย่างเป็นทางการ dotenv สำหรับโหลดตัวแปรสภาพแวดล้อม และสุดท้าย helmet สำหรับตั้งค่าเฮดเดอร์ด้านความปลอดภัย
สำหรับการพัฒนา ให้ติดตั้ง nodemon เป็น dev dependency ด้วยคำสั่งด้านล่าง เพื่อให้เซิร์ฟเวอร์รีโหลดอัตโนมัติเมื่อไฟล์มีการเปลี่ยนแปลง:
จากนั้น เปิดโฟลเดอร์โปรเจกต์ใน IDE แล้วอัปเดตส่วน scripts ใน package.json ให้มีดังนี้ ซึ่งจะช่วยให้เริ่มเซิร์ฟเวอร์ด้วย npm start หรือรันโหมดพัฒนาด้วย npm run dev:
โครงสร้างที่กำลังจะสร้างมีดังนี้ แต่ละโฟลเดอร์มีหน้าที่ชัดเจน: db/ สำหรับการเชื่อมต่อฐานข้อมูล lib/ สำหรับฟังก์ชันช่วยเหลือ middleware/ สำหรับ Express middleware และ routes/ สำหรับกำหนดตัวจัดการคำขอ โฟลเดอร์ public/ จะเก็บเฟรันต์เอนด์เล็ก ๆ ที่จะเพิ่มภายหลัง แม้โปรเจกต์เล็ก โครงสร้างนี้ช่วยให้ง่ายต่อการติดตาม และเห็นชัดว่าควรใส่โค้ดใหม่ตรงไหน จะสร้างเลย์เอาต์โฟลเดอร์ตอนนี้แล้วค่อยเติมไฟล์ภายหลังก็ได้ หรือจะข้ามไปแล้วเพิ่มไฟล์ทีละส่วนตามขั้นตอนก็ได้:
ขั้นตอนที่ 2: เชื่อมต่อกับ MongoDB
เพื่อบันทึกและดึงข้อมูลงาน ขั้นแรกต้องเชื่อมต่อแอปกับ MongoDB โดยใช้สตริงการเชื่อมต่อ ซึ่งบอกไดรเวอร์ MongoDB ว่าจะเชื่อมต่อฐานข้อมูลอย่างไร แทนที่จะฮาร์ดโค้ดไว้ในโค้ด ควรเก็บไว้ในไฟล์สภาพแวดล้อม เพื่อป้องกันข้อมูลอ่อนไหวอยู่ในซอร์สโค้ด และสลับสภาพแวดล้อมได้ง่าย
สร้างไฟล์ .env.example เพื่ออธิบายตัวแปรที่ต้องใช้ และไฟล์ .env สำหรับค่าจริงของคุณ ดังนี้:
เมื่อมีสตริงการเชื่อมต่อแล้ว ก็ใช้ตั้งค่าการเชื่อมต่อ MongoDB ได้ สร้างไฟล์ db/connect.js ซึ่งจะเป็นที่เริ่มต้น MongoDB client และให้ส่วนอื่นของแอปเรียกใช้ได้:
ไฟล์นี้ตั้งค่า MongoDB client ตัวเดียวให้ส่วนอื่นของแอปใช้ซ้ำ ควรสร้างเพียงครั้งเดียว เพราะไดรเวอร์จัดการ connection pooling ให้แล้ว การสร้างไคลเอนต์ใหม่ทุกคำขออาจดูไม่เป็นไรช่วงแรก แต่จะทำให้ประสิทธิภาพแย่ลงอย่างรวดเร็ว
ขั้นตอนที่ 3: กำหนดโครงสร้างและการตรวจสอบของ Task
ถึงตอนนี้ แอปเชื่อมต่อ MongoDB ได้แล้ว ต่อไปต้องกำหนดว่ารูปแบบของงาน (task) ควรเป็นอย่างไรก่อนจะเริ่มบันทึกข้อมูล
ตรงนี้การใช้ MongoDB ไดรเวอร์แบบเนทีฟจะรู้สึกต่างเล็กน้อย เพราะไม่มีไฟล์สคีมาแบบที่มีใน Mongoose แทนที่จะเป็นสคีมา ไดรเวอร์จะรับอ็อบเจ็กต์ตามรูปร่างที่เราอินเสิร์ตเข้าไปในฐานข้อมูล ซึ่งอาจดูหลวมในตอนแรก แต่จริง ๆ มีประโยชน์เพราะเราใกล้เคียงกับสิ่งที่ MongoDB ทำจริง ๆ และไม่มีอะไรซ่อนอยู่หลังเลเยอร์นามธรรม
อย่างไรก็ตาม เรายังคงต้องการการตรวจสอบความถูกต้องของข้อมูล ดังนั้นแทนที่จะกระจายตรรกะนี้ไปตามเราต์ต่าง ๆ จะรวบไว้ที่เดียวเพื่อให้ทุกอย่างยึดกฎเดียวกัน สร้างไฟล์ชื่อ lib/taskDocument.js แล้วใส่โค้ดด้านล่าง:
ไฟล์นี้ทำหน้าที่เป็นด่านก่อนเข้าฐานข้อมูล ทุกคำขอ create หรือ update จะผ่านส่วนนี้ ทำให้กำหนดกฎไว้ที่เดียว
ช่วยให้รูปแบบงานถูกต้องเสมอ รักษาค่าหมวดหมู่ให้สม่ำเสมอ และลดปัญหายามคิวรีข้อมูลในภายหลัง คลาส ValidationError แบบกำหนดเองยังช่วยแยกแยะอินพุตที่ไม่ดีออกจากปัญหาเซิร์ฟเวอร์จริง ทำให้ API ตอบสนองได้เหมาะสม เมื่อมีส่วนนี้แล้ว ที่เหลือของแอปก็ยังคงเรียบง่าย เราต์แต่ละตัวโฟกัสงานของตัวเองได้ เพราะมั่นใจว่าข้อมูลที่ได้รับถูกต้องแล้ว ต่อไปจะเชื่อมต่อเราต์และเริ่มบันทึกงานลง MongoDB
ขั้นตอนที่ 4: จัดการ ObjectId และการตรวจสอบคำขอ
เมื่อรู้แล้วว่างานหน้าตาเป็นอย่างไร ต่อไปจัดการวิธีอ้างอิงงาน เวลาเราต์มีพารามิเตอร์ :id ค่านั้นจะมาเป็นสตริงธรรมดา แต่ MongoDB คาดหวังเป็น ObjectId หากสตริงนั้นรูปแบบไม่ถูกต้อง ไดรเวอร์จะโยนข้อผิดพลาดที่ไม่ค่อยช่วยอะไร แทนที่จะต้องจัดการในทุกเราต์ จะรวมไว้ด้วย middleware เพื่อให้ทุกเอ็นด์พอยต์มีพฤติกรรมเดียวกัน
ให้สร้างไฟล์ชื่อ middleware/parseObjectId.js แล้วเพิ่มโค้ดต่อไปนี้:
มิดเดิลแวร์นี้จะรันก่อนตัวจัดการเราต์ ตรวจสอบว่า id ถูกต้อง แปลงให้เป็น ObjectId และผูกไว้ที่ req.taskId พอถึงขั้นตอนเราต์ทำงาน ก็จะได้ทำงานกับ ObjectId ที่ถูกต้องเสมอ และอินพุตที่ไม่ดีจะถูกปฏิเสธตั้งแต่แรกด้วยสถานะ 400 ที่ชัดเจน ต่อไปจะนำไปใช้กับเราต์และเชื่อมทุกอย่างเข้าด้วยกัน
ขั้นตอนที่ 5: สร้างเราต์ของ Task (CRUD + การกรอง)
ถึงตอนนี้ งานหนักส่วนใหญ่ทำเสร็จแล้ว เรากำหนดรูปแบบงานที่ถูกต้องไว้ และจัดการการพาร์สและตรวจสอบ ID แล้ว นั่นทำให้โค้ดตัวจัดการเราต์โฟกัสเพียงอย่างเดียวคือคุยกับฐานข้อมูล
มาสร้างเราต์ API จริงโดยสร้างไฟล์ชื่อ routes/tasks.js แล้วเพิ่มโค้ดต่อไปนี้:
แต่ละเราต์ด้านบนทำตามแพตเทิร์นเดียวกัน: รับอินพุตจากคำขอ ส่งผ่านฮีลเปอร์ที่สร้างไว้ก่อนหน้า เรียก MongoDB แล้วส่งผลลัพธ์กลับ ด้วยการตรวจสอบและการพาร์ส ID ที่จัดการไว้แล้ว โค้ดส่วนนี้จึงสั้นและคาดเดาได้ง่าย โดยสรุปคือ:
ยังมีรายละเอียดอื่น ๆ ที่น่าสนใจในโค้ดนี้ด้วย:
ขั้นตอนที่ 6: เชื่อมทุกอย่างเข้าด้วยกันในเซิร์ฟเวอร์
ตอนนี้ทุกส่วนพร้อมแล้ว มีการตรวจสอบที่ดี เราต์ที่สะอาด และการเชื่อมต่อฐานข้อมูลที่ทำงานได้ ต่อไปคือเชื่อมทั้งหมดเข้าด้วยกันและเริ่มเซิร์ฟเวอร์ สร้างไฟล์ server.js ซึ่งเป็นจุดเริ่มต้นของแอปที่รวมทุกอย่างไว้:
ไฟล์นี้เชื่อมทุกอย่างเข้าด้วยกันอย่างเป็นระเบียบ ตรวจสอบตัวแปรสภาพแวดล้อมก่อนรัน ใช้ helmet เพื่อความปลอดภัยพื้นฐาน และจำกัดขนาด JSON ที่เข้ามาเพื่อป้องกันรับเพย์โหลดใหญ่โดยไม่ตั้งใจ อีกทั้งยังเสิร์ฟโฟลเดอร์ public/ ทำให้เฟรันต์เอนด์อยู่ในแอปเดียวกันโดยไม่ต้องมีเซิร์ฟเวอร์แยก
เราต์ทั้งหมดถูกเมานต์ไว้ใต้ /tasks และคำขอที่ไม่ตรงใด ๆ จะได้กลับมาเป็น 404 ที่ชัดเจน จัดการข้อผิดพลาดไว้ที่จุดเดียวแทนการทำซ้ำในทุกเราต์ ช่วยให้สม่ำเสมอและดูแลง่าย การเชื่อมต่อฐานข้อมูลถูกตั้งค่าก่อนเซิร์ฟเวอร์เริ่มฟังคำขอ จึงไม่รับคำขอก่อนพร้อมใช้งาน และปิดตัวแบบนุ่มนวลเพื่อปิดการเชื่อมต่ออย่างถูกต้อง
ตอนนี้รันและตรวจสอบว่าทุกอย่างทำงาน โดยใช้คำสั่ง npm run dev หากเชื่อมต่อทุกอย่างถูกต้อง จะเห็นสิ่งที่คล้ายกับนี้:
ถึงตอนนี้ ทุกอย่างเชื่อมต่อและรันแล้ว! เรามี API ที่ทำงานได้ครบถ้วน มาเริ่มทดสอบกัน
ขั้นตอนที่ 7: ทดสอบ API Endpoints
ก่อนสร้าง UI ควรยืนยันว่าแต่ละเอ็นด์พอยต์ทำงานได้ด้วยตัวเอง จะช่วยให้ดีบักง่ายขึ้น เพราะจะรู้ได้เร็วว่าปัญหามาจาก API หรือฝั่งเฟรันต์เอนด์ จะใช้ Postman (หรือไคลเอนต์ HTTP ใด ๆ) แล้วรันคำขอสักชุดตามลำดับ แต่ละคำขอจะต่อยอดจากข้อมูลที่สร้างในขั้นก่อนหน้า
สร้างงาน: ส่งคำขอ POST พร้อม title, description และ category หากทุกอย่างเรียบร้อย API จะส่งกลับ 201 Created พร้อมงานใหม่และค่า _id ที่สร้างขึ้น
สร้างเพิ่มอีกไม่กี่งาน: รันคำขอ POST แบบเดิมด้วยบอดีต่างกัน เพื่อให้มีข้อมูลพอสำหรับทดสอบการลิสต์และการกรอง หลังจากนี้จะมีชุดข้อมูลเล็ก ๆ ให้คิวรี
ลิสต์งานทั้งหมด: ส่งคืนงานทั้งหมด เรียงจากใหม่สุดก่อน
กรองตามหมวดหมู่: ส่งคืนเฉพาะงานหมวด Work อินเด็กซ์ที่เพิ่มไว้ก่อนหน้าช่วยให้คิวรีนี้เร็วแม้ข้อมูลจะโตขึ้น
อัปเดตงาน: ใช้ค่า _id จากงานที่เพิ่งสร้าง (คัดลอกจากการตอบกลับของ POST หรือจากเอ็นด์พอยต์ลิสต์) จะได้งานที่อัปเดตกลับมา ส่งเฉพาะฟิลด์ที่ต้องการเปลี่ยน ทำให้อัปเดตบางส่วนง่าย
ลบงาน: ใช้ค่า _id ของงานที่ต้องการลบ เอ็นด์พอยต์จะส่งกลับ 204 No Content หากลองดึงงานเดิมอีกครั้ง จะได้สถานะ 404 พร้อมข้อความ "task not found"
ตอนนี้ยืนยันได้แล้วว่าเราต์ทั้งหมดทำงานตามคาด API ทำหน้าที่ของมันแล้ว ต่อไปสามารถสร้าง UI มาครอบทับ
ขั้นตอนที่ 8: เพิ่มเฟรันต์เอนด์เพื่อใช้งาน API
Postman ดีสำหรับการยืนยัน API แต่จะดีกว่าหากได้เห็นการทำงานบนเว็บไซต์จริง ๆ มาลองเพิ่มโค้ดฝั่งเฟรันต์เอนด์ให้กับแอปกัน
แทนการวางไฟล์ HTML และ CSS ขนาดใหญ่ไว้ที่นี่ โค้ดถูกเพิ่มไว้ใน GitHub repo แล้ว ไปคัดลอกเนื้อหาในไฟล์ index.html, styles.css และ favicon.svg ไปไว้ในโฟลเดอร์ public/ ของโปรเจกต์ บันทึกการเปลี่ยนแปลงแล้วไปที่ localhost:3000 จากนั้นวางโค้ดด้านล่างลงในไฟล์ app.js:
โค้ด 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 แทนการซ่อนหลังเลเยอร์นามธรรม จากตรงนี้สามารถพัฒนาแอปต่อให้พร้อมใช้งานจริงได้
สาระสำคัญ