在现代 Web 开发中,前后端分离(Frontend-Backend Separation)已成为主流架构模式,前端负责用户界面和交互逻辑,后端专注于数据处理和业务逻辑,而 API(应用程序编程接口)作为两者之间的桥梁,决定了系统的通信效率、可维护性和可扩展性。设计一套高效、清晰、可扩展的 API 是前后端分离项目的核心任务。
1. 前后端分离的 API 设计基础
1.1 为什么需要前后端分离?
前后端分离通过明确职责分工,解决了传统单体应用(如 JSP、PHP 渲染页面)中的诸多问题:
- 开发效率:前端和后端可并行开发,减少耦合。
- 技术栈灵活性:前端可使用 React、Vue,后端可选择 Node.js、Java 等。
- 可维护性:前后端代码分离,逻辑更清晰。
- 用户体验:前端通过异步 API 调用,实现动态加载和流畅交互。
API 作为前后端通信的纽带,必须满足以下要求:
- 清晰性:接口定义直观,易于理解。
- 一致性:命名、结构和响应格式统一。
- 可扩展性:支持功能迭代和版本演进。
- 性能:响应速度快,资源占用低。
1.2 API 设计的核心原则
在前后端分离中,API 设计应遵循以下原则:
- 以资源为中心:将数据抽象为资源(如用户、订单),通过 URI 标识。
- 无状态:每个请求包含完整信息,服务器不保存客户端状态。
- 一致性命名:使用统一的命名规范(如小写、连字符)。
- 可预测性:响应结构和错误格式可预期。
- 文档化:提供详细的 API 文档,便于前后端协作。
2. RESTful API 设计
REST(Representational State Transfer)是前后端分离中最常用的 API 设计范式,其核心是将数据建模为资源,通过 HTTP 方法操作。
2.1 资源与 URI 设计
RESTful API 以资源为核心,每个资源通过唯一的 URI 标识。设计 URI 时,应遵循以下规则:
- 使用名词 :URI 表示资源(如
/users
,而不是/getUsers
)。 - 层级清晰 :通过路径反映资源关系(如
/users/123/orders
)。 - 避免过长:保持 URI 简洁,通常不超过三层。
- 使用连字符 :如
/user-profiles
而非/user_profiles
。
示例:
http
GET /users # 获取用户列表
GET /users/123 # 获取单个用户
POST /users # 创建用户
PUT /users/123 # 更新用户
DELETE /users/123 # 删除用户
2.2 HTTP 方法
RESTful API 使用标准的 HTTP 方法:
- GET:查询资源,无副作用。
- POST:创建资源或触发操作。
- PUT:更新整个资源(全量更新)。
- PATCH:部分更新资源。
- DELETE:删除资源。
示例:
http
PATCH /users/123
Content-Type: application/json
{
"email": "[email protected]"
}
2.3 响应结构
RESTful API 的响应应具有一致的结构,通常包括:
- data:返回的核心数据。
- status :请求状态(如
success
或error
)。 - message:错误或提示信息。
- meta:分页或附加信息。
示例响应:
json
{
"status": "success",
"data": {
"id": 123,
"name": "Alice",
"email": "[email protected]"
},
"meta": {
"total": 100,
"page": 1,
"limit": 10
}
}
错误响应:
json
{
"status": "error",
"message": "User not found",
"code": 404
}
2.4 状态码
使用 HTTP 状态码表示请求结果:
- 200 OK:请求成功。
- 201 Created:资源创建成功。
- 204 No Content:请求成功,无返回内容(如 DELETE)。
- 400 Bad Request:请求参数错误。
- 401 Unauthorized:未授权。
- 403 Forbidden:权限不足。
- 404 Not Found:资源不存在。
- 500 Internal Server Error:服务器错误。
2.5 实现 RESTful API
以 Node.js 和 Express 为例,创建一个简单的 RESTful API:
bash
mkdir rest-api
cd rest-api
npm init -y
npm install express typescript ts-node @types/express @types/node --save-dev
创建 tsconfig.json
:
json
{
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"strict": true,
"esModuleInterop": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
创建 src/index.ts
:
typescript
import express, { Request, Response } from 'express';
const app = express();
app.use(express.json());
interface User {
id: string;
name: string;
email: string;
}
let users: User[] = [
{ id: '1', name: 'Alice', email: '[email protected]' },
];
app.get('/users', (req: Request, res: Response) => {
res.status(200).json({
status: 'success',
data: users,
meta: { total: users.length },
});
});
app.get('/users/:id', (req: Request, res: Response) => {
const user = users.find(u => u.id === req.params.id);
if (!user) {
return res.status(404).json({ status: 'error', message: 'User not found' });
}
res.status(200).json({ status: 'success', data: user });
});
app.post('/users', (req: Request, res: Response) => {
const { name, email } = req.body;
if (!name || !email) {
return res.status(400).json({ status: 'error', message: 'Name and email required' });
}
const newUser: User = { id: String(users.length + 1), name, email };
users.push(newUser);
res.status(201).json({ status: 'success', data: newUser });
});
app.put('/users/:id', (req: Request, res: Response) => {
const userIndex = users.findIndex(u => u.id === req.params.id);
if (userIndex === -1) {
return res.status(404).json({ status: 'error', message: 'User not found' });
}
const { name, email } = req.body;
users[userIndex] = { id: req.params.id, name, email };
res.status(200).json({ status: 'success', data: users[userIndex] });
});
app.delete('/users/:id', (req: Request, res: Response) => {
const userIndex = users.findIndex(u => u.id === req.params.id);
if (userIndex === -1) {
return res.status(404).json({ status: 'error', message: 'User not found' });
}
users.splice(userIndex, 1);
res.status(204).send();
});
app.listen(3000, () => console.log('Server running on port 3000'));
运行:
bash
npx ts-node src/index.ts
使用 cURL 测试:
bash
curl http://localhost:3000/users
curl -X POST http://localhost:3000/users -H "Content-Type: application/json" -d '{"name":"Bob","email":"[email protected]"}'
3. GraphQL API 设计
GraphQL 是一种替代 REST 的查询语言,特别适合前后端分离中复杂数据需求场景。它的核心优势是允许客户端精确指定所需数据,避免过量或不足。
3.1 GraphQL Schema 设计
GraphQL 使用 Schema 定义数据模型和操作,包括:
- Type:定义数据结构。
- Query:查询操作。
- Mutation:变更操作。
- Subscription:实时更新。
示例 Schema:
graphql
type User {
id: ID!
name: String!
email: String
orders: [Order!]
}
type Order {
id: ID!
total: Float!
createdAt: String!
}
type Query {
user(id: ID!): User
users: [User!]!
}
type Mutation {
createUser(name: String!, email: String): User!
}
3.2 实现 GraphQL API
使用 Apollo Server 实现 GraphQL API:
bash
mkdir graphql-api
cd graphql-api
npm init -y
npm install apollo-server graphql typescript ts-node @types/node --save-dev
创建 tsconfig.json
(同上)。
创建 src/schema.ts
:
typescript
import { gql } from 'apollo-server';
const typeDefs = gql`
type User {
id: ID!
name: String!
email: String
orders: [Order!]
}
type Order {
id: ID!
total: Float!
createdAt: String!
}
type Query {
user(id: ID!): User
users: [User!]!
}
type Mutation {
createUser(name: String!, email: String): User!
}
`;
export default typeDefs;
创建 src/data.ts
:
typescript
export const users = [
{ id: '1', name: 'Alice', email: '[email protected]' },
{ id: '2', name: 'Bob', email: '[email protected]' },
];
export const orders = [
{ id: '1', userId: '1', total: 99.99, createdAt: '2023-01-01' },
{ id: '2', userId: '1', total: 49.99, createdAt: '2023-01-02' },
];
创建 src/resolvers.ts
:
typescript
import { users, orders } from './data';
interface User {
id: string;
name: string;
email?: string;
}
interface Order {
id: string;
userId: string;
total: number;
createdAt: string;
}
const resolvers = {
Query: {
user: (_: any, { id }: { id: string }) => users.find(user => user.id === id),
users: () => users,
},
Mutation: {
createUser: (_: any, { name, email }: { name: string; email?: string }) => {
const newUser: User = { id: String(users.length + 1), name, email };
users.push(newUser);
return newUser;
},
},
User: {
orders: (parent: User) => orders.filter(order => order.userId === parent.id),
},
};
export default resolvers;
创建 src/index.ts
:
typescript
import { ApolloServer } from 'apollo-server';
import typeDefs from './schema';
import resolvers from './resolvers';
const server = new ApolloServer({ typeDefs, resolvers });
server.listen({ port: 4000 }).then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});
运行:
bash
npx ts-node src/index.ts
访问 http://localhost:4000
,使用以下查询:
graphql
query {
user(id: "1") {
name
email
orders {
id
total
}
}
}
3.3 GraphQL 客户端集成
在前端使用 Apollo Client 调用 GraphQL API:
bash
mkdir frontend
cd frontend
npm init -y
npm install @apollo/client graphql react react-dom typescript @types/react @types/react-dom --save-dev
创建 src/client.ts
:
typescript
import { ApolloClient, InMemoryCache } from '@apollo/client';
const client = new ApolloClient({
uri: 'http://localhost:4000',
cache: new InMemoryCache(),
});
export default client;
创建 src/App.tsx
:
typescript
import React from 'react';
import { useQuery, gql } from '@apollo/client';
const GET_USER = gql`
query GetUser($id: ID!) {
user(id: $id) {
name
email
orders {
id
total
}
}
}
`;
const App: React.FC = () => {
const { loading, error, data } = useQuery(GET_USER, {
variables: { id: '1' },
});
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<div>
<h1>{data.user.name}</h1>
<p>{data.user.email}</p>
<ul>
{data.user.orders.map((order: any) => (
<li key={order.id}>Order #{order.id}: ${order.total}</li>
))}
</ul>
</div>
);
};
export default App;
创建 src/index.tsx
:
typescript
import React from 'react';
import { createRoot } from 'react-dom/client';
import { ApolloProvider } from '@apollo/client';
import App from './App';
import client from './client';
const root = createRoot(document.getElementById('root')!);
root.render(
<ApolloProvider client={client}>
<App />
</ApolloProvider>
);
4. API 版本管理
在前后端分离中,API 演进不可避免,版本管理确保向后兼容。
4.1 URI 版本化
在 URI 中嵌入版本号:
http
GET /api/v1/users
GET /api/v2/users
实现:
typescript
app.get('/api/v1/users', (req: Request, res: Response) => {
res.status(200).json({ status: 'success', data: users });
});
app.get('/api/v2/users', (req: Request, res: Response) => {
const updatedUsers = users.map(u => ({ ...u, version: '2.0' }));
res.status(200).json({ status: 'success', data: updatedUsers });
});
4.2 请求头版本化
通过 Accept
头指定版本:
http
GET /api/users
Accept: application/vnd.example.v1+json
实现:
typescript
app.get('/api/users', (req: Request, res: Response) => {
const accept = req.header('Accept');
if (accept === 'application/vnd.example.v2+json') {
const updatedUsers = users.map(u => ({ ...u, version: '2.0' }));
return res.status(200).json({ status: 'success', data: updatedUsers });
}
res.status(200).json({ status: 'success', data: users });
});
4.3 GraphQL 的版本管理
GraphQL 通过 Schema 演进避免显式版本管理:
-
添加字段:向类型添加新字段,不会破坏现有查询。
-
弃用字段 :使用
@deprecated
标记:graphqltype User { id: ID! name: String! email: String @deprecated(reason: "Use emailAddress instead") emailAddress: String }
5. 认证与授权
5.1 JWT 认证
JSON Web Token(JWT)是前后端分离中常用的认证方式。
安装依赖:
bash
npm install jsonwebtoken @types/jsonwebtoken
实现登录端点:
typescript
import jwt from 'jsonwebtoken';
const SECRET = 'your-secret-key';
app.post('/login', (req: Request, res: Response) => {
const { username, password } = req.body;
if (username === 'admin' && password === 'password') {
const token = jwt.sign({ username }, SECRET, { expiresIn: '1h' });
return res.status(200).json({ status: 'success', token });
}
res.status(401).json({ status: 'error', message: 'Invalid credentials' });
});
中间件验证:
typescript
const authenticate = (req: Request, res: Response, next: Function) => {
const token = req.header('Authorization')?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({ status: 'error', message: 'No token provided' });
}
try {
const decoded = jwt.verify(token, SECRET);
(req as any).user = decoded;
next();
} catch (err) {
res.status(401).json({ status: 'error', message: 'Invalid token' });
}
};
app.get('/users', authenticate, (req: Request, res: Response) => {
res.status(200).json({ status: 'success', data: users });
});
前端发送请求:
javascript
fetch('http://localhost:3000/users', {
headers: {
Authorization: `Bearer ${token}`,
},
})
.then(res => res.json())
.then(data => console.log(data));
5.2 OAuth 2.0
OAuth 2.0 适合第三方授权场景。使用 passport
实现:
bash
npm install passport passport-google-oauth20 @types/passport @types/passport-google-oauth20
配置:
typescript
import passport from 'passport';
import { Strategy as GoogleStrategy } from 'passport-google-oauth20';
passport.use(
new GoogleStrategy(
{
clientID: 'your-client-id',
clientSecret: 'your-client-secret',
callbackURL: 'http://localhost:3000/auth/google/callback',
},
(accessToken, refreshToken, profile, done) => {
// 保存用户
done(null, profile);
}
)
);
app.get('/auth/google', passport.authenticate('google', { scope: ['profile', 'email'] }));
app.get(
'/auth/google/callback',
passport.authenticate('google', { session: false }),
(req: Request, res: Response) => {
const token = jwt.sign({ id: (req.user as any).id }, SECRET);
res.redirect(`/dashboard?token=${token}`);
}
);
5.3 GraphQL 认证
在 GraphQL 中,通过 context
传递用户信息:
typescript
const server = new ApolloServer({
typeDefs,
resolvers,
context: ({ req }) => {
const token = req.headers.authorization?.replace('Bearer ', '');
if (token) {
const user = jwt.verify(token, SECRET);
return { user };
}
return {};
},
});
在 Resolver 中验证:
typescript
const resolvers = {
Query: {
user: (_: any, { id }: { id: string }, { user }: { user: any }) => {
if (!user) throw new Error('Unauthorized');
return users.find(u => u.id === id);
},
},
};
6. 错误处理
6.1 REST 错误处理
定义全局错误中间件:
typescript
app.use((err: Error, req: Request, res: Response, next: Function) => {
console.error(err.stack);
res.status(500).json({ status: 'error', message: 'Internal server error' });
});
自定义错误类:
typescript
class ApiError extends Error {
status: number;
constructor(message: string, status: number) {
super(message);
this.status = status;
}
}
app.get('/users/:id', (req: Request, res: Response, next: Function) => {
const user = users.find(u => u.id === req.params.id);
if (!user) {
return next(new ApiError('User not found', 404));
}
res.status(200).json({ status: 'success', data: user });
});
app.use((err: ApiError, req: Request, res: Response, next: Function) => {
res.status(err.status || 500).json({ status: 'error', message: err.message });
});
6.2 GraphQL 错误处理
GraphQL 使用 errors
字段返回错误:
typescript
const resolvers = {
Query: {
user: (_: any, { id }: { id: string }) => {
const user = users.find(u => u.id === id);
if (!user) {
throw new Error('User not found');
}
return user;
},
},
};
自定义错误:
typescript
import { ApolloError } from 'apollo-server';
const resolvers = {
Query: {
user: (_: any, { id }: { id: string }) => {
const user = users.find(u => u.id === id);
if (!user) {
throw new ApolloError('User not found', 'NOT_FOUND', { status: 404 });
}
return user;
},
},
};
7. 性能优化
7.1 REST 性能优化
-
分页:
httpGET /users?page=1&limit=10
typescriptapp.get('/users', (req: Request, res: Response) => { const page = parseInt(req.query.page as string) || 1; const limit = parseInt(req.query.limit as string) || 10; const start = (page - 1) * limit; const paginatedUsers = users.slice(start, start + limit); res.status(200).json({ status: 'success', data: paginatedUsers, meta: { total: users.length, page, limit }, }); });
-
缓存 :
使用 ETag 或 Redis 缓存:
typescriptimport redis from 'redis'; const client = redis.createClient(); app.get('/users', async (req: Request, res: Response) => { const cached = await client.get('users'); if (cached) { return res.status(200).json(JSON.parse(cached)); } const response = { status: 'success', data: users }; await client.setEx('users', 3600, JSON.stringify(response)); res.status(200).json(response); });
-
压缩:
bashnpm install compression
typescriptimport compression from 'compression'; app.use(compression());
7.2 GraphQL 性能优化
-
DataLoader:
bashnpm install dataloader
typescriptimport DataLoader from 'dataloader'; const userLoader = new DataLoader(async (ids: string[]) => { const users = await db.users.find({ id: { $in: ids } }); return ids.map(id => users.find(user => user.id === id)); }); const resolvers = { Query: { user: (_: any, { id }: { id: string }) => userLoader.load(id), }, };
-
查询深度限制:
bashnpm install graphql-depth-limit
typescriptimport depthLimit from 'graphql-depth-limit'; const server = new ApolloServer({ typeDefs, resolvers, validationRules: [depthLimit(5)], });
-
持久化查询:
typescriptconst server = new ApolloServer({ persistedQueries: { cache: new InMemoryLRUCache(), }, });
8. API 文档生成
8.1 REST 文档(OpenAPI/Swagger)
使用 swagger-ui-express
生成文档:
bash
npm install swagger-ui-express @types/swagger-ui-express
创建 src/swagger.json
:
json
{
"openapi": "3.0.0",
"info": {
"title": "REST API",
"version": "1.0.0"
},
"paths": {
"/users": {
"get": {
"summary": "Get all users",
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"status": { "type": "string" },
"data": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": { "type": "string" },
"name": { "type": "string" },
"email": { "type": "string" }
}
}
}
}
}
}
}
}
}
}
}
}
}
在 index.ts
中集成:
typescript
import swaggerUi from 'swagger-ui-express';
import swaggerDocument from './swagger.json';
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument));
访问 http://localhost:3000/api-docs
查看文档。
8.2 GraphQL 文档
Apollo Server 内置文档支持,访问 http://localhost:4000
进入 Apollo Studio。也可以使用 graphql-codegen
生成类型:
bash
npm install @graphql-codegen/cli @graphql-codegen/typescript
创建 codegen.yml
:
yaml
schema: http://localhost:4000
documents: './src/**/*.graphql'
generates:
./src/generated/graphql.ts:
plugins:
- typescript
- typescript-operations
运行:
bash
npx graphql-codegen
9. 实时 API 设计
9.1 WebSocket(REST)
使用 ws
实现实时更新:
bash
npm install ws @types/ws
在 src/websocket.ts
:
typescript
import { WebSocketServer } from 'ws';
const wss = new WebSocketServer({ port: 8080 });
wss.on('connection', ws => {
ws.on('message', message => {
wss.clients.forEach(client => {
if (client.readyState === WebSocket.OPEN) {
client.send(`Broadcast: ${message}`);
}
});
});
});
前端连接:
javascript
const ws = new WebSocket('ws://localhost:8080');
ws.onmessage = event => console.log(event.data);
ws.send('Hello, server!');
9.2 GraphQL Subscription
在 schema.ts
添加:
graphql
type Subscription {
orderAdded(userId: ID!): Order!
}
在 resolvers.ts
:
typescript
import { PubSub } from 'apollo-server';
const pubsub = new PubSub();
const ORDER_ADDED = 'ORDER_ADDED';
const resolvers = {
Subscription: {
orderAdded: {
subscribe: (_: any, { userId }: { userId: string }) => pubsub.asyncIterator([ORDER_ADDED]),
},
},
Mutation: {
createOrder: (_: any, { userId, total }: { userId: string; total: number }) => {
const newOrder = { id: String(orders.length + 1), userId, total, createdAt: new Date().toISOString() };
orders.push(newOrder);
pubsub.publish(ORDER_ADDED, { orderAdded: newOrder });
return newOrder;
},
},
};
前端使用:
typescript
import { useSubscription, gql } from '@apollo/client';
const ORDER_ADDED = gql`
subscription OrderAdded($userId: ID!) {
orderAdded(userId: $userId) {
id
total
}
}
`;
const OrderListener: React.FC<{ userId: string }> = ({ userId }) => {
const { data, loading } = useSubscription(ORDER_ADDED, {
variables: { userId },
});
if (loading) return <p>Waiting...</p>;
return data ? <p>New Order: #{data.orderAdded.id}</p> : null;
};
10. 测试 API
10.1 REST 测试
使用 Jest 和 Supertest:
bash
npm install jest supertest @types/jest @types/supertest ts-jest --save-dev
创建 src/__tests__/users.test.ts
:
typescript
import request from 'supertest';
import app from '../index';
describe('Users API', () => {
it('should get all users', async () => {
const res = await request(app).get('/users');
expect(res.status).toBe(200);
expect(res.body.status).toBe('success');
expect(res.body.data).toBeInstanceOf(Array);
});
it('should create a user', async () => {
const res = await request(app)
.post('/users')
.send({ name: 'Charlie', email: '[email protected]' });
expect(res.status).toBe(201);
expect(res.body.data.name).toBe('Charlie');
});
});
配置 jest.config.js
:
javascript
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
};
运行:
bash
npx jest
10.2 GraphQL 测试
创建 src/__tests__/graphql.test.ts
:
typescript
import { ApolloServer } from 'apollo-server';
import typeDefs from '../schema';
import resolvers from '../resolvers';
const server = new ApolloServer({ typeDefs, resolvers });
describe('GraphQL API', () => {
it('should get user', async () => {
const query = `
query {
user(id: "1") {
name
email
}
}
`;
const res = await server.executeOperation({ query });
expect(res.data?.user.name).toBe('Alice');
});
});
运行:
bash
npx jest