普通视图

发现新文章,点击刷新页面。
昨天以前首页

Next.js 教程系列(六)API Routes 与全栈开发基础

作者 鲫小鱼
2025年7月1日 10:58

前言

大家好,我是鲫小鱼。是一名不写前端代码的前端工程师,热衷于分享非前端的知识,带领切图仔逃离切图圈子,欢迎关注我,微信公众号:《鲫小鱼不正经》。欢迎点赞、收藏、关注,一键三连!!

第六章:API Routes 与全栈开发基础

教程简介

本章将带你深入理解 Next.js 的 API Routes 功能,掌握如何在同一个项目中实现前后端一体化开发。你将学会如何编写高质量的 API 接口,处理数据请求、鉴权、错误处理、性能优化等企业级场景。我们还会结合移动端适配、接口安全、全栈开发最佳实践,帮助你构建健壮、可维护的全栈应用。


理论讲解

1.1 API Routes 概述与架构演进

  • API Routes 是 Next.js 提供的后端接口开发能力,允许你在 /pages/api/app/api 目录下直接编写 Node.js 风格的接口。
  • 支持 RESTful、GraphQL、Webhooks、BFF(Backend For Frontend)等多种接口风格。
  • 与前端页面共享同一项目、同一依赖、同一部署流程,极大提升开发效率。
  • 适合中小型全栈项目、BFF 模式、原型开发、企业级微服务网关等场景。
  • 推荐分层架构:API 层(路由)、服务层(业务逻辑)、数据访问层(DAO),便于维护和扩展。
  • 支持接口版本管理(如 /api/v1/),便于平滑升级。

1.2 API Routes 的基本用法与进阶

  • /pages/api 目录下创建任意 .ts/.js 文件,即可自动成为一个 API 路由。
  • 每个文件导出一个默认函数,接收 req(请求对象)和 res(响应对象)。
  • 支持 GET、POST、PUT、DELETE 等 HTTP 方法。
  • 支持中间件、Cookie、Session、文件上传、数据库操作、Edge API Routes(边缘计算)、Server Actions(App Router)。
  • 推荐按业务模块拆分目录,如 /api/user//api/order//api/admin/

1.3 API Routes 与全栈开发

  • 前端页面通过 fetchaxiosSWRReact Query 等方式请求本地 API。
  • API 层可集成数据库(如 Prisma、TypeORM)、第三方服务(如 Stripe、微信支付)、缓存(如 Redis)、消息队列等。
  • 支持 SSR/SSG/ISR 等多种渲染模式下的数据获取。
  • 可作为微服务网关,聚合/转发后端服务。

1.4 企业级安全与权限控制

  • 鉴权:结合 JWT、Session、OAuth2、API Key、第三方登录(如 GitHub、微信)实现用户身份校验。
  • 接口限流:防止恶意刷接口,可用 Redis、内存、第三方服务实现。
  • CSRF/XSS 防护:合理设置 CORS、校验 Referer、过滤输入。
  • 敏感信息保护:环境变量、加密存储、日志脱敏、接口审计。
  • 接口签名与幂等性:对关键接口请求参数签名校验,防止篡改和重复提交。
  • 多租户支持:通过租户ID、Token、Header 实现多租户隔离。

1.5 性能优化与高可用

  • 缓存:HTTP 缓存头、Redis、CDN、接口预热、缓存穿透防护。
  • 批量/合并请求:减少接口数量,提升移动端体验。
  • 异步处理:如队列、定时任务,避免接口阻塞主线程。
  • 边缘计算:利用 Vercel Edge Functions 实现低延迟接口。
  • 接口降级与容灾:主服务异常时自动降级到备用方案。
  • 日志与监控:接口需有日志、埋点、告警,便于排查问题。

1.6 Mock、自动化测试与文档

  • Mock Service Worker(MSW):前端可独立开发调试,后端未完成时模拟接口。
  • Jest/Supertest:为 API Routes 编写单元测试、集成测试。
  • 契约测试:保证前后端数据结构一致。
  • OpenAPI/Swagger:自动生成接口文档,支持在线调试。
  • 类型注释:结合 TypeScript 类型,提升文档准确性。
  • CI 持续集成:自动化测试覆盖,保障接口质量。

1.7 多端适配与国际化

  • 响应式接口:根据 UA/参数返回不同数据结构,适配 Web/移动/小程序/桌面端。
  • 国际化:接口支持多语言返回,结合 i18n、next-intl。
  • 图片/多媒体优化:返回合适尺寸的图片链接,支持 WebP、AVIF。
  • 网络异常处理:接口需返回明确错误码和提示,前端可友好降级。

1.8 错误处理与监控

  • 统一错误码与响应格式:便于前端处理和埋点。
  • Sentry/LogRocket:接入错误监控,自动上报异常。
  • 慢接口告警:接口超时自动告警,便于性能优化。
  • 日志采集:接口请求日志、用户行为日志、异常日志。

代码示例

2.1 创建基础 API 路由

// pages/api/hello.ts
import type { NextApiRequest, NextApiResponse } from 'next';

export default function handler(req: NextApiRequest, res: NextApiResponse) {
  res.status(200).json({ message: 'Hello, Next.js API!' });
}

2.2 支持多种 HTTP 方法

// pages/api/user.ts
import type { NextApiRequest, NextApiResponse } from 'next';

let users = [
  { id: 1, name: '小明' },
  { id: 2, name: '小红' },
];

export default function handler(req: NextApiRequest, res: NextApiResponse) {
  if (req.method === 'GET') {
    res.status(200).json(users);
  } else if (req.method === 'POST') {
    const { name } = req.body;
    const newUser = { id: Date.now(), name };
    users.push(newUser);
    res.status(201).json(newUser);
  } else {
    res.status(405).json({ error: 'Method Not Allowed' });
  }
}

2.3 接口鉴权与 JWT 校验

// pages/api/profile.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import jwt from 'jsonwebtoken';

const SECRET = process.env.JWT_SECRET || 'demo_secret';

export default function handler(req: NextApiRequest, res: NextApiResponse) {
  const token = req.headers.authorization?.split(' ')[1];
  if (!token) {
    return res.status(401).json({ error: '未登录' });
  }
  try {
    const user = jwt.verify(token, SECRET);
    res.status(200).json({ user });
  } catch {
    res.status(401).json({ error: 'Token 无效' });
  }
}

2.4 接口限流中间件

// lib/rateLimit.ts
const rateLimitMap = new Map<string, { count: number; last: number }>();

export function rateLimit(ip: string, limit = 10, windowMs = 60_000) {
  const now = Date.now();
  const entry = rateLimitMap.get(ip) || { count: 0, last: now };
  if (now - entry.last > windowMs) {
    rateLimitMap.set(ip, { count: 1, last: now });
    return false;
  }
  if (entry.count >= limit) return true;
  rateLimitMap.set(ip, { count: entry.count + 1, last: entry.last });
  return false;
}
// pages/api/secure-data.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { rateLimit } from '@/lib/rateLimit';

export default function handler(req: NextApiRequest, res: NextApiResponse) {
  const ip = req.headers['x-forwarded-for']?.toString() || req.socket.remoteAddress || '';
  if (rateLimit(ip, 5, 60_000)) {
    return res.status(429).json({ error: '请求过于频繁,请稍后再试' });
  }
  res.status(200).json({ data: '安全数据' });
}

2.5 文件上传与表单处理

// pages/api/upload.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import formidable from 'formidable';
import fs from 'fs';

export const config = {
  api: {
    bodyParser: false,
  },
};

export default function handler(req: NextApiRequest, res: NextApiResponse) {
  const form = new formidable.IncomingForm();
  form.parse(req, (err, fields, files) => {
    if (err) return res.status(500).json({ error: '上传失败' });
    // 假设保存到本地
    const file = files.file as formidable.File;
    fs.renameSync(file.filepath, `./public/uploads/${file.originalFilename}`);
    res.status(200).json({ url: `/uploads/${file.originalFilename}` });
  });
}

2.6 数据库操作(以 Prisma 为例)

// pages/api/products.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  if (req.method === 'GET') {
    const products = await prisma.product.findMany();
    res.status(200).json(products);
  } else if (req.method === 'POST') {
    const { name, price } = req.body;
    const product = await prisma.product.create({ data: { name, price } });
    res.status(201).json(product);
  } else {
    res.status(405).json({ error: 'Method Not Allowed' });
  }
}

2.7 移动端分页与懒加载接口

// pages/api/feed.ts
import type { NextApiRequest, NextApiResponse } from 'next';

const allItems = Array.from({ length: 100 }).map((_, i) => ({ id: i + 1, title: `Item ${i + 1}` }));

export default function handler(req: NextApiRequest, res: NextApiResponse) {
  const page = parseInt(req.query.page as string) || 1;
  const limit = parseInt(req.query.limit as string) || 10;
  const start = (page - 1) * limit;
  const end = start + limit;
  res.status(200).json({
    items: allItems.slice(start, end),
    total: allItems.length,
    page,
    limit,
  });
}

2.8 错误处理与统一响应格式

// lib/response.ts
export function success(data: any) {
  return { code: 0, data };
}
export function error(message: string, code = 1) {
  return { code, message };
}
// pages/api/unified.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { success, error } from '@/lib/response';

export default function handler(req: NextApiRequest, res: NextApiResponse) {
  try {
    // ...业务逻辑
    res.status(200).json(success({ msg: 'ok' }));
  } catch (e) {
    res.status(500).json(error('服务器异常'));
  }
}

2.9 GraphQL API 基础

// pages/api/graphql.ts
import { ApolloServer, gql } from 'apollo-server-micro';

const typeDefs = gql`
  type User { id: ID! name: String! }
  type Query { user(id: ID!): User }
`;
const resolvers = {
  Query: {
    user: (_: any, { id }: { id: string }) => ({ id, name: '小明' }),
  },
};
const apolloServer = new ApolloServer({ typeDefs, resolvers });
export const config = { api: { bodyParser: false } };
export default apolloServer.createHandler({ path: '/api/graphql' });

2.10 API Key 鉴权与签名校验

// pages/api/secure.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import crypto from 'crypto';

const API_KEY = process.env.API_KEY || 'demo_key';
const SECRET = process.env.API_SECRET || 'demo_secret';

function verifySignature(req: NextApiRequest) {
  const signature = req.headers['x-signature'] as string;
  const timestamp = req.headers['x-timestamp'] as string;
  const raw = `${timestamp}${API_KEY}${SECRET}`;
  const expected = crypto.createHash('sha256').update(raw).digest('hex');
  return signature === expected;
}

export default function handler(req: NextApiRequest, res: NextApiResponse) {
  if (req.headers['x-api-key'] !== API_KEY || !verifySignature(req)) {
    return res.status(401).json({ error: '无效签名' });
  }
  res.status(200).json({ data: '安全数据' });
}

2.11 幂等性与防重复提交

// lib/idempotency.ts
const idempotencyMap = new Map<string, number>();
export function isDuplicate(id: string) {
  if (idempotencyMap.has(id)) return true;
  idempotencyMap.set(id, Date.now());
  setTimeout(() => idempotencyMap.delete(id), 60_000);
  return false;
}
// pages/api/order.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { isDuplicate } from '@/lib/idempotency';

export default function handler(req: NextApiRequest, res: NextApiResponse) {
  const id = req.headers['x-idempotency-key'] as string;
  if (!id || isDuplicate(id)) {
    return res.status(409).json({ error: '重复提交' });
  }
  // ...创建订单逻辑
  res.status(201).json({ message: '订单创建成功' });
}

2.12 Mock 与自动化测试

// __tests__/api/user.test.ts
import handler from '../../pages/api/user';
import { createMocks } from 'node-mocks-http';

test('GET /api/user', async () => {
  const { req, res } = createMocks({ method: 'GET' });
  await handler(req, res);
  expect(res._getStatusCode()).toBe(200);
  expect(JSON.parse(res._getData())).toEqual([
    { id: 1, name: '小明' },
    { id: 2, name: '小红' },
  ]);
});

2.13 OpenAPI/Swagger 自动生成文档

// scripts/generate-openapi.js
// 使用 swagger-jsdoc 自动生成 openapi.json
const swaggerJSDoc = require('swagger-jsdoc');
const options = { ... };
const openapiSpec = swaggerJSDoc(options);
require('fs').writeFileSync('openapi.json', JSON.stringify(openapiSpec, null, 2));

2.14 多语言接口

// pages/api/i18n.ts
import type { NextApiRequest, NextApiResponse } from 'next';
const messages = {
  zh: { hello: '你好' },
  en: { hello: 'Hello' },
};
export default function handler(req: NextApiRequest, res: NextApiResponse) {
  const lang = req.query.lang || 'zh';
  res.status(200).json({ message: messages[lang as string]?.hello || messages.zh.hello });
}

2.15 Sentry 错误监控集成

// pages/api/_middleware.ts
import * as Sentry from '@sentry/nextjs';
Sentry.init({ dsn: process.env.SENTRY_DSN });
export default function middleware(req, ev) {
  try {
    return NextResponse.next();
  } catch (e) {
    Sentry.captureException(e);
    throw e;
  }
}

实战项目

3.1 构建全栈商品管理系统

目标:实现一个支持商品增删改查、图片上传、用户鉴权、移动端适配的全栈商品管理系统。

主要功能:
  1. 商品列表页:支持分页、搜索、移动端自适应。
  2. 商品详情页:展示商品图片、价格、描述。
  3. 后台管理页:支持商品的新增、编辑、删除。
  4. 用户登录鉴权:JWT 登录、接口权限控制。
  5. 图片上传:支持多图上传、进度显示。
  6. API 接口:全部基于 Next.js API Routes 实现。
  7. 错误处理与统一响应格式。
  8. 性能优化:接口缓存、按需加载。
  9. 日志与监控:接口请求日志、错误告警。
  10. 国际化与移动端适配。
关键代码片段:
// pages/products/index.tsx
import useSWR from 'swr';
import { useState } from 'react';

const fetcher = (url: string) => fetch(url).then(res => res.json());

export default function ProductsPage() {
  const [page, setPage] = useState(1);
  const { data, error } = useSWR(`/api/feed?page=${page}&limit=10`, fetcher);
  if (error) return <div>加载失败</div>;
  if (!data) return <div>加载中...</div>;
  return (
    <div>
      <ul>
        {data.items.map((item: any) => (
          <li key={item.id}>{item.title}</li>
        ))}
      </ul>
      <button disabled={page === 1} onClick={() => setPage(page - 1)}>上一页</button>
      <button disabled={data.items.length < 10} onClick={() => setPage(page + 1)}>下一页</button>
    </div>
  );
}
// pages/admin/products.tsx
import { useState } from 'react';

export default function AdminProducts() {
  const [name, setName] = useState('');
  const [price, setPrice] = useState('');
  const [msg, setMsg] = useState('');
  const handleAdd = async () => {
    const res = await fetch('/api/products', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ name, price: Number(price) }),
    });
    const data = await res.json();
    setMsg(data.code === 0 ? '添加成功' : data.message);
  };
  return (
    <div>
      <input value={name} onChange={e => setName(e.target.value)} placeholder="商品名" />
      <input value={price} onChange={e => setPrice(e.target.value)} placeholder="价格" type="number" />
      <button onClick={handleAdd}>添加商品</button>
      <div>{msg}</div>
    </div>
  );
}

3.2 多租户商城 API 设计

  • 支持多租户(如不同商家/品牌)数据隔离。
  • 每个租户有独立的商品、订单、用户数据。
  • 通过 Header、Token、子域名等区分租户。
  • 接口返回结构需兼容多端(Web/移动/小程序)。
  • 支持租户级别的权限、限流、定制化配置。

3.3 订单系统与支付回调

  • 订单创建接口需防止重复提交(幂等性)。
  • 支付回调接口需校验签名、防止伪造。
  • 订单状态流转需有日志、告警。
  • 支持异步通知、消息推送。

3.4 实时消息与 WebSocket

  • 使用 nextjs-websocket、socket.io 实现实时订单状态推送。
  • 支持移动端、桌面端多端同步。
  • 接口需有鉴权、限流、断线重连机制。

3.5 复杂参数校验与批量导入导出

// lib/validate.ts
import * as z from 'zod';
export const productSchema = z.object({
  name: z.string().min(2),
  price: z.number().positive(),
});
// pages/api/products.ts
import { productSchema } from '@/lib/validate';
export default async function handler(req, res) {
  if (req.method === 'POST') {
    const parse = productSchema.safeParse(req.body);
    if (!parse.success) {
      return res.status(400).json({ error: '参数校验失败', details: parse.error.errors });
    }
    // ...创建逻辑
  }
}
  • 支持 Excel/CSV 批量导入商品,接口需校验格式、异步处理。
  • 导出接口支持大数据量分片导出,防止超时。

3.6 长轮询与 WebSocket

  • 长轮询接口需有超时、重试机制。
  • WebSocket 需支持断线重连、心跳包。

常见问题与最佳实践

  • API 目录混乱:建议按业务模块拆分,统一命名规范。
  • 环境变量泄漏:敏感信息只放 .env,不要暴露到前端。
  • SSR/CSR 下接口复用:建议统一用 API Routes,避免重复实现。
  • 接口 Mock 不一致:Mock 数据与真实接口保持同步,自动化测试覆盖。
  • 接口文档滞后:用 OpenAPI/Swagger 自动生成,CI 校验。
  • 多端适配遗漏:接口返回结构需兼容 Web/移动/小程序。
  • 接口安全被忽视:务必加鉴权、限流、签名、日志。
  • 性能瓶颈:接口加缓存、CDN、边缘计算,定期分析慢接口。
  • 错误处理不统一:统一响应格式,前端友好提示。

片尾

最后感谢阅读!欢迎关注我,微信公众号:《鲫小鱼不正经》。欢迎点赞、收藏、关注,一键三连!!!

❌
❌