阶段五:实战项目
恭喜你来到最后一课!本课将前面24课的所有知识融合成一个完整的API平台——从项目结构到生产部署,从RESTful API到GraphQL端点,从认证授权到监控告警。这是一个可以上线运行的真实项目模板。
api-platform/
├── src/
│ ├── index.js # 入口
│ ├── config/
│ │ ├── index.js # 配置管理
│ │ └── database.js # 数据库配置
│ ├── middleware/
│ │ ├── auth.js # JWT认证
│ │ ├── rateLimit.js # 限流
│ │ ├── validate.js # 输入验证
│ │ ├── errorHandler.js # 错误处理
│ │ └── cache.js # 缓存
│ ├── routes/
│ │ ├── auth.js # 认证路由
│ │ ├── users.js # 用户路由
│ │ ├── products.js # 商品路由
│ │ └── orders.js # 订单路由
│ ├── graphql/
│ │ ├── schema.js # GraphQL Schema
│ │ └── resolvers.js # Resolvers
│ ├── services/
│ │ ├── userService.js # 业务逻辑
│ │ ├── productService.js
│ │ └── orderService.js
│ └── utils/
│ ├── logger.js # 日志
│ └── errors.js # 自定义错误
├── docs/
│ └── openapi.yaml # API文档
├── proto/
│ └── order.proto # gRPC定义
├── tests/
│ ├── auth.test.js
│ ├── products.test.js
│ └── orders.test.js
├── docker-compose.yml
├── Dockerfile
└── package.json
// platform.js - 完整API平台(单文件版)
const express = require('express');
const crypto = require('crypto');
const { createHandler } = require('graphql-http/lib/use/express');
const { buildSchema } = require('graphql');
const app = express();
// ===== 配置 =====
const config = {
port: process.env.PORT || 3000,
jwtSecret: process.env.JWT_SECRET || 'dev-secret-change-in-production',
jwtExpiry: '15m',
refreshTokenExpiry: '7d',
rateLimitWindow: 60000,
rateLimitMax: 200,
cacheDefaultTTL: 60,
};
// ===== 工具函数 =====
const hashPassword = (pwd) => crypto.createHash('sha256').update(pwd + config.jwtSecret).digest('hex');
const createJWT = (payload) => {
const header = Buffer.from(JSON.stringify({ alg: 'HS256', typ: 'JWT' })).toString('base64url');
const body = Buffer.from(JSON.stringify({ ...payload, iat: Math.floor(Date.now() / 1000) })).toString('base64url');
const sig = crypto.createHmac('sha256', config.jwtSecret).update(`${header}.${body}`).digest('base64url');
return `${header}.${body}.${sig}`;
};
const verifyJWT = (token) => {
const [h, b, s] = token.split('.');
const expected = crypto.createHmac('sha256', config.jwtSecret).update(`${h}.${b}`).digest('base64url');
if (s !== expected) throw new Error('Invalid signature');
const payload = JSON.parse(Buffer.from(b, 'base64').toString());
if (payload.exp < Date.now() / 1000) throw new Error('Token expired');
return payload;
};
// ===== LRU缓存 =====
class LRUCache {
constructor(max = 500, defaultTTL = 60) { this.max = max; this.defaultTTL = defaultTTL; this.cache = new Map(); }
get(key) { const e = this.cache.get(key); if (!e || Date.now() > e.exp) { this.cache.delete(key); return null; } this.cache.delete(key); this.cache.set(key, e); return e.data; }
set(key, data, ttl) { if (this.cache.size >= this.max) { const k = this.cache.keys().next().value; this.cache.delete(k); } this.cache.set(key, { data, exp: Date.now() + (ttl || this.defaultTTL) * 1000 }); }
invalidate(prefix) { for (const k of [...this.cache.keys()]) if (k.startsWith(prefix)) this.cache.delete(k); }
stats() { return { size: this.cache.size, max: this.max }; }
}
const cache = new LRUCache();
// ===== 数据层 =====
const db = {
users: new Map([
['1', { id: '1', name: '管理员', email: 'admin@example.com', password: hashPassword('admin123'), role: 'admin', createdAt: '2025-01-01' }],
['2', { id: '2', name: '张三', email: 'zhang@example.com', password: hashPassword('user123'), role: 'user', createdAt: '2025-01-02' }],
]),
products: new Map([
['1', { id: '1', name: '机械键盘', price: 599, category: '外设', stock: 120, status: 'active', createdAt: '2025-01-05' }],
['2', { id: '2', name: '无线鼠标', price: 199, category: '外设', stock: 250, status: 'active', createdAt: '2025-01-06' }],
['3', { id: '3', name: '4K显示器', price: 2999, category: '显示器', stock: 45, status: 'active', createdAt: '2025-01-07' }],
['4', { id: '4', name: '降噪耳机', price: 1299, category: '音频', stock: 60, status: 'active', createdAt: '2025-01-08' }],
]),
orders: new Map([
['1', { id: '1', userId: '2', items: [{ productId: '1', quantity: 1, price: 599 }, { productId: '2', quantity: 2, price: 199 }], total: 997, status: 'completed', createdAt: '2025-01-10' }],
]),
nextUserId: 3, nextProductId: 5, nextOrderId: 2,
};
// ===== 中间件 =====
app.use(express.json({ limit: '100kb' }));
// CORS
app.use((req, res, next) => {
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');
if (req.method === 'OPTIONS') return res.sendStatus(204);
next();
});
// 请求ID
app.use((req, res, next) => {
req.requestId = req.headers['x-request-id'] || crypto.randomUUID();
res.setHeader('X-Request-ID', req.requestId);
next();
});
// 日志
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
console.log(`${req.method} ${req.path} ${res.statusCode} ${Date.now() - start}ms [${req.requestId}]`);
});
next();
});
// 限流
const rateCounters = new Map();
app.use((req, res, next) => {
if (req.path === '/health' || req.path === '/api/auth/login') return next();
const key = req.user?.sub || req.ip;
const now = Date.now();
let counter = rateCounters.get(key);
if (!counter || now - counter.windowStart > config.rateLimitWindow) {
counter = { count: 0, windowStart: now };
rateCounters.set(key, counter);
}
counter.count++;
res.setHeader('X-RateLimit-Limit', String(config.rateLimitMax));
res.setHeader('X-RateLimit-Remaining', String(Math.max(0, config.rateLimitMax - counter.count)));
if (counter.count > config.rateLimitMax) {
return res.status(429).json({ success: false, error: { code: 'RATE_LIMIT', message: '请求频率超限' } });
}
next();
});
// 认证
function auth(req, res, next) {
const token = req.headers.authorization?.substring(7);
if (!token) return res.status(401).json({ success: false, error: { code: 'AUTH_MISSING' } });
try {
req.user = verifyJWT(token);
next();
} catch (e) {
return res.status(401).json({ success: false, error: { code: 'AUTH_INVALID', message: e.message } });
}
}
// ===== REST路由 =====
// 认证
app.post('/api/auth/login', (req, res) => {
const { email, password } = req.body;
const user = [...db.users.values()].find(u => u.email === email);
if (!user || user.password !== hashPassword(password)) {
return res.status(401).json({ success: false, error: { code: 'INVALID_CREDENTIALS' } });
}
const token = createJWT({ sub: user.id, role: user.role, exp: Math.floor(Date.now() / 1000) + 900 });
const refreshToken = createJWT({ sub: user.id, type: 'refresh', exp: Math.floor(Date.now() / 1000) + 7 * 86400 });
res.json({ success: true, data: { token, refreshToken, expiresIn: 900 } });
});
app.post('/api/auth/register', (req, res) => {
const { name, email, password } = req.body;
if (!name || !email || !password) return res.status(400).json({ success: false, error: { code: 'VALIDATION_ERROR', message: 'name, email, password必填' } });
if ([...db.users.values()].some(u => u.email === email)) return res.status(409).json({ success: false, error: { code: 'CONFLICT', message: '邮箱已注册' } });
const id = String(db.nextUserId++);
const user = { id, name, email, password: hashPassword(password), role: 'user', createdAt: new Date().toISOString() };
db.users.set(id, user);
const token = createJWT({ sub: id, role: 'user', exp: Math.floor(Date.now() / 1000) + 900 });
res.status(201).json({ success: true, data: { id, name, email, role: user.role, token } });
});
// 商品
app.get('/api/products', (req, res) => {
const cacheKey = `products:${req.originalUrl}`;
const cached = cache.get(cacheKey);
if (cached) { res.setHeader('X-Cache', 'HIT'); return res.json(cached); }
res.setHeader('X-Cache', 'MISS');
const { page = 1, limit = 20, category, status } = req.query;
let data = [...db.products.values()];
if (category) data = data.filter(p => p.category === category);
if (status) data = data.filter(p => p.status === status);
const total = data.length;
const p = Math.max(1, parseInt(page));
const l = Math.min(100, parseInt(limit));
data = data.slice((p - 1) * l, p * l);
const response = { success: true, data, pagination: { page: p, limit: l, total, totalPages: Math.ceil(total / l) } };
cache.set(cacheKey, response, 30);
res.json(response);
});
app.get('/api/products/:id', (req, res) => {
const product = db.products.get(req.params.id);
if (!product) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND' } });
res.json({ success: true, data: product });
});
app.post('/api/products', auth, (req, res) => {
if (req.user.role !== 'admin') return res.status(403).json({ success: false, error: { code: 'FORBIDDEN' } });
const { name, price, category, stock } = req.body;
if (!name || price === undefined) return res.status(400).json({ success: false, error: { code: 'VALIDATION_ERROR' } });
const id = String(db.nextProductId++);
const product = { id, name, price, category: category || '未分类', stock: stock || 0, status: 'active', createdAt: new Date().toISOString() };
db.products.set(id, product);
cache.invalidate('products:');
res.status(201).json({ success: true, data: product });
});
// 订单
app.post('/api/orders', auth, async (req, res) => {
const { items, shippingAddress } = req.body;
if (!items?.length) return res.status(400).json({ success: false, error: { code: 'VALIDATION_ERROR' } });
// 验证商品和计算总价
let total = 0;
for (const item of items) {
const product = db.products.get(item.productId);
if (!product) return res.status(400).json({ success: false, error: { code: 'PRODUCT_NOT_FOUND', message: `商品 ${item.productId} 不存在` } });
if (product.stock < item.quantity) return res.status(400).json({ success: false, error: { code: 'INSUFFICIENT_STOCK', message: `${product.name} 库存不足` } });
total += product.price * item.quantity;
item.price = product.price;
}
const id = String(db.nextOrderId++);
const order = { id, userId: req.user.sub, items, total, shippingAddress, status: 'pending', createdAt: new Date().toISOString() };
db.orders.set(id, order);
// 扣减库存
items.forEach(item => {
const product = db.products.get(item.productId);
if (product) product.stock -= item.quantity;
});
cache.invalidate('products:');
res.status(201).json({ success: true, data: order });
});
app.get('/api/orders', auth, (req, res) => {
const orders = [...db.orders.values()].filter(o => req.user.role === 'admin' || o.userId === req.user.sub);
res.json({ success: true, data: orders });
});
app.get('/api/orders/:id', auth, (req, res) => {
const order = db.orders.get(req.params.id);
if (!order) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND' } });
if (order.userId !== req.user.sub && req.user.role !== 'admin') return res.status(403).json({ success: false, error: { code: 'FORBIDDEN' } });
res.json({ success: true, data: order });
});
// ===== GraphQL端点 =====
const graphqlSchema = buildSchema(`
type Query {
me: User!
products(category: String, first: Int = 20): [Product!]!
product(id: ID!): Product
myOrders: [Order!]!
}
type Mutation {
createOrder(items: [OrderItemInput!]!): Order!
updateProfile(name: String, email: String): User!
}
type User { id: ID! name: String! email: String! role: String! }
type Product { id: ID! name: String! price: Float! category: String! stock: Int! status: String! }
type Order { id: ID! total: Float! status: String! items: [OrderItem!]! createdAt: String! }
type OrderItem { productId: ID! quantity: Int! price: Float! }
input OrderItemInput { productId: ID! quantity: Int! }
`);
const graphqlRoot = {
me: (args, req) => { const u = db.users.get(req.user?.sub); return u ? { id: u.id, name: u.name, email: u.email, role: u.role } : null; },
products: ({ category, first }) => { let r = [...db.products.values()]; if (category) r = r.filter(p => p.category === category); return r.slice(0, first); },
product: ({ id }) => db.products.get(id),
myOrders: (args, req) => [...db.orders.values()].filter(o => o.userId === req.user?.sub),
createOrder: ({ items }, req) => {
if (!req.user) throw new Error('未认证');
let total = 0;
items.forEach(i => { const p = db.products.get(i.productId); if (p) { total += p.price * i.quantity; i.price = p.price; } });
const id = String(db.nextOrderId++);
const order = { id, userId: req.user.sub, items, total, status: 'pending', createdAt: new Date().toISOString() };
db.orders.set(id, order);
return order;
},
updateProfile: ({ name, email }, req) => {
if (!req.user) throw new Error('未认证');
const user = db.users.get(req.user.sub);
if (name) user.name = name;
if (email) user.email = email;
return { id: user.id, name: user.name, email: user.email, role: user.role };
},
};
app.use('/graphql', auth, createHandler({ schema: graphqlSchema, rootValue: graphqlRoot, context: (req) => req.raw }));
// ===== 管理/健康端点 =====
app.get('/health', (req, res) => {
res.json({ status: 'healthy', uptime: process.uptime(), cache: cache.stats(), users: db.users.size, products: db.products.size, orders: db.orders.size });
});
// ===== 错误处理 =====
app.use((err, req, res, next) => {
console.error(`[${req.requestId}] 错误:`, err);
res.status(500).json({ success: false, error: { code: 'INTERNAL_ERROR', message: process.env.NODE_ENV === 'development' ? err.message : '服务器内部错误' }, meta: { requestId: req.requestId } });
});
app.use((req, res) => {
res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: `路径 ${req.method} ${req.path} 不存在` } });
});
// ===== 启动 =====
app.listen(config.port, () => {
console.log(`🚀 API平台运行在 http://localhost:${config.port}`);
console.log(` REST API: http://localhost:${config.port}/api/`);
console.log(` GraphQL: http://localhost:${config.port}/graphql`);
console.log(` 健康检查: http://localhost:${config.port}/health`);
console.log(`\n 快速测试:`);
console.log(` curl -X POST http://localhost:${config.port}/api/auth/login -H "Content-Type: application/json" -d '{"email":"admin@example.com","password":"admin123"}'`);
});
# 安装依赖
npm install express graphql graphql-http
# 启动平台
node platform.js
# 1. 登录
TOKEN=$(curl -s -X POST http://localhost:3000/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"admin@example.com","password":"admin123"}' | jq -r '.data.token')
# 2. REST: 商品列表
curl -s http://localhost:3000/api/products | jq .
# 3. REST: 创建订单
curl -s -X POST http://localhost:3000/api/orders \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"items":[{"productId":"1","quantity":1},{"productId":"2","quantity":2}]}' | jq .
{
"success": true,
"data": {
"id": "2",
"userId": "1",
"items": [{"productId":"1","quantity":1,"price":599},{"productId":"2","quantity":2,"price":199}],
"total": 997,
"status": "pending",
"createdAt": "2025-01-15T10:30:00.000Z"
}
}
# 4. GraphQL: 获取个人信息和订单
curl -s -X POST http://localhost:3000/graphql \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"query":"{ me { name email role } myOrders { id total status items { productId quantity price } } }"}' | jq .
# 5. 健康检查
curl -s http://localhost:3000/health | jq .
| 阶段 | 课程 | 核心收获 |
|---|---|---|
| RESTful基础 | 01-05 | API设计原则、HTTP语义、URL设计、请求响应、状态码 |
| RESTful进阶 | 06-10 | JWT/OAuth2认证、版本管理、分页过滤排序、限流熔断、缓存策略 |
| GraphQL | 11-15 | Schema设计、查询变更、订阅实时、DataLoader、实战 |
| gRPC | 16-20 | Protobuf、四种通信模式、流式处理、gRPC网关 |
| 实战项目 | 21-25 | OpenAPI文档、API网关、微服务设计、安全加固、完整平台 |
恭喜完成全部25课!你已经掌握了:
你现在可以自信地设计和实现生产级API系统。继续实践,持续学习,保持对新技术的好奇心!🚀