1. 什麼是 Node.js? / What is Node.js?
Node.js 是一個基於 Chrome V8 引擎的 JavaScript 執行環境。它讓開發者可以在伺服器端執行 JavaScript,打破了 JS 只能在瀏覽器運行的限制。
Node.js is a JavaScript runtime built on Chrome's V8 engine. It enables developers to run JavaScript on the server-side, breaking the limitation of JS only running in browsers.
核心特點: 非同步、事件驅動、非阻塞 I/O 模型。
Key Features: Asynchronous, event-driven, and non-blocking I/O model.
1.1 如何安裝 Node.js / How to Install Node.js
安裝 Node.js 最簡單的方式是前往官方網站下載 LTS (長期支援) 版本。LTS 版本最為穩定,適合大多數開發者。
The easiest way to install Node.js is to download the LTS (Long Term Support) version from the official website. The LTS version is stable and recommended for most users.
安裝完成後,打開終端機 (Terminal) 並輸入以下指令來確認是否安裝成功:
After installation, open your terminal and run the following commands to verify if it was successful:
# 檢查 Node.js 版本 / Check Node.js version
node -v
# 檢查 NPM 版本 / Check NPM version
npm -v
💡 專業建議: 如果你需要切換不同版本的 Node.js(例如某些舊專案需要舊版本),建議使用 NVM (Node Version Manager) 來管理。
💡 Pro Tip: If you need to switch between different Node.js versions, it is highly recommended to use NVM (Node Version Manager).
1.2 Node.js 不是瀏覽器 / Node.js is NOT a Browser
初學者最常見的誤解之一,是以為「Node.js 就是沒有畫面的 Chrome」。事實上,Node.js 只借用了 V8 引擎,但它並不包含瀏覽器的 API。
- ❌ 沒有
document - ❌ 沒有
window - ❌ 沒有 DOM
A common misconception is thinking Node.js is just "Chrome without UI". In reality, Node.js only uses the V8 engine and does NOT include browser APIs.
- ❌ No
document - ❌ No
window - ❌ No DOM
// 這段程式碼在 Node.js 會直接報錯
console.log(document.title);
1.3 為什麼 Node.js 適合高併發 / Why Node.js Handles Concurrency Well
Node.js 的核心優勢並不是「速度快」,而是它的 非阻塞 I/O 模型。
當伺服器需要等待資料庫或檔案系統回應時,Node.js 不會閒著,而是繼續處理其他請求。
The key advantage of Node.js is not raw speed, but its non-blocking I/O model.
While waiting for databases or file systems, Node.js continues handling other requests.
💡 重點: Node.js 非常適合「大量請求 + 等待 I/O」的應用。
💡 Key Point: Node.js excels at handling many concurrent I/O-bound requests.
1.4 Node.js 的典型使用場景 / Common Use Cases
- ✔ RESTful API / Backend for Frontend
- ✔ 即時應用(Chat、WebSocket)
- ✔ 微服務(Microservices)
- ✔ 前端建構工具(Vite、Webpack)
- ✔ RESTful APIs / BFF
- ✔ Real-time apps (Chat, WebSocket)
- ✔ Microservices
- ✔ Frontend build tools
1.5 什麼情況不適合使用 Node.js / When NOT to Use Node.js
Node.js 並非萬能。以下情境需特別小心:
- ⚠️ 大量 CPU 密集運算(影像處理、科學計算)
- ⚠️ 單一請求就會長時間佔用 CPU
這類情況通常更適合使用多執行緒或多進程語言。
Node.js is not a silver bullet. Be cautious in these cases:
- ⚠️ CPU-intensive tasks (image processing, scientific computing)
- ⚠️ Long-running blocking computations
Multi-threaded or multi-process languages may be a better fit.
2. 同步 vs 非同步 / Sync vs Async
在 Node.js 中,理解兩者的差異至關重要。同步操作會阻塞執行緒,而非同步則允許程式在等待 I/O 時繼續處理其他任務。
In Node.js, understanding the difference is crucial. Synchronous operations block the thread, while asynchronous allows the program to handle other tasks while waiting for I/O.
❌ 同步做法 / Synchronous (Blocking)
const fs = require('fs');
// 讀取檔案時會卡住 / Execution stops here until file is read
const data = fs.readFileSync('file.txt');
console.log(data);
console.log('最後才執行 / Runs last');
✅ 非同步做法 / Asynchronous (Non-blocking)
const fs = require('fs');
fs.readFile('file.txt', (err, data) => {
if (err) throw err;
console.log('檔案讀完才跑這 / Runs after file is read');
});
console.log('我會先被執行 / I will run first!');
2.1 為什麼同步在伺服器是危險的 / Why Sync is Dangerous on Servers
在 Node.js 中,同步程式碼會阻塞整個事件循環。這代表:
- ❌ 其他使用者請求會被卡住
- ❌ 整個伺服器暫時「失去回應」
In Node.js, synchronous code blocks the entire event loop, meaning:
- ❌ Other requests are blocked
- ❌ The server becomes unresponsive
// ❌ 錯誤示範:同步阻塞
app.get('/data', (req, res) => {
const data = fs.readFileSync('big.json');
res.send(data);
});
⚠️ 重點: 在後端,*Sync API 幾乎只適合啟動階段。
⚠️ Key: *Sync APIs should almost never be used in request handlers.
2.2 Callback Hell 是怎麼誕生的 / How Callback Hell Happens
早期 Node.js 只有 callback。當多個非同步任務需要「照順序」執行時,程式碼就會開始向右漂移。
Early Node.js relied solely on callbacks. When async tasks must run sequentially, code starts drifting right.
// ❌ Callback Hell
login(user, () => {
getProfile(() => {
getOrders(() => {
sendResponse();
});
});
});
問題不只是不美觀,而是:
- 錯誤處理困難
- 流程難以閱讀
- 難以維護與測試
The problem is not just aesthetics, but:
- Error handling is painful
- Control flow is unclear
- Hard to maintain
2.3 Promise 的出現 / The Rise of Promises
Promise 讓非同步流程「攤平」,但過度 chaining 仍然會造成可讀性問題。
Promises flatten async flows, but excessive chaining can still hurt readability.
login(user)
.then(getProfile)
.then(getOrders)
.then(sendResponse)
.catch(handleError);
2.4 async / await 的真正意義 / What async/await Really Means
async / await 只是 Promise 的語法糖,不是同步。
它讓你用「像同步」的方式寫非同步程式。
async / await is syntactic sugar over Promises — not synchronous.
async function handler() {
const user = await login();
const profile = await getProfile(user);
return profile;
}
2.5 async/await 地獄 / async-await Hell
async/await 最大的陷阱是「不小心變成同步流程」。
The biggest pitfall of async/await is accidentally forcing sequential execution.
// ❌ 效能陷阱(每個 await 都在等)
for (const id of ids) {
const data = await fetchData(id);
console.log(data);
}
2.6 正確處理多個非同步任務 / Handling Async Tasks Properly
如果任務彼此獨立,應該平行處理:
If tasks are independent, run them in parallel:
// ✅ 正確做法
const results = await Promise.all(
ids.map(id => fetchData(id))
);
✅ 原則: await 用來「等待依賴」,不是用來偷懶。
✅ Rule: Use await for dependencies, not convenience.
3. 事件循環 / Event Loop
這是 Node.js 的靈魂。儘管它是單執行緒,但透過「事件循環」機制,它能將耗時任務(如資料庫查詢)交給系統處理,處理完後再透過回呼函數通知主執行緒。
This is the heart of Node.js. Although single-threaded, the Event Loop offloads heavy tasks (like DB queries) to the system, notifying the main thread via callbacks once completed.
3.1 Event Loop 在做什麼 / What Does the Event Loop Do?
Node.js 本身是單執行緒,但它可以同時處理大量請求,靠的就是 Event Loop。
Event Loop 的工作只有一個:
👉 不斷檢查「現在能不能執行下一個任務」
Node.js is single-threaded, yet handles massive concurrency thanks to the Event Loop.
The Event Loop has one job:
👉 Continuously check if the next task can be executed
3.2 Call Stack 與 Task Queue / Call Stack & Task Queue
Call Stack 是目前正在執行的同步程式碼。
當 Stack 清空時,Event Loop 才會考慮處理非同步任務。
The Call Stack holds currently executing synchronous code.
Only when the stack is empty will the Event Loop process async tasks.
[ Call Stack ]
| main() |
| foo() |
| bar() |
--------------
(Call Stack must be empty first)
3.3 Microtask vs Macrotask
非同步任務並不都一樣,它們會進入不同的佇列:
- Microtask Queue:Promise.then / await
- Macrotask Queue:setTimeout / setInterval / I/O
👉 Microtask 永遠優先於 Macrotask
Async tasks are placed into different queues:
- Microtask Queue: Promise.then / await
- Macrotask Queue: setTimeout / setInterval / I/O
👉 Microtasks always run before macrotasks
3.4 Event Loop 執行流程 / Execution Flow
1️⃣ 執行 Call Stack(同步程式碼)
2️⃣ Call Stack 清空
3️⃣ 清空所有 Microtask Queue
4️⃣ 執行一個 Macrotask
5️⃣ 回到第 2 步(無限循環)
這個順序是理解 Node.js 行為的「黃金法則」。
This order is the golden rule of understanding Node.js behavior.
3.5 經典考題:console.log 順序解析
console.log('A');
setTimeout(() => {
console.log('B');
}, 0);
Promise.resolve().then(() => {
console.log('C');
});
console.log('D');
正確輸出順序:
- A(同步)
- D(同步)
- C(Microtask)
- B(Macrotask)
Correct output order:
- A (sync)
- D (sync)
- C (microtask)
- B (macrotask)
🧠 記住一句話: 同步 → Microtask → Macrotask
🧠 Remember: Sync → Microtask → Macrotask
4. 第一個伺服器 / Your First Server
讓我們建立一個真正的 Web 伺服器!這幾行代碼就能讓你在瀏覽器看到結果。
Let's build a real Web Server! These few lines of code will show results in your browser.
const http = require('http');
const server = http.createServer((req, res) => {
// 設定回應標頭 / Set response header
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Hello from Node.js!');
});
server.listen(3000, () => {
console.log('Server running at http://localhost:3000/');
});
4.1 為什麼非同步錯誤這麼難抓? / Why Async Errors Are Tricky
在同步程式中,錯誤會立即拋出;但在非同步中,錯誤通常是「未來才發生」。
這代表:如果你沒有正確等待(await),錯誤會直接逃走。
In synchronous code, errors are thrown immediately. In async code, errors happen in the future.
If you don’t await, the error escapes your control.
4.2 try/catch 的真正限制 / Limits of try-catch
// ❌ 抓不到錯誤
try {
fetchData(); // 沒有 await
} catch (err) {
console.error(err);
}
// ✅ 正確
try {
await fetchData();
} catch (err) {
console.error(err);
}
⚠️ 規則: try/catch 只能捕捉「當下 call stack 內」的錯誤。
⚠️ Rule: try/catch only catches errors in the current call stack.
4.3 Promise.all 的失敗策略 / Promise.all Failure Behavior
Promise.all 的設計是:
👉 只要有一個 Promise reject,整組立刻失敗
Promise.all is designed so that:
👉 One rejection fails everything
await Promise.all([
fetchUser(),
fetchOrders(), // ❌ 這裡失敗
fetchProfile()
]);
// → 整個 Promise.all reject
策略一:全部都很重要(Fail Fast)
try {
const [user, orders] = await Promise.all([
fetchUser(),
fetchOrders()
]);
} catch (err) {
return res.status(500).send('系統錯誤');
}
策略二:部分失敗可接受 / Partial Failure
const results = await Promise.allSettled([
fetchUser(),
fetchOrders(),
fetchCoupon()
]);
results.forEach(r => {
if (r.status === 'rejected') {
console.warn(r.reason);
}
});
策略三:自行包裝錯誤 / Manual Error Wrapping
const safe = p =>
p.then(data => ({ ok: true, data }))
.catch(err => ({ ok: false, err }));
const results = await Promise.all([
safe(fetchUser()),
safe(fetchOrders())
]);
4.5 Express 中的 async 錯誤處理
// ❌ Express 不會自動 catch async 錯誤
app.get('/api/data', async (req, res) => {
throw new Error('Boom!');
});
這個錯誤會直接 crash server(取決於 Node 版本)。
This error may crash the server if unhandled.
4.6 Express 正確錯誤處理模式
const asyncHandler = fn => (req, res, next) =>
Promise.resolve(fn(req, res, next)).catch(next);
app.get('/api/data', asyncHandler(async (req, res) => {
const data = await fetchData();
res.json(data);
}));
```html
// 全域錯誤中間件(一定要有)
app.use((err, req, res, next) => {
console.error(err);
res.status(500).json({ message: 'Internal Server Error' });
});
4.7 本章重點總結 / Key Takeaways
- ✅ try/catch 只抓得到 await 的 Promise
- ✅ Promise.all 是「全有或全無」
- ✅ Promise.allSettled 適合容錯場景
- ✅ Express async 錯誤一定要往 next 丟
5. Express 請求全流程 / Express Request Lifecycle
5.1 一個請求進來時,發生了什麼? / What Happens on a Request
當瀏覽器或前端發送一個 HTTP Request 到 Express,它會經過一條固定但可被你攔截的流程。
When a browser sends an HTTP request to Express, it goes through a fixed but interceptable pipeline.
Request
↓
Middleware(依註冊順序)
↓
Route Handler
↓
Response
5.2 Middleware 的真正角色 / What Middleware Really Is
Middleware 不是「工具函式」,而是請求管線上的一站。
每一個 middleware 都可以:
- 修改
req - 修改
res - 決定要不要往下走
Middleware is not just a helper — it is a station in the request pipeline.
5.3 Middleware 執行順序 / Execution Order
app.use(logger);
app.use(auth);
app.get('/profile', handler);
當請求 /profile 進來時,執行順序是:
- logger
- auth
- handler
Execution order for /profile:
- logger
- auth
- handler
⚠️ 規則: Express 永遠照你「註冊的順序」跑。
⚠️ Rule: Express always executes in registration order.
5.4 next() 的真正意義 / What next() Really Does
app.use((req, res, next) => {
console.log('檢查完成');
next();
});
next() 的意思是:
👉「我處理完了,換下一站」
如果你沒有呼叫 next(),請求就會停在這裡。
next() means:
👉 “I’m done, move to the next step”
5.5 Route Handler 是終點 / Route Handler as the Endpoint
app.get('/api/user', (req, res) => {
res.json({ name: 'Tom' });
});
一旦你呼叫:
res.send()res.json()res.end()
👉 請求流程立即結束
Once you call a response method, the request lifecycle ends.
5.6 錯誤在 Express 中怎麼流動?
app.use((req, res, next) => {
next(new Error('爆炸'));
});
Request
↓
Middleware
↓
❌ Error
↑
Error Middleware(4 個參數)
一旦 next(err) 被呼叫:
- ❌ 跳過所有正常 middleware
- ✅ 只找錯誤中間件
Calling next(err) skips normal middleware and jumps to error handlers.
5.7 Express Request Lifecycle(總覽)
Client Request
↓
Global Middleware
↓
Route Middleware
↓
Route Handler
↓
Response Sent
↓
(若發生錯誤)
↑
Error Middleware
5.8 本章重點總結 / Key Takeaways
- ✅ Middleware 是一條「管線」
- ✅ 順序錯,整個系統就錯
- ✅ next() 是流程控制,不是裝飾
- ✅ 錯誤一定要送到 Error Middleware
6. REST API 實戰 / REST API CRUD
REST 是一種軟體架構風格。透過不同的 HTTP 方法(Methods),我們可以對同一種資源執行不同的操作。
REST is a software architectural style. By using different HTTP methods, we can perform various operations on the same resource.
以下是 Express 中常見的 CRUD 路由範例:
Here is an example of common CRUD routes in Express:
// 取得所有項目 (Read) / Get all items
app.get('/api/items', (req, res) => {
res.json({ message: "獲取所有資料" });
});
// 建立新項目 (Create) / Create new item
app.post('/api/items', (req, res) => {
res.status(201).json({ message: "資料已建立" });
});
// 更新項目 (Update) / Update item
app.put('/api/items/:id', (req, res) => {
const id = req.params.id;
res.json({ message: `ID ${id} 已更新` });
});
// 刪除項目 (Delete) / Delete item
app.delete('/api/items/:id', (req, res) => {
res.json({ message: "資料已刪除" });
});
6.1 專案目標 / Project Goal
我們將從零開始建立一個 Todo REST API,具備真實後端必備能力。
- ✔ CRUD API
- ✔ Middleware
- ✔ async/await
- ✔ 錯誤處理
We will build a real Todo REST API from scratch.
6.2 專案結構 / Project Structure
todo-api/
├─ app.js
├─ server.js
├─ routes/
│ └─ todos.js
├─ controllers/
│ └─ todoController.js
├─ middleware/
│ └─ errorHandler.js
└─ data/
└─ todos.json
這種分層方式可以:
- 避免 app.js 爆炸
- 讓每個檔案只做一件事
This structure prevents bloated files and improves maintainability.
6.3 Server 啟動點 / server.js
const app = require('./app');
const PORT = 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
6.4 Express App 管線 / app.js
const express = require('express');
const todoRoutes = require('./routes/todos');
const errorHandler = require('./middleware/errorHandler');
const app = express();
app.use(express.json());
app.use('/api/todos', todoRoutes);
// 一定放最後
app.use(errorHandler);
module.exports = app;
6.5 Routes / routes/todos.js
const express = require('express');
const router = express.Router();
const controller = require('../controllers/todoController');
router.get('/', controller.getAll);
router.post('/', controller.create);
router.put('/:id', controller.update);
router.delete('/:id', controller.remove);
module.exports = router;
6.6 Controller / controllers/todoController.js
let todos = [];
exports.getAll = async (req, res) => {
res.json(todos);
};
exports.create = async (req, res) => {
const todo = { id: Date.now(), title: req.body.title };
todos.push(todo);
res.status(201).json(todo);
};
exports.update = async (req, res) => {
const todo = todos.find(t => t.id == req.params.id);
if (!todo) throw new Error('Todo not found');
todo.title = req.body.title;
res.status(200).json({
success: true,
data: todo
});
exports.remove = async (req, res) => {
todos = todos.filter(t => t.id != req.params.id);
res.status(204).end();
};
6.7 Error Middleware / middleware/errorHandler.js
module.exports = (err, req, res, next) => {
console.error(err.message);
res.status(500).json({ error: err.message });
};
6.8 一次 API 請求全流程回顧
Client
↓
express.json()
↓
/api/todos Router
↓
Controller (async)
↓
Response
↓
(錯誤時)
↓
Error Middleware
6.9 本章你學到什麼?
- ✔ Express 專案怎麼拆
- ✔ Middleware 正確位置
- ✔ async/await 真實使用場景
- ✔ 錯誤不會再炸 server
7. API 設計原則與實務 / API Design Best Practices
7.1 什麼是好的 API? / What Makes a Good API
好的 API 不是「功能多」,而是:
- 可預期(Predictable)
- 一致(Consistent)
- 不需要猜(No Guessing)
A good API is predictable, consistent, and guessable.
7.2 REST 命名慣例 / RESTful Naming
GET /api/todos → 取得清單
POST /api/todos → 新增
GET /api/todos/:id → 取得單筆
PUT /api/todos/:id → 更新
DELETE /api/todos/:id → 刪除
規則:
- 使用名詞,不用動詞
- 使用複數
- 動作交給 HTTP Method
7.3 HTTP Status Code 使用原則
| 狀態碼 | 使用時機 |
|---|---|
| 200 OK | 成功取得 / 更新 |
| 201 Created | 成功建立資源 |
| 204 No Content | 成功刪除(無回傳) |
| 400 Bad Request | 參數錯誤 |
| 404 Not Found | 資源不存在 |
| 500 Server Error | 後端例外 |
7.4 API Response 格式設計
// 成功
{
"success": true,
"data": {...}
}
// 失敗
{
"success": false,
"error": {
"message": "Todo not found"
}
}
永遠不要:
- 有時回字串
- 有時回物件
7.5 錯誤回應設計 / Error Response
throw {
status: 404,
message: 'Todo not found'
};
```js
// errorHandler.js
module.exports = (err, req, res, next) => {
res.status(err.status || 500).json({
success: false,
error: {
message: err.message || 'Server error'
}
});
};
7.6 用「API 使用者」的腦袋設計
- 前端要不要猜狀態?
- 錯誤好不好顯示給使用者?
- 每支 API 回傳是不是長一樣?
7.7 本章重點
- ✔ REST 命名是第一印象
- ✔ Status Code 是溝通工具
- ✔ Response 結構要固定
- ✔ 錯誤也是 API 的一部分
8. 環境變數與部署思維 / Environment & Deployment
在實際開發中,我們絕不應將資料庫密碼或 API 金鑰直接寫在程式碼裡。我們會使用 .env 檔案來管理這些敏感資訊。
In real-world development, we should never hardcode database passwords or API keys. We use .env files to manage this sensitive information.
# 安裝 dotenv 套件 / Install dotenv package
npm install dotenv
建立一個名為 .env 的檔案,並在程式碼中讀取它:
Create a file named .env and load it in your code:
require('dotenv').config();
const port = process.env.PORT || 3000;
const dbUrl = process.env.DATABASE_URL;
console.log(`Port is: ${port}`);
8.1 為什麼要分環境? / Why Environments Matter
真實世界的後端專案,至少會有三種環境:
- development(開發)
- test(測試)
- production(正式)
原因只有一個:設定不能共用
Real-world backends always separate environments.
8.2 Hard Code 是災難 / Never Hard Code Secrets
❌ 錯誤示範:
const PORT = 3000;
const DB_PASSWORD = '123456';
問題:
- 不能換環境
- 容易外洩
- 無法部署
8.3 使用 dotenv / Using dotenv
npm install dotenv
```js
// server.js
require('dotenv').config();
8.4 根據 NODE_ENV 調整行為
if (process.env.NODE_ENV === 'production') {
console.log = () => {};
}
常見用途:
- 關閉 debug log
- 隱藏錯誤細節
- 切換資料庫
8.5 Production Error Handling
module.exports = (err, req, res, next) => {
const isProd = process.env.NODE_ENV === 'production';
res.status(err.status || 500).json({
success: false,
error: {
message: isProd ? 'Server error' : err.message
}
});
};
正式環境:
- 不回 stack trace
- 不暴露內部資訊
8.6 Deployment Checklist
- ✔ 所有設定來自
process.env - ✔ 沒有 hard code 密碼
- ✔ Error handler 已區分 production
- ✔ 專案可用
npm start啟動
8.7 工程師思維轉換
能跑在我電腦上,不代表能跑在世界上。
Running locally is not the same as running in production.
8.8 Nodebook Final Summary
- 你理解了 Node.js 的非同步模型
- 你掌握了 Express 的請求生命週期
- 你能設計一致的 REST API
- 你知道如何準備一個可部署的後端
9. 接上 MongoDB:真正的後端資料層 / MongoDB Integration
9.1 為什麼使用 MongoDB? / Why MongoDB
MongoDB 是 Node.js 生態中最常見的資料庫之一,原因很簡單:
- JSON-like 文件(天然適合 JavaScript)
- Schema 彈性
- 非同步 I/O,與 Node 完美契合
MongoDB fits Node.js naturally due to its JSON-based document model.
9.2 安裝 MongoDB Driver(Mongoose)
npm install mongoose
Mongoose 是 MongoDB 在 Node.js 世界的標準 ORM / ODM。
9.3 MongoDB 連線設定 / Database Connection
// config/db.js
const mongoose = require('mongoose');
module.exports = async function connectDB() {
try {
await mongoose.connect(process.env.MONGO_URI);
console.log('MongoDB connected');
} catch (err) {
console.error(err);
process.exit(1);
}
};
9.4 啟動時連線資料庫
// server.js
require('dotenv').config();
const connectDB = require('./config/db');
const app = require('./app');
connectDB();
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
9.5 定義 Model / Mongoose Schema
// models/Todo.js
const mongoose = require('mongoose');
const TodoSchema = new mongoose.Schema({
title: {
type: String,
required: true
},
completed: {
type: Boolean,
default: false
}
}, {
timestamps: true
});
module.exports = mongoose.model('Todo', TodoSchema);
9.6 使用 MongoDB 重構 Controller
// controllers/todoController.js
const Todo = require('../models/Todo');
exports.getAll = async (req, res, next) => {
try {
const todos = await Todo.find();
res.json({ success: true, data: todos });
} catch (err) {
next(err);
}
};
exports.create = async (req, res, next) => {
try {
const todo = await Todo.create({ title: req.body.title });
res.status(201).json({ success: true, data: todo });
} catch (err) {
next(err);
}
};
exports.update = async (req, res, next) => {
try {
const todo = await Todo.findByIdAndUpdate(
req.params.id,
req.body,
{ new: true }
);
if (!todo) {
return next({ status: 404, message: 'Todo not found' });
}
res.json({ success: true, data: todo });
} catch (err) {
next(err);
}
};
exports.remove = async (req, res, next) => {
try {
const todo = await Todo.findByIdAndDelete(req.params.id);
if (!todo) {
return next({ status: 404, message: 'Todo not found' });
}
res.status(204).end();
} catch (err) {
next(err);
}
};
9.7 真正的非同步 I/O
現在的 await:
- 是真的在等 I/O
- 會進 Event Loop
- 不會阻塞主執行緒
This is real async I/O, not fake async.
9.8 含 MongoDB 的請求生命週期
Client
↓
Express Middleware
↓
Controller (await MongoDB)
↓
MongoDB I/O
↓
Response
9.9 本章總結
- ✔ Model / Controller / Route 分層
- ✔ 真實資料庫 CRUD
- ✔ async/await + Error Middleware
- ✔ 可長期運作的後端架構
10. JWT 認證流程(真正可用的後端) / JWT Authentication
10.1 JWT 是什麼? / What is JWT
JWT(JSON Web Token)是一種:
- 無狀態(Stateless)的認證方式
- 由 Server 簽發、Client 保存
- 每次請求都可驗證身份
JWT is a stateless authentication mechanism.
10.2 JWT Login Flow
User Login
↓
Verify Password
↓
Server signs JWT
↓
Client stores token
↓
Client sends token in Authorization header
↓
Server verifies token
10.3 安裝認證套件
npm install jsonwebtoken bcryptjs
10.4 User Model / models/User.js
const mongoose = require('mongoose');
const UserSchema = new mongoose.Schema({
email: {
type: String,
required: true,
unique: true
},
password: {
type: String,
required: true
}
});
module.exports = mongoose.model('User', UserSchema);
10.5 Register API
// controllers/authController.js
const User = require('../models/User');
const bcrypt = require('bcryptjs');
exports.register = async (req, res, next) => {
try {
const hashed = await bcrypt.hash(req.body.password, 10);
const user = await User.create({
email: req.body.email,
password: hashed
});
res.status(201).json({ success: true });
} catch (err) {
next(err);
}
};
10.6 Login + Sign JWT
const jwt = require('jsonwebtoken');
exports.login = async (req, res, next) => {
try {
const user = await User.findOne({ email: req.body.email });
if (!user) {
return next({ status: 401, message: 'Invalid credentials' });
}
const match = await bcrypt.compare(req.body.password, user.password);
if (!match) {
return next({ status: 401, message: 'Invalid credentials' });
}
const token = jwt.sign(
{ userId: user._id },
process.env.JWT_SECRET,
{ expiresIn: '1h' }
);
res.json({ success: true, token });
} catch (err) {
next(err);
}
};
10.7 Auth Middleware / middleware/auth.js
const jwt = require('jsonwebtoken');
module.exports = (req, res, next) => {
const auth = req.headers.authorization;
if (!auth || !auth.startsWith('Bearer ')) {
return next({ status: 401, message: 'No token' });
}
const token = auth.split(' ')[1];
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.userId = decoded.userId;
next();
} catch {
next({ status: 401, message: 'Invalid token' });
}
};
10.8 Protect Routes
// routes/todos.js
const auth = require('../middleware/auth');
router.use(auth);
10.9 JWT 常見錯誤
- ❌ 把 JWT 存在 localStorage(XSS 風險)Do not store JWTs in localStorage (XSS risk)
- ❌ Token 不設過期時間 Do not set an expiration time for tokens
- ❌ 在 JWT 裡放敏感資料 Do not put sensitive data in JWTs
10.10 本章總結
- ✔ 密碼一定要 hash
- ✔ JWT 無狀態、可擴充
- ✔ Auth Middleware 是防線
- ✔ 這已是正式後端架構
11. 使用 ngrok 公開網站 / Expose with ngrok
通常你的 Node.js 網站只能在自己的電腦上瀏覽 (localhost)。如果你想傳網址給朋友看,或是測試外部 API (如 LINE Bot, Stripe) 的 Webhook,你需要 ngrok。
Typically, your Node.js app runs locally (localhost). If you want to share a URL with friends or test external API Webhooks (like LINE Bot, Stripe), you need ngrok.
原理: ngrok 會建立一個安全通道,將公網的請求轉發到你本機的指定 Port。
Concept: ngrok creates a secure tunnel to forward public internet requests to a specific port on your local machine.
步驟 1: 啟動你的 Node.js 伺服器 / Step 1: Start Server
假設你的 Express 伺服器正運行在 3000 port。
Assume your Express server is running on port 3000.
node app.js
# Output: Server running at http://localhost:3000/
步驟 2: 啟動 ngrok / Step 2: Run ngrok
打開另一個終端機視窗,輸入以下指令來建立通道:
Open another terminal window and run the following command to start the tunnel:
# 如果你有安裝 ngrok 軟體
ngrok http 3000
# 或者使用 npx (無需安裝) / Or use npx (no install needed)
npx ngrok http 3000
步驟 3: 獲得網址 / Step 3: Get URL
終端機將會顯示一個隨機生成的網址(例如 https://a1b2.ngrok-free.app)。任何擁有這個網址的人都能看到你的網站!
The terminal will show a randomly generated URL (e.g., https://a1b2.ngrok-free.app). Anyone with this URL can access your site!
Session Status online
Account Your Name (Plan: Free)
Forwarding https://random-name.ngrok-free.app -> http://localhost:3000
Web Interface http://127.0.0.1:4040
12. API Security Hardening(真正可上線的防禦)
12.1 Zero Trust 原則
後端的第一條安全原則是:
❝ 永遠假設請求是惡意的 ❞
不是因為使用者壞,而是因為:
- 請求可以被偽造
- Token 可能被竊取
- API 可能被暴力測試
The first security principle for the backend is:
❝ Always assume the request is malicious ❞
Not because the user is bad, but because:
- Requests can be forged
- Tokens can be stolen
- APIs can be brute-forced
12.2 Rate Limiting
npm install express-rate-limit
```js
// middleware/rateLimit.js
const rateLimit = require('express-rate-limit');
module.exports = rateLimit({
windowMs: 15 * 60 * 1000, // 15 分鐘
max: 100,
message: {
success: false,
error: { message: 'Too many requests' }
}
});
12.3 使用 Helmet
npm install helmet
```js
// app.js
const helmet = require('helmet');
app.use(helmet());
12.4 正確設定 CORS
npm install cors
```js
const cors = require('cors');
app.use(cors({
origin: 'https://your-frontend.com',
methods: ['GET', 'POST', 'PUT', 'DELETE'],
credentials: true
}));
12.5 防止 NoSQL Injection
npm install express-mongo-sanitize
```js
const mongoSanitize = require('express-mongo-sanitize');
app.use(mongoSanitize());
12.6 Error 資訊最小化
if (process.env.NODE_ENV === 'production') {
err.message = 'Server error';
}
原因:
- 錯誤訊息本身就是情報
- Stack trace 會暴露架構
12.7 Middleware 載入順序
helmet
↓
rateLimit
↓
cors
↓
mongoSanitize
↓
routes
↓
errorHandler
12.8 Security Checklist
- ✔ Rate Limit 已開
- ✔ JWT 有 expiry
- ✔ CORS 非全開
- ✔ Error 資訊最小化
- ✔ 沒有 hard-coded secret
12.9 最重要的一句話
功能錯誤會被發現, 安全漏洞會被利用。 Functional errors will be discovered, and security vulnerabilities will be exploited.
13. API 分頁、搜尋、排序(Product-grade API)
13.1 為什麼需要 Pagination?
如果一次回傳 10,000 筆資料:
- 慢
- 浪費頻寬
- 前端難處理
Pagination is mandatory for real-world APIs.
13.2 Query Parameter 設計
GET /api/todos
?page=1
&limit=10
&sort=createdAt
&order=desc
&q=learn
原則:
- 預設值一定要有
- 不存在就忽略
- 不要讓 API 失敗
13.3 Todo 查詢 Controller(Pagination + Search + Sort)
exports.getAll = async (req, res, next) => {
try {
const page = Math.max(parseInt(req.query.page) || 1, 1);
const limit = Math.min(parseInt(req.query.limit) || 10, 100);
const skip = (page - 1) * limit;
const sortField = req.query.sort || 'createdAt';
const sortOrder = req.query.order === 'asc' ? 1 : -1;
const query = {};
if (req.query.q) {
query.title = { $regex: req.query.q, $options: 'i' };
}
const [todos, total] = await Promise.all([
Todo.find(query)
.sort({ [sortField]: sortOrder })
.skip(skip)
.limit(limit),
Todo.countDocuments(query)
]);
res.json({
success: true,
data: todos,
meta: {
page,
limit,
total,
totalPages: Math.ceil(total / limit)
}
});
} catch (err) {
next(err);
}
};
13.4 Response Meta 設計
{
"success": true,
"data": [...],
"meta": {
"page": 1,
"limit": 10,
"total": 42,
"totalPages": 5
}
}
13.5 搜尋效能注意事項 (Search performance considerations)
- Regex 搜尋適合小型專案 Regex。 searches are suitable for small projects.
- 正式產品應使用 Text Index。 Production should use a text index.
- 避免讓使用者自由指定 sort 欄位。 void letting users freely specify the sort field.
13.6 Sort 欄位白名單
const allowedSortFields = ['createdAt', 'title'];
const sortField = allowedSortFields.includes(req.query.sort)
? req.query.sort
: 'createdAt';
13.7 API 使用範例
GET /api/todos?page=2&limit=5&q=node&sort=createdAt&order=desc
13.8 常見錯誤
- ❌ 一次回傳全部資料 (All data returned at once)
- ❌ 沒有 meta 資訊 (No meta information)
- ❌ query 參數未驗證 (Query parameters not validated)
13.9 本章總結
- ✔ Pagination 是必須
- ✔ Search / Sort 提升 UX
- ✔ Meta 是 API 契約
- ✔ 這已是產品級 API