今天是詞窮的日子,所以沒有前言。但總之就是整理一下 RESTful API 是什麼,還有整理一些範例。
REST(Representational State Transfer)是一種軟體架構風格,用來設計網路服務。RESTful API 就是遵循 REST 原則的 API 設計。
想像一下,API 就像餐廳的菜單和服務流程。RESTful API 就是一套標準化的規則,讓客戶(前端)和廚房(後端)能夠用統一的方式溝通。
更詳細可以參考wiki
// 用途:讀取資料,不會修改伺服器狀態
// 特性:安全的、可快取的、冪等的
const http = require('node:http');
const url = require('node:url');
function handleGetRequest(req, res, pathname) {
res.setHeader('Content-Type', 'application/json');
if (pathname === '/users') {
// 取得所有用戶
const users = [
{ id: 1, name: '張三', email: 'zhang@example.com' },
{ id: 2, name: '李四', email: 'li@example.com' }
];
res.writeHead(200);
res.end(JSON.stringify(users));
}
else if (pathname.startsWith('/users/')) {
// 取得單一用戶
const userId = pathname.split('/')[2];
const user = { id: userId, name: '張三', email: 'zhang@example.com' };
res.writeHead(200);
res.end(JSON.stringify(user));
}
else {
res.writeHead(404);
res.end(JSON.stringify({ error: '找不到資源' }));
}
}
// 用途:創建新資源
// 特性:不安全的、不可快取的、非冪等的
function handlePostRequest(req, res, pathname) {
if (pathname === '/users') {
let body = '';
req.on('data', chunk => {
body += chunk.toString();
});
req.on('end', () => {
try {
const userData = JSON.parse(body);
// 基本驗證
if (!userData.name || !userData.email) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
error: '缺少必要欄位',
required: ['name', 'email']
}));
return;
}
const newUser = {
id: Date.now(),
...userData,
createdAt: new Date().toISOString()
};
res.writeHead(201, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
message: '用戶創建成功',
user: newUser
}));
} catch (err) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'JSON 格式錯誤' }));
}
});
} else {
res.writeHead(405, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: '方法不允許' }));
}
}
// 用途:完整更新資源
// 特性:不安全的、不可快取的、冪等的
function handlePutRequest(req, res, pathname) {
const pathParts = pathname.split('/');
if (pathParts[1] === 'users' && pathParts[2]) {
const userId = pathParts[2];
let body = '';
req.on('data', chunk => {
body += chunk.toString();
});
req.on('end', () => {
try {
const userData = JSON.parse(body);
// PUT 要求完整的資源資料
if (!userData.name || !userData.email) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
error: 'PUT 需要完整的資源資料',
required: ['name', 'email']
}));
return;
}
// 更新用戶(完整替換)
const updatedUser = {
id: parseInt(userId),
name: userData.name,
email: userData.email,
updatedAt: new Date().toISOString()
};
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
message: '用戶更新成功',
user: updatedUser
}));
} catch (err) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'JSON 格式錯誤' }));
}
});
} else {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: '找不到資源' }));
}
}
// 用途:部分更新資源
// 特性:不安全的、不可快取的、非冪等的
function handlePatchRequest(req, res, pathname) {
const pathParts = pathname.split('/');
if (pathParts[1] === 'users' && pathParts[2]) {
const userId = pathParts[2];
let body = '';
req.on('data', chunk => {
body += chunk.toString();
});
req.on('end', () => {
try {
const updates = JSON.parse(body);
// 模擬從資料庫取得現有用戶
const existingUser = {
id: parseInt(userId),
name: '張三',
email: 'zhang@example.com'
};
// 只更新提供的欄位
const updatedUser = {
...existingUser,
...updates,
updatedAt: new Date().toISOString()
};
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
message: '用戶部分更新成功',
user: updatedUser
}));
} catch (err) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'JSON 格式錯誤' }));
}
});
} else {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: '找不到資源' }));
}
}
// 用途:刪除資源
// 特性:不安全的、不可快取的、冪等的
function handleDeleteRequest(req, res, pathname) {
const pathParts = pathname.split('/');
if (pathParts[1] === 'users' && pathParts[2]) {
const userId = pathParts[2];
// 檢查用戶是否存在
if (!userId || isNaN(userId)) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: '無效的用戶 ID' }));
return;
}
// 模擬刪除操作
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
message: `用戶 ${userId} 已被刪除`
}));
} else {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: '找不到資源' }));
}
}
好的 RESTful API 需要清楚、一致的 URL 設計:
// 好的 URL 設計範例
const goodUrls = {
// 使用複數名詞表示集合
getAllUsers: 'GET /users',
getUser: 'GET /users/123',
createUser: 'POST /users',
updateUser: 'PUT /users/123',
partialUpdateUser: 'PATCH /users/123',
deleteUser: 'DELETE /users/123',
// 巢狀資源
getUserPosts: 'GET /users/123/posts',
getUserPost: 'GET /users/123/posts/456',
// 查詢參數用於過濾和分頁
searchUsers: 'GET /users?name=john&page=1&limit=10',
// 動作型端點(少用)
activateUser: 'POST /users/123/activate'
};
// 避免的 URL 設計
const badUrls = {
// 避免動詞
getUsers: 'GET /getUsers', // 錯誤
createUser: 'POST /createUser', // 錯誤
// 避免混合大小寫
getUsers: 'GET /Users', // 錯誤,應該用小寫
// 避免在 URL 中包含檔案副檔名
getUser: 'GET /users/123.json' // 錯誤
};
const url = require('node:url');
class UrlParser {
static parseRestfulUrl(urlString) {
const parsedUrl = url.parse(urlString, true);
const pathParts = parsedUrl.pathname.split('/').filter(part => part !== '');
return {
resource: pathParts[0], // 'users'
id: pathParts[1] || null, // '123' or null
subResource: pathParts[2] || null, // 'posts' or null
subId: pathParts[3] || null, // '456' or null
query: parsedUrl.query // { page: '1', limit: '10' }
};
}
static isValidResourceId(id) {
return id && !isNaN(id) && parseInt(id) > 0;
}
}
// 使用範例
function routeRequest(req, res) {
const parsed = UrlParser.parseRestfulUrl(req.url);
console.log('解析結果:', parsed);
// /users/123/posts?page=1
// { resource: 'users', id: '123', subResource: 'posts', subId: null, query: { page: '1' } }
if (parsed.resource === 'users') {
if (!parsed.id) {
// /users - 處理用戶集合
handleUserCollection(req, res, parsed.query);
} else if (UrlParser.isValidResourceId(parsed.id)) {
// /users/123 - 處理單一用戶
handleSingleUser(req, res, parsed.id, parsed.subResource);
} else {
res.writeHead(400);
res.end(JSON.stringify({ error: '無效的用戶 ID' }));
}
}
}
讓我們建立一個完整的用戶管理 API:
// 簡單的記憶體資料庫模擬
class UserDatabase {
constructor() {
this.users = [
{ id: 1, name: '張三', email: 'zhang@example.com', createdAt: '2024-01-01T00:00:00Z' },
{ id: 2, name: '李四', email: 'li@example.com', createdAt: '2024-01-02T00:00:00Z' }
];
this.nextId = 3;
}
// 取得所有用戶
findAll(filters = {}) {
let result = [...this.users];
// 名稱過濾
if (filters.name) {
result = result.filter(user =>
user.name.toLowerCase().includes(filters.name.toLowerCase())
);
}
// 分頁
if (filters.page && filters.limit) {
const page = parseInt(filters.page);
const limit = parseInt(filters.limit);
const start = (page - 1) * limit;
result = result.slice(start, start + limit);
}
return result;
}
// 根據 ID 查找用戶
findById(id) {
return this.users.find(user => user.id === parseInt(id));
}
// 根據 email 查找用戶
findByEmail(email) {
return this.users.find(user => user.email === email);
}
// 創建用戶
create(userData) {
const newUser = {
id: this.nextId++,
...userData,
createdAt: new Date().toISOString()
};
this.users.push(newUser);
return newUser;
}
// 更新用戶
update(id, userData) {
const index = this.users.findIndex(user => user.id === parseInt(id));
if (index === -1) {
return null;
}
this.users[index] = {
...this.users[index],
...userData,
updatedAt: new Date().toISOString()
};
return this.users[index];
}
// 刪除用戶
delete(id) {
const index = this.users.findIndex(user => user.id === parseInt(id));
if (index === -1) {
return false;
}
this.users.splice(index, 1);
return true;
}
// 取得總數量
count(filters = {}) {
return this.findAll(filters).length;
}
}
class UserController {
constructor() {
this.db = new UserDatabase();
}
// GET /users
async getUsers(req, res, query) {
try {
const filters = {
name: query.name,
page: query.page,
limit: Math.min(parseInt(query.limit) || 10, 100) // 限制最大值
};
const users = this.db.findAll(filters);
const total = this.db.count({ name: query.name });
const response = {
users,
pagination: {
total,
page: parseInt(filters.page) || 1,
limit: filters.limit,
totalPages: Math.ceil(total / filters.limit)
}
};
ApiResponse.success(res, response);
} catch (err) {
console.error('取得用戶列表錯誤:', err);
ServerError.internal(res);
}
}
// GET /users/:id
async getUser(req, res, userId) {
try {
const user = this.db.findById(userId);
if (!user) {
ApiError.notFound(res, '找不到指定的用戶');
return;
}
ApiResponse.success(res, user);
} catch (err) {
console.error('取得用戶錯誤:', err);
ServerError.internal(res);
}
}
// POST /users
async createUser(req, res, userData) {
try {
// 驗證資料
const errors = this.validateUserData(userData);
if (errors.length > 0) {
ApiError.validationError(res, errors);
return;
}
// 檢查 email 是否已存在
if (this.db.findByEmail(userData.email)) {
ApiError.conflict(res, '此 email 已被使用');
return;
}
const newUser = this.db.create(userData);
ApiResponse.created(res, newUser);
} catch (err) {
console.error('創建用戶錯誤:', err);
ServerError.internal(res);
}
}
// PUT /users/:id
async updateUser(req, res, userId, userData) {
try {
// 檢查用戶是否存在
if (!this.db.findById(userId)) {
ApiError.notFound(res, '找不到指定的用戶');
return;
}
// PUT 需要完整資料
if (!userData.name || !userData.email) {
ApiError.badRequest(res, 'PUT 請求需要提供完整的用戶資料', {
required: ['name', 'email']
});
return;
}
// 驗證資料
const errors = this.validateUserData(userData);
if (errors.length > 0) {
ApiError.validationError(res, errors);
return;
}
// 檢查 email 衝突(排除自己)
const existingUser = this.db.findByEmail(userData.email);
if (existingUser && existingUser.id !== parseInt(userId)) {
ApiError.conflict(res, '此 email 已被其他用戶使用');
return;
}
const updatedUser = this.db.update(userId, userData);
ApiResponse.success(res, updatedUser, '用戶更新成功');
} catch (err) {
console.error('更新用戶錯誤:', err);
ServerError.internal(res);
}
}
// PATCH /users/:id
async patchUser(req, res, userId, updates) {
try {
// 檢查用戶是否存在
if (!this.db.findById(userId)) {
ApiError.notFound(res, '找不到指定的用戶');
return;
}
// 驗證提供的欄位
const errors = this.validatePartialUserData(updates);
if (errors.length > 0) {
ApiError.validationError(res, errors);
return;
}
// 檢查 email 衝突(如果有提供 email)
if (updates.email) {
const existingUser = this.db.findByEmail(updates.email);
if (existingUser && existingUser.id !== parseInt(userId)) {
ApiError.conflict(res, '此 email 已被其他用戶使用');
return;
}
}
const updatedUser = this.db.update(userId, updates);
ApiResponse.success(res, updatedUser, '用戶部分更新成功');
} catch (err) {
console.error('部分更新用戶錯誤:', err);
ServerError.internal(res);
}
}
// DELETE /users/:id
async deleteUser(req, res, userId) {
try {
const success = this.db.delete(userId);
if (!success) {
ApiError.notFound(res, '找不到指定的用戶');
return;
}
ApiResponse.noContent(res);
} catch (err) {
console.error('刪除用戶錯誤:', err);
ServerError.internal(res);
}
}
// 驗證完整用戶資料
validateUserData(userData) {
const errors = [];
if (!userData.name || typeof userData.name !== 'string') {
errors.push({ field: 'name', message: '姓名為必填且必須是字串' });
} else if (userData.name.length < 2 || userData.name.length > 50) {
errors.push({ field: 'name', message: '姓名長度必須在 2-50 字元之間' });
}
if (!userData.email || typeof userData.email !== 'string') {
errors.push({ field: 'email', message: 'Email 為必填且必須是字串' });
} else if (!this.isValidEmail(userData.email)) {
errors.push({ field: 'email', message: '請提供有效的 email 格式' });
}
return errors;
}
// 驗證部分用戶資料(PATCH 用)
validatePartialUserData(updates) {
const errors = [];
if (updates.name !== undefined) {
if (typeof updates.name !== 'string' || updates.name.length < 2 || updates.name.length > 50) {
errors.push({ field: 'name', message: '姓名長度必須在 2-50 字元之間' });
}
}
if (updates.email !== undefined) {
if (typeof updates.email !== 'string' || !this.isValidEmail(updates.email)) {
errors.push({ field: 'email', message: '請提供有效的 email 格式' });
}
}
return errors;
}
isValidEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
}
把所有功能整合成完整的 API:
const http = require('node:http');
const url = require('node:url');
class RestfulApiServer {
constructor() {
this.userController = new UserController();
}
createServer() {
return http.createServer((req, res) => {
this.handleRequest(req, res).catch(err => {
console.error('請求處理錯誤:', err);
ServerError.internal(res);
});
});
}
async handleRequest(req, res) {
// 設定 CORS
this.setCorsHeaders(res);
// 處理 OPTIONS 請求
if (req.method === 'OPTIONS') {
res.writeHead(200);
res.end();
return;
}
// 解析 URL
const parsedUrl = url.parse(req.url, true);
const pathname = parsedUrl.pathname;
const query = parsedUrl.query;
const method = req.method;
console.log(`${method} ${pathname}`);
// 路由分發
if (pathname === '/api/users') {
await this.handleUserCollection(req, res, method, query);
}
else if (pathname.match(/^\/api\/users\/\d+$/)) {
const userId = pathname.split('/')[3];
await this.handleSingleUser(req, res, method, userId);
}
else if (pathname === '/api/health') {
this.handleHealthCheck(res);
}
else {
ApiError.notFound(res, 'API 端點不存在');
}
}
async handleUserCollection(req, res, method, query) {
switch (method) {
case 'GET':
await this.userController.getUsers(req, res, query);
break;
case 'POST':
const userData = await this.parseRequestBody(req);
await this.userController.createUser(req, res, userData);
break;
default:
ApiError.badRequest(res, `${method} 方法不支援此端點`);
}
}
async handleSingleUser(req, res, method, userId) {
switch (method) {
case 'GET':
await this.userController.getUser(req, res, userId);
break;
case 'PUT':
const putData = await this.parseRequestBody(req);
await this.userController.updateUser(req, res, userId, putData);
break;
case 'PATCH':
const patchData = await this.parseRequestBody(req);
await this.userController.patchUser(req, res, userId, patchData);
break;
case 'DELETE':
await this.userController.deleteUser(req, res, userId);
break;
default:
ApiError.badRequest(res, `${method} 方法不支援此端點`);
}
}
handleHealthCheck(res) {
ApiResponse.success(res, {
status: 'healthy',
timestamp: new Date().toISOString(),
version: '1.0.0'
});
}
parseRequestBody(req) {
return new Promise((resolve, reject) => {
let body = '';
req.on('data', chunk => {
body += chunk.toString();
// 防止過大的請求
if (body.length > 1000000) { // 1MB
reject(new Error('請求內容過大'));
}
});
req.on('end', () => {
try {
if (body.trim() === '') {
resolve({});
} else {
resolve(JSON.parse(body));
}
} catch (err) {
reject(new Error('JSON 格式錯誤'));
}
});
req.on('error', reject);
});
}
setCorsHeaders(res) {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
}
start(port = 3000) {
const server = this.createServer();
server.listen(port, () => {
console.log(`RESTful API 伺服器啟動在 http://localhost:${port}`);
console.log('可用端點:');
console.log(' GET /api/users - 取得用戶列表');
console.log(' POST /api/users - 創建新用戶');
console.log(' GET /api/users/:id - 取得單一用戶');
console.log(' PUT /api/users/:id - 完整更新用戶');
console.log(' PATCH /api/users/:id - 部分更新用戶');
console.log(' DELETE /api/users/:id - 刪除用戶');
console.log(' GET /api/health - 健康檢查');
});
return server;
}
}
// 啟動伺服器
const apiServer = new RestfulApiServer();
apiServer.start(3000);
class VersionedApiServer extends RestfulApiServer {
constructor() {
super();
this.v1Controller = new UserController();
this.v2Controller = new UserV2Controller(); // 假設有 v2 版本
}
async handleRequest(req, res) {
const parsedUrl = url.parse(req.url, true);
const pathParts = parsedUrl.pathname.split('/');
// 解析版本號
if (pathParts[1] === 'api' && pathParts[2] && pathParts[2].startsWith('v')) {
const version = pathParts[2];
const resource = pathParts[3];
const id = pathParts[4];
console.log(`${req.method} ${version}/${resource}${id ? '/' + id : ''}`);
if (version === 'v1') {
await this.handleV1Request(req, res, resource, id, parsedUrl.query);
} else if (version === 'v2') {
await this.handleV2Request(req, res, resource, id, parsedUrl.query);
} else {
ApiError.badRequest(res, '不支援的 API 版本');
}
} else {
// 沒有版本號,預設使用最新版本或回傳錯誤
ApiError.badRequest(res, '請在 URL 中指定 API 版本,例如: /api/v1/users');
}
}
async handleV1Request(req, res, resource, id, query) {
// 使用 v1 控制器處理請求
if (resource === 'users') {
if (id) {
await this.handleSingleUserV1(req, res, req.method, id);
} else {
await this.handleUserCollectionV1(req, res, req.method, query);
}
}
}
// v1 版本的回應格式
async handleUserCollectionV1(req, res, method, query) {
if (method === 'GET') {
const users = this.v1Controller.db.findAll(query);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
success: true,
data: users
}));
}
// 其他方法...
}
}
// 成功回應
{
"status": "success",
"message": "操作成功",
"data": { /* 實際資料 */ }
}
// 錯誤回應
{
"status": "error",
"message": "錯誤描述",
"errors": [ /* 詳細錯誤資訊 */ ]
}
這個部分後續整理註冊登入功能會再複習一次,明天可以開始 Express 系列了。
淺談 API 與 RESTful API
表現層狀態轉換
HTTP 請求方法
HTTP狀態碼