API 和集成
概述
Node.js 在构建 API 和集成外部服务方面表现出色。本章涵盖 REST API 开发、GraphQL 实现、第三方服务集成、身份验证以及 API 设计和使用的最佳实践。
REST API 开发
使用 Express 的基础 REST API
javascript
// rest-api-basic.js
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const app = express();
// 安全和中间件
app.use(helmet());
app.use(cors());
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true }));
// 速率限制
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 分钟
max: 100, // 每个 IP 在 windowMs 内限制 100 个请求
message: {
error: '此 IP 请求过多',
retryAfter: '15 分钟'
}
});
app.use('/api/', limiter);
// 模拟数据库
let users = [
{ id: 1, name: 'John Doe', email: 'john@example.com', createdAt: new Date() },
{ id: 2, name: 'Jane Smith', email: 'jane@example.com', createdAt: new Date() }
];
let nextId = 3;
// 验证中间件
const validateUser = (req, res, next) => {
const { name, email } = req.body;
const errors = [];
if (!name || typeof name !== 'string' || name.trim().length === 0) {
errors.push('名称是必需的,必须是非空字符串');
}
if (!email || typeof email !== 'string' || !email.includes('@')) {
errors.push('需要有效的电子邮件地址');
}
if (errors.length > 0) {
return res.status(400).json({
success: false,
message: '验证失败',
errors
});
}
next();
};
// GET /api/users - 获取所有用户(带分页)
app.get('/api/users', (req, res) => {
const { page = 1, limit = 10, search, sortBy = 'id', sortOrder = 'asc' } = req.query;
let filteredUsers = [...users];
// 搜索功能
if (search) {
const searchLower = search.toLowerCase();
filteredUsers = users.filter(user =>
user.name.toLowerCase().includes(searchLower) ||
user.email.toLowerCase().includes(searchLower)
);
}
// 排序
filteredUsers.sort((a, b) => {
const aValue = a[sortBy];
const bValue = b[sortBy];
if (sortOrder === 'desc') {
return aValue < bValue ? 1 : -1;
}
return aValue > bValue ? 1 : -1;
});
// 分页
const startIndex = (page - 1) * limit;
const endIndex = startIndex + parseInt(limit);
const paginatedUsers = filteredUsers.slice(startIndex, endIndex);
res.json({
success: true,
data: paginatedUsers,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total: filteredUsers.length,
pages: Math.ceil(filteredUsers.length / limit),
hasNext: endIndex < filteredUsers.length,
hasPrev: page > 1
}
});
});
// GET /api/users/:id - 根据 ID 获取用户
app.get('/api/users/:id', (req, res) => {
const id = parseInt(req.params.id);
const user = users.find(u => u.id === id);
if (!user) {
return res.status(404).json({
success: false,
message: '用户未找到'
});
}
res.json({
success: true,
data: user
});
});
// POST /api/users - 创建新用户
app.post('/api/users', validateUser, (req, res) => {
const { name, email } = req.body;
// 检查重复的电子邮件
const existingUser = users.find(u => u.email === email);
if (existingUser) {
return res.status(409).json({
success: false,
message: '使用此电子邮件的用户已存在'
});
}
const newUser = {
id: nextId++,
name: name.trim(),
email: email.trim().toLowerCase(),
createdAt: new Date(),
updatedAt: new Date()
};
users.push(newUser);
res.status(201).json({
success: true,
message: '用户创建成功',
data: newUser
});
});
// PUT /api/users/:id - 更新用户
app.put('/api/users/:id', validateUser, (req, res) => {
const id = parseInt(req.params.id);
const userIndex = users.findIndex(u => u.id === id);
if (userIndex === -1) {
return res.status(404).json({
success: false,
message: '用户未找到'
});
}
const { name, email } = req.body;
// 检查重复的电子邮件(排除当前用户)
const existingUser = users.find(u => u.email === email && u.id !== id);
if (existingUser) {
return res.status(409).json({
success: false,
message: '另一个使用此电子邮件的用户已存在'
});
}
users[userIndex] = {
...users[userIndex],
name: name.trim(),
email: email.trim().toLowerCase(),
updatedAt: new Date()
};
res.json({
success: true,
message: '用户更新成功',
data: users[userIndex]
});
});
// DELETE /api/users/:id - 删除用户
app.delete('/api/users/:id', (req, res) => {
const id = parseInt(req.params.id);
const userIndex = users.findIndex(u => u.id === id);
if (userIndex === -1) {
return res.status(404).json({
success: false,
message: '用户未找到'
});
}
const deletedUser = users.splice(userIndex, 1)[0];
res.json({
success: true,
message: '用户删除成功',
data: deletedUser
});
});
// 健康检查端点
app.get('/health', (req, res) => {
res.json({
status: 'healthy',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
memory: process.memoryUsage()
});
});
// 404 处理器
app.use('*', (req, res) => {
res.status(404).json({
success: false,
message: '端点未找到',
path: req.originalUrl
});
});
// 错误处理器
app.use((error, req, res, next) => {
console.error('API Error:', error);
res.status(error.status || 500).json({
success: false,
message: error.message || '内部服务器错误',
...(process.env.NODE_ENV === 'development' && { stack: error.stack })
});
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`REST API 服务器运行在 http://localhost:${PORT}`);
});高级 REST API 功能
javascript
// rest-api-advanced.js
const express = require('express');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
const multer = require('multer');
const path = require('path');
const app = express();
app.use(express.json());
// JWT 身份验证中间件
const authenticateToken = (req, res, next) => {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return res.status(401).json({ error: '需要访问令牌' });
}
jwt.verify(token, process.env.JWT_SECRET || 'secret', (err, user) => {
if (err) {
return res.status(403).json({ error: '无效或过期的令牌' });
}
req.user = user;
next();
});
};
// 基于角色的授权
const authorize = (roles = []) => {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({ error: '需要身份验证' });
}
if (roles.length && !roles.includes(req.user.role)) {
return res.status(403).json({ error: '权限不足' });
}
next();
};
};
// 文件上传配置
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, 'uploads/');
},
filename: (req, file, cb) => {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
cb(null, file.fieldname + '-' + uniqueSuffix + path.extname(file.originalname));
}
});
const upload = multer({
storage,
limits: {
fileSize: 5 * 1024 * 1024 // 5MB 限制
},
fileFilter: (req, file, cb) => {
const allowedTypes = /jpeg|jpg|png|gif|pdf/;
const extname = allowedTypes.test(path.extname(file.originalname).toLowerCase());
const mimetype = allowedTypes.test(file.mimetype);
if (mimetype && extname) {
return cb(null, true);
} else {
cb(new Error('无效的文件类型'));
}
}
});
// API 版本控制
const v1Router = express.Router();
const v2Router = express.Router();
// 版本 1 端点
v1Router.get('/users', (req, res) => {
res.json({
version: 'v1',
users: [{ id: 1, name: 'John' }]
});
});
// 版本 2 端点(增强的响应格式)
v2Router.get('/users', (req, res) => {
res.json({
version: 'v2',
data: {
users: [{
id: 1,
firstName: 'John',
lastName: 'Doe',
profile: { email: 'john@example.com' }
}],
meta: {
total: 1,
page: 1,
limit: 10
}
}
});
});
app.use('/api/v1', v1Router);
app.use('/api/v2', v2Router);
// 内容协商
app.get('/api/data', (req, res) => {
const data = { message: 'Hello World', timestamp: new Date() };
res.format({
'application/json': () => {
res.json(data);
},
'application/xml': () => {
const xml = `<?xml version="1.0"?>
<response>
<message>${data.message}</message>
<timestamp>${data.timestamp}</timestamp>
</response>`;
res.type('application/xml').send(xml);
},
'text/plain': () => {
res.send(`${data.message} - ${data.timestamp}`);
},
default: () => {
res.status(406).json({ error: '不可接受' });
}
});
});
// 文件上传端点
app.post('/api/upload', authenticateToken, upload.single('file'), (req, res) => {
if (!req.file) {
return res.status(400).json({ error: '未上传文件' });
}
res.json({
success: true,
message: '文件上传成功',
file: {
filename: req.file.filename,
originalName: req.file.originalname,
size: req.file.size,
mimetype: req.file.mimetype,
path: req.file.path
}
});
});
// 批量操作
app.post('/api/users/batch', authenticateToken, authorize(['admin']), (req, res) => {
const { operations } = req.body;
if (!Array.isArray(operations)) {
return res.status(400).json({ error: '操作必须是数组' });
}
const results = operations.map((operation, index) => {
try {
switch (operation.type) {
case 'create':
return { index, success: true, data: { id: Date.now() + index, ...operation.data } };
case 'update':
return { index, success: true, data: { id: operation.id, ...operation.data } };
case 'delete':
return { index, success: true, message: `用户 ${operation.id} 已删除` };
default:
return { index, success: false, error: '未知的操作类型' };
}
} catch (error) {
return { index, success: false, error: error.message };
}
});
res.json({
success: true,
results,
summary: {
total: operations.length,
successful: results.filter(r => r.success).length,
failed: results.filter(r => !r.success).length
}
});
});
// 服务器发送事件 (SSE)
app.get('/api/events', (req, res) => {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'Access-Control-Allow-Origin': '*'
});
// 发送初始事件
res.write(`data: ${JSON.stringify({ type: 'connected', timestamp: new Date() })}\n\n`);
// 发送定期更新
const interval = setInterval(() => {
const event = {
type: 'update',
data: { value: Math.random(), timestamp: new Date() }
};
res.write(`data: ${JSON.stringify(event)}\n\n`);
}, 5000);
// 客户端断开连接时清理
req.on('close', () => {
clearInterval(interval);
res.end();
});
});
app.listen(3000);GraphQL 实现
基础 GraphQL 服务器
javascript
// graphql-server.js
const express = require('express');
const { graphqlHTTP } = require('express-graphql');
const { buildSchema } = require('graphql');
// GraphQL 模式
const schema = buildSchema(`
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
createdAt: String!
}
type Post {
id: ID!
title: String!
content: String!
author: User!
published: Boolean!
createdAt: String!
}
input UserInput {
name: String!
email: String!
}
input PostInput {
title: String!
content: String!
authorId: ID!
published: Boolean = false
}
type Query {
users: [User!]!
user(id: ID!): User
posts: [Post!]!
post(id: ID!): Post
searchUsers(query: String!): [User!]!
}
type Mutation {
createUser(input: UserInput!): User!
updateUser(id: ID!, input: UserInput!): User!
deleteUser(id: ID!): Boolean!
createPost(input: PostInput!): Post!
updatePost(id: ID!, input: PostInput!): Post!
deletePost(id: ID!): Boolean!
}
type Subscription {
userAdded: User!
postAdded: Post!
}
`);
// 模拟数据
let users = [
{ id: '1', name: 'John Doe', email: 'john@example.com', createdAt: new Date().toISOString() },
{ id: '2', name: 'Jane Smith', email: 'jane@example.com', createdAt: new Date().toISOString() }
];
let posts = [
{ id: '1', title: 'First Post', content: 'Hello World', authorId: '1', published: true, createdAt: new Date().toISOString() },
{ id: '2', title: 'Second Post', content: 'GraphQL is awesome', authorId: '2', published: false, createdAt: new Date().toISOString() }
];
let nextUserId = 3;
let nextPostId = 3;
// 解析器
const root = {
// 查询
users: () => users,
user: ({ id }) => users.find(user => user.id === id),
posts: () => posts.map(post => ({
...post,
author: users.find(user => user.id === post.authorId)
})),
post: ({ id }) => {
const post = posts.find(post => post.id === id);
if (post) {
return {
...post,
author: users.find(user => user.id === post.authorId)
};
}
return null;
},
searchUsers: ({ query }) => {
const searchTerm = query.toLowerCase();
return users.filter(user =>
user.name.toLowerCase().includes(searchTerm) ||
user.email.toLowerCase().includes(searchTerm)
);
},
// 变更
createUser: ({ input }) => {
const newUser = {
id: String(nextUserId++),
name: input.name,
email: input.email,
createdAt: new Date().toISOString()
};
users.push(newUser);
return newUser;
},
updateUser: ({ id, input }) => {
const userIndex = users.findIndex(user => user.id === id);
if (userIndex === -1) {
throw new Error('User not found');
}
users[userIndex] = {
...users[userIndex],
...input
};
return users[userIndex];
},
deleteUser: ({ id }) => {
const userIndex = users.findIndex(user => user.id === id);
if (userIndex === -1) {
return false;
}
users.splice(userIndex, 1);
// 同时删除用户的帖子
posts = posts.filter(post => post.authorId !== id);
return true;
},
createPost: ({ input }) => {
const author = users.find(user => user.id === input.authorId);
if (!author) {
throw new Error('Author not found');
}
const newPost = {
id: String(nextPostId++),
title: input.title,
content: input.content,
authorId: input.authorId,
published: input.published,
createdAt: new Date().toISOString()
};
posts.push(newPost);
return {
...newPost,
author
};
},
updatePost: ({ id, input }) => {
const postIndex = posts.findIndex(post => post.id === id);
if (postIndex === -1) {
throw new Error('Post not found');
}
posts[postIndex] = {
...posts[postIndex],
...input
};
const author = users.find(user => user.id === posts[postIndex].authorId);
return {
...posts[postIndex],
author
};
},
deletePost: ({ id }) => {
const postIndex = posts.findIndex(post => post.id === id);
if (postIndex === -1) {
return false;
}
posts.splice(postIndex, 1);
return true;
}
};
// 向 User 类型添加 posts 字段
const originalUserResolver = root.user;
root.user = ({ id }) => {
const user = users.find(user => user.id === id);
if (user) {
return {
...user,
posts: posts.filter(post => post.authorId === id).map(post => ({
...post,
author: user
}))
};
}
return null;
};
const app = express();
app.use('/graphql', graphqlHTTP({
schema: schema,
rootValue: root,
graphiql: true, // 启用 GraphiQL 界面
}));
app.listen(4000, () => {
console.log('GraphQL 服务器运行在 http://localhost:4000/graphql');
});第三方 API 集成
HTTP 客户端封装
javascript
// api-client.js
const axios = require('axios');
class APIClient {
constructor(baseURL, options = {}) {
this.client = axios.create({
baseURL,
timeout: options.timeout || 10000,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
this.retryConfig = {
retries: options.retries || 3,
retryDelay: options.retryDelay || 1000,
retryCondition: options.retryCondition || this.defaultRetryCondition
};
this.setupInterceptors();
}
setupInterceptors() {
// 请求拦截器
this.client.interceptors.request.use(
(config) => {
console.log(`正在发送 ${config.method.toUpperCase()} 请求到 ${config.url}`);
return config;
},
(error) => {
console.error('请求错误:', error);
return Promise.reject(error);
}
);
// 响应拦截器
this.client.interceptors.response.use(
(response) => {
console.log(`收到响应: ${response.status} ${response.statusText}`);
return response;
},
async (error) => {
const originalRequest = error.config;
if (this.shouldRetry(error) && !originalRequest._retry) {
originalRequest._retry = true;
originalRequest._retryCount = (originalRequest._retryCount || 0) + 1;
if (originalRequest._retryCount <= this.retryConfig.retries) {
console.log(`重试请求 (${originalRequest._retryCount}/${this.retryConfig.retries})`);
await this.delay(this.retryConfig.retryDelay * originalRequest._retryCount);
return this.client(originalRequest);
}
}
return Promise.reject(error);
}
);
}
defaultRetryCondition(error) {
return (
error.code === 'ECONNABORTED' ||
error.code === 'ENOTFOUND' ||
error.code === 'ECONNRESET' ||
(error.response && error.response.status >= 500)
);
}
shouldRetry(error) {
return this.retryConfig.retryCondition(error);
}
delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// 身份验证方法
setAuthToken(token) {
this.client.defaults.headers.common['Authorization'] = `Bearer ${token}`;
}
setApiKey(key, headerName = 'X-API-Key') {
this.client.defaults.headers.common[headerName] = key;
}
// HTTP 方法
async get(url, config = {}) {
try {
const response = await this.client.get(url, config);
return response.data;
} catch (error) {
throw this.handleError(error);
}
}
async post(url, data, config = {}) {
try {
const response = await this.client.post(url, data, config);
return response.data;
} catch (error) {
throw this.handleError(error);
}
}
async put(url, data, config = {}) {
try {
const response = await this.client.put(url, data, config);
return response.data;
} catch (error) {
throw this.handleError(error);
}
}
async delete(url, config = {}) {
try {
const response = await this.client.delete(url, config);
return response.data;
} catch (error) {
throw this.handleError(error);
}
}
handleError(error) {
if (error.response) {
// 服务器返回了错误状态
const apiError = new Error(error.response.data?.message || error.message);
apiError.status = error.response.status;
apiError.data = error.response.data;
return apiError;
} else if (error.request) {
// 已发出请求但未收到响应
const networkError = new Error('网络错误: 未收到响应');
networkError.code = 'NETWORK_ERROR';
return networkError;
} else {
// 发生了其他情况
return error;
}
}
}
// 特定服务的 API 客户端
class GitHubAPI extends APIClient {
constructor(token) {
super('https://api.github.com', {
headers: {
'Authorization': `token ${token}`,
'Accept': 'application/vnd.github.v3+json'
}
});
}
async getUser(username) {
return this.get(`/users/${username}`);
}
async getUserRepos(username) {
return this.get(`/users/${username}/repos`);
}
async createRepo(data) {
return this.post('/user/repos', data);
}
}
class StripeAPI extends APIClient {
constructor(secretKey) {
super('https://api.stripe.com/v1', {
headers: {
'Authorization': `Bearer ${secretKey}`
}
});
}
async createCustomer(data) {
return this.post('/customers', data);
}
async createPaymentIntent(data) {
return this.post('/payment_intents', data);
}
async getCustomer(customerId) {
return this.get(`/customers/${customerId}`);
}
}
// 使用示例
async function demonstrateAPIIntegration() {
// GitHub API 示例
const github = new GitHubAPI(process.env.GITHUB_TOKEN);
try {
const user = await github.getUser('octocat');
console.log('GitHub 用户:', user.name);
const repos = await github.getUserRepos('octocat');
console.log('仓库数量:', repos.length);
} catch (error) {
console.error('GitHub API 错误:', error.message);
}
// 通用 API 客户端示例
const jsonPlaceholder = new APIClient('https://jsonplaceholder.typicode.com');
try {
const posts = await jsonPlaceholder.get('/posts');
console.log('帖子数量:', posts.length);
const newPost = await jsonPlaceholder.post('/posts', {
title: '测试帖子',
body: '这是一个测试帖子',
userId: 1
});
console.log('创建的帖子:', newPost.id);
} catch (error) {
console.error('JSONPlaceholder API 错误:', error.message);
}
}
module.exports = { APIClient, GitHubAPI, StripeAPI };Webhook 处理
javascript
// webhook-handler.js
const express = require('express');
const crypto = require('crypto');
const bodyParser = require('body-parser');
class WebhookHandler {
constructor() {
this.handlers = new Map();
this.middleware = [];
}
// 注册 webhook 处理器
register(event, handler) {
if (!this.handlers.has(event)) {
this.handlers.set(event, []);
}
this.handlers.get(event).push(handler);
}
// 添加中间件
use(middleware) {
this.middleware.push(middleware);
}
// 验证 webhook 签名
verifySignature(payload, signature, secret, algorithm = 'sha256') {
const expectedSignature = crypto
.createHmac(algorithm, secret)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
// 处理 webhook
async process(event, payload, headers = {}) {
// 运行中间件
for (const middleware of this.middleware) {
await middleware(event, payload, headers);
}
// 获取事件处理器
const handlers = this.handlers.get(event) || [];
// 执行所有处理器
const results = await Promise.allSettled(
handlers.map(handler => handler(payload, headers))
);
// 记录任何失败
results.forEach((result, index) => {
if (result.status === 'rejected') {
console.error(`事件 ${event} 的处理器 ${index} 失败:`, result.reason);
}
});
return results;
}
// 创建 Express 中间件
createExpressHandler(options = {}) {
const {
path = '/webhook',
secret,
signatureHeader = 'x-signature',
eventHeader = 'x-event-type'
} = options;
const router = express.Router();
// 用于签名验证的原始 body 解析器
router.use(path, bodyParser.raw({ type: 'application/json' }));
router.post(path, async (req, res) => {
try {
const payload = req.body;
const signature = req.headers[signatureHeader];
const event = req.headers[eventHeader];
if (!event) {
return res.status(400).json({ error: '缺少事件类型头' });
}
// 如果提供了密钥,则验证签名
if (secret && signature) {
if (!this.verifySignature(payload, signature, secret)) {
return res.status(401).json({ error: '无效的签名' });
}
}
// 解析 JSON 负载
const parsedPayload = JSON.parse(payload.toString());
// 处理 webhook
await this.process(event, parsedPayload, req.headers);
res.status(200).json({ success: true, message: 'Webhook 已处理' });
} catch (error) {
console.error('Webhook 处理错误:', error);
res.status(500).json({ error: 'Webhook 处理失败' });
}
});
return router;
}
}
// 使用示例
const webhookHandler = new WebhookHandler();
// 添加日志中间件
webhookHandler.use(async (event, payload, headers) => {
console.log(`收到 webhook: ${event}`, {
timestamp: new Date().toISOString(),
payloadSize: JSON.stringify(payload).length,
userAgent: headers['user-agent']
});
});
// 注册事件处理器
webhookHandler.register('user.created', async (payload) => {
console.log('新用户已创建:', payload.user.email);
// 发送欢迎邮件
// 更新分析数据
});
webhookHandler.register('payment.completed', async (payload) => {
console.log('支付已完成:', payload.payment.id);
// 更新订单状态
// 发送确认邮件
});
webhookHandler.register('order.shipped', async (payload) => {
console.log('订单已发货:', payload.order.id);
// 发送跟踪信息
// 更新库存
});
// 创建 Express 应用
const app = express();
// 挂载 webhook 处理器
app.use(webhookHandler.createExpressHandler({
path: '/webhooks',
secret: process.env.WEBHOOK_SECRET,
signatureHeader: 'x-hub-signature-256',
eventHeader: 'x-github-event'
}));
app.listen(3000, () => {
console.log('Webhook 服务器运行在 http://localhost:3000');
});
module.exports = WebhookHandler;下一步
在下一章中,我们将探讨错误处理策略和构建健壮的 Node.js 应用程序的最佳实践。
练习
- 构建一个包含身份验证、授权和文件上传的完整 REST API
- 使用 WebSockets 创建一个带有订阅功能的 GraphQL API
- 实现一个用于处理 GitHub 事件的 webhook 系统
- 构建一个聚合多个微服务的 API 网关
要点
- REST API 提供了一种标准化的方式来暴露应用程序功能
- GraphQL 提供灵活的数据查询和实时订阅
- 适当的身份验证和授权对 API 安全至关重要
- HTTP 客户端封装简化了第三方 API 集成
- Webhook 处理器支持实时事件处理
- API 版本控制确保向后兼容性
- 速率限制和验证防止滥用
- 全面的错误处理提高了 API 可靠性