Claude Code 实战案例:全栈电商平台Next.js + Express + MongoDB 从零到上线
目录
第一部分:项目规划与架构设计
1.1 项目概览
| 属性 | 前端 | 后端 |
| 框架 | Next.js 14(App Router) | Express 4 + TypeScript |
| 数据库 | — | MongoDB + Mongoose |
| 样式 | Tailwind CSS + shadcn/ui | — |
| 支付 | Stripe(前端集成) | Stripe Webhook(后端) |
| 状态管理 | Zustand | — |
| 认证 | NextAuth.js | JWT 验证中间件 |
| 部署 | Vercel | Railway |
| 目标耗时 | 25小时 | 传统方式80小时 |
1.2 核心功能清单
- 商品模块:商品列表、详情、搜索、分类筛选、库存管理
- 购物车模块:加入/删除商品、数量修改、本地持久化
- 订单模块:下单、支付(Stripe)、订单状态追踪
- 用户模块:注册/登录、个人中心、历史订单
- 管理后台:商品管理、订单管理、数据统计仪表盘
- 基础设施:图片上传(Cloudinary)、邮件通知、错误监控
1.3 系统架构图

第二部分:完整目录结构
2.1 Monorepo 结构
全栈项目采用 Monorepo 组织,前后端在同一个仓库中管理:
ecommerce/
├── frontend/ # Next.js 前端
│ ├── app/
│ │ ├── (shop)/ # 商城页面组
│ │ │ ├── page.tsx # 首页 / 商品列表
│ │ │ ├── product/[id]/ # 商品详情
│ │ │ └── cart/ # 购物车
│ │ ├── (auth)/ # 认证页面组
│ │ │ ├── login/
│ │ │ └── register/
│ │ ├── dashboard/ # 管理后台
│ │ │ ├── products/
│ │ │ └── orders/
│ │ ├── api/ # Next.js API Routes
│ │ │ └── auth/[...nextauth]/
│ │ └── layout.tsx
│ ├── components/
│ │ ├── ui/ # shadcn/ui 基础组件
│ │ ├── product/ # 商品相关组件
│ │ └── cart/ # 购物车组件
│ ├── store/ # Zustand 状态管理
│ │ └── cartStore.ts
│ ├── lib/
│ │ ├── api.ts # API 请求封装
│ │ └── stripe.ts # Stripe 客户端
│ └── types/ # 共享类型定义
│ └── index.ts
│
├── backend/ # Express 后端
│ ├── src/
│ │ ├── models/ # Mongoose 模型
│ │ │ ├── Product.ts
│ │ │ ├── Order.ts
│ │ │ └── User.ts
│ │ ├── routes/ # API 路由
│ │ │ ├── products.ts
│ │ │ ├── orders.ts
│ │ │ └── users.ts
│ │ ├── middleware/
│ │ │ ├── auth.ts # JWT 验证
│ │ │ └── errorHandler.ts
│ │ ├── services/
│ │ │ ├── stripeService.ts
│ │ │ └── emailService.ts
│ │ └── app.ts
│ └── package.json
│
├── shared/ # 前后端共享
│ └── types/
│ └── index.ts # 统一类型定义
│
└── package.json # 根 package.json(workspaces)
第三部分:关键代码详解
3.1 共享类型定义(解决前后端对齐问题)
全栈最大的痛点是前后端类型不一致。解决方案:在 shared/ 目录统一定义,前后端都引用同一份。
// shared/types/index.ts
// ── 商品 ──
export interface Product {
_id: string
name: string
description: string
price: number
stock: number
images: string[]
category: string
tags: string[]
createdAt: string
}
export interface ProductQuery {
page?: number
limit?: number
category?: string
minPrice?: number
maxPrice?: number
search?: string
}
// ── 购物车 ──
export interface CartItem {
product: Product
quantity: number
}
// ── 订单 ──
export type OrderStatus = 'pending' | 'paid' | 'shipped' | 'delivered' | 'cancelled'
export interface Order {
_id: string
userId: string
items: CartItem[]
totalAmount: number
status: OrderStatus
stripePaymentId?: string
shippingAddress: Address
createdAt: string
}
// ── 地址 ──
export interface Address {
street: string
city: string
province: string
zipCode: string
country: string
}
// ── API响应 ──
export interface ApiResponse<T> {
success: boolean
data: T
message?: string
}
export interface PaginatedResponse<T> extends ApiResponse<T[]> {
total: number
page: number
totalPages: number
}
3.2 Zustand 购物车状态管理
// frontend/store/cartStore.ts
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
import type { CartItem, Product } from '../../shared/types'
interface CartStore {
items: CartItem[]
totalItems: number
totalAmount: number
addItem: (product: Product, quantity?: number) => void
removeItem: (productId: string) => void
updateQty: (productId: string, quantity: number) => void
clearCart: () => void
}
export const useCartStore = create<CartStore>()(
persist(
(set, get) => ({
items: [],
totalItems: 0,
totalAmount: 0,
addItem: (product, quantity = 1) => {
const existing = get().items.find(i => i.product._id === product._id)
const newItems = existing
? get().items.map(i =>
i.product._id === product._id
? { ...i, quantity: i.quantity + quantity }
: i
)
: [...get().items, { product, quantity }]
set({
items: newItems,
totalItems: newItems.reduce((sum, i) => sum + i.quantity, 0),
totalAmount: newItems.reduce((sum, i) => sum + i.product.price * i.quantity, 0),
})
},
removeItem: (productId) => {
const newItems = get().items.filter(i => i.product._id !== productId)
set({
items: newItems,
totalItems: newItems.reduce((sum, i) => sum + i.quantity, 0),
totalAmount: newItems.reduce((sum, i) => sum + i.product.price * i.quantity, 0),
})
},
updateQty: (productId, quantity) => {
if (quantity <= 0) { get().removeItem(productId); return }
set({ items: get().items.map(i =>
i.product._id === productId ? { ...i, quantity } : i
)})
},
clearCart: () => set({ items: [], totalItems: 0, totalAmount: 0 }),
}),
{ name: 'cart-storage' } // 自动持久化到 localStorage
)
)
3.3 Stripe 支付集成(最关键部分)
前端:创建支付会话
// frontend/app/(shop)/checkout/page.tsx
import { loadStripe } from '@stripe/stripe-js'
import { useCartStore } from '@/store/cartStore'
const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_KEY!)
export default function CheckoutPage() {
const { items, totalAmount } = useCartStore()
const handlePayment = async () => {
const stripe = await stripePromise
if (!stripe) return
// 调用后端创建 Stripe Session
const res = await fetch('/api/create-checkout-session', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ items }),
})
const { sessionId } = await res.json()
// 跳转到 Stripe 支付页
await stripe.redirectToCheckout({ sessionId })
}
return (
<div className='max-w-lg mx-auto p-8'>
<h1 className='text-2xl font-bold mb-6'>确认订单</h1>
<p className='text-lg mb-4'>总计:¥{totalAmount.toFixed(2)}</p>
<button
onClick={handlePayment}
className='w-full bg-blue-600 text-white py-3 rounded-lg font-bold'
>
前往支付
</button>
</div>
)
}
后端:处理支付 Webhook
// backend/src/routes/webhook.ts
import Stripe from 'stripe'
import { Order } from '../models/Order'
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
export async function handleWebhook(req: Request, res: Response) {
const sig = req.headers['stripe-signature'] as string
let event: Stripe.Event
try {
// 验证 Webhook 签名(防止伪造)
event = stripe.webhooks.constructEvent(
req.body, // 原始 Buffer
sig,
process.env.STRIPE_WEBHOOK_SECRET!
)
} catch (err) {
return res.status(400).send('Webhook 签名验证失败')
}
// 处理支付成功事件
if (event.type === 'checkout.session.completed') {
const session = event.data.object as Stripe.Checkout.Session
await Order.findOneAndUpdate(
{ stripeSessionId: session.id },
{
status: 'paid',
stripePaymentId: session.payment_intent as string,
paidAt: new Date(),
}
)
// 触发发货邮件通知(异步,不阻塞响应)
sendOrderConfirmationEmail(session.customer_email!).catch(console.error)
}
res.json({ received: true })
}
3.4 Next.js 商品列表页(服务端渲染)
// frontend/app/(shop)/page.tsx
import ProductCard from '@/components/product/ProductCard'
import type { Product, ProductQuery } from '../../shared/types'
// Next.js 服务端组件 - 直接在服务端获取数据
export default async function ShopPage({
searchParams
}: {
searchParams: ProductQuery
}) {
const params = new URLSearchParams({
page: String(searchParams.page ?? 1),
limit: String(searchParams.limit ?? 12),
category: searchParams.category ?? '',
search: searchParams.search ?? '',
})
// 服务端直接请求后端 API
const res = await fetch(
`${process.env.BACKEND_URL}/api/products?${params}`,
{ next: { revalidate: 60 } } // ISR:每60秒重新验证缓存
)
const { data: products, total, totalPages } = await res.json()
return (
<main className='container mx-auto px-4 py-8'>
<h1 className='text-3xl font-bold mb-8'>全部商品</h1>
{/* 商品网格 */}
<div className='grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6'>
{products.map((product: Product) => (
<ProductCard key={product._id} product={product} />
))}
</div>
{/* 分页信息 */}
<p className='text-center mt-8 text-gray-600'>
共 {total} 件商品,第 {searchParams.page ?? 1} / {totalPages} 页
</p>
</main>
)
}
第四部分:全栈协作的核心挑战
4.1 三大典型挑战与解决方案
挑战1:前后端类型不同步
❌ 问题:前端写的 product.price(number)
后端返回的 price: '199.00'(string)
→ 结算时总价计算错误
✅ 解决:统一在 shared/types/index.ts 定义 Product 接口
前后端都引用同一份,TypeScript 编译时捕获不一致
挑战2:Stripe Webhook 必须接收原始 Body
❌ 问题:Express 全局使用了 express.json() 解析请求体
Stripe 签名验证需要原始 Buffer,JSON解析后验证失败
✅ 解决:Webhook 路由单独配置,跳过全局 JSON 中间件
// app.ts
app.post('/webhook', express.raw({ type: 'application/json' }), handleWebhook)
app.use(express.json()) // 其余路由正常使用 JSON 解析
挑战3:Next.js App Router 的缓存策略
❌ 问题:用户下单后,商品库存已减少
但商品列表还显示旧库存(Next.js 缓存未更新)
✅ 解决:下单成功后主动触发缓存重验证
// 下单 API Route
import { revalidatePath } from 'next/cache'
export async function POST(req: Request) {
// ... 创建订单逻辑 ...
revalidatePath('/') // 首页商品列表
revalidatePath('/product/[id]') // 对应商品详情页
return Response.json({ success: true })
}
4.2 Claude Code 处理全栈的提示词策略
全栈开发中,提示词需要同时说清楚前端和后端的期望:
提示词示例(生成商品创建功能):
"实现电商平台的'创建商品'功能,要求:
【后端】Express + TypeScript
- POST /api/products 路由
- 需要管理员 JWT 验证
- 支持图片上传(Cloudinary)
- Mongoose 模型中库存必须 >= 0
- 返回格式遵循 ApiResponse<Product>(shared/types)
【前端】Next.js + TypeScript
- 管理后台表单页面 /dashboard/products/new
- 字段:名称、描述、价格、库存、分类、图片上传
- 使用 react-hook-form 管理表单
- 上传成功后跳转到商品列表页
- 错误状态需要友好提示
类型从 shared/types/index.ts 引用,不要重复定义"
第五部分:最终数据与效果
5.1 开发耗时明细
| 模块 | Claude Code | 手写传统 | 节省 | 备注 |
| 架构设计 + 目录 | 30分钟 | 180分钟 | ⬇ 83% | Monorepo |
| 共享类型定义 | 20分钟 | 60分钟 | ⬇ 67% | |
| 后端 API | 90分钟 | 300分钟 | ⬇ 70% | 4个路由模块 |
| 前端页面 | 120分钟 | 360分钟 | ⬇ 67% | 6个主要页面 |
| Stripe集成 | 45分钟 | 180分钟 | ⬇ 75% | 含Webhook |
| 状态管理 | 30分钟 | 90分钟 | ⬇ 67% | Zustand |
| Docker + 部署 | 30分钟 | 120分钟 | ⬇ 75% | |
| 调试与修复 | 135分钟 | 510分钟 | ⬇ 74% | 人工主导 |
| 合计 | 500分钟(≈25h) | 1800分钟(≈80h) | ⬇ 72% |
5.2 上线效果指标
| 指标 | Claude Code版 | 手写版 | 评价 |
| Lighthouse 性能分 | 91 | 94 | 略低 |
| 首页加载时间(LCP) | 1.4秒 | 1.2秒 | 相当 |
| API平均响应时间 | 42ms | 38ms | 相当 |
| TypeScript覆盖率 | 100% | 78% | Code大优 |
| 测试覆盖率 | 76% | 55% | Code更优 |
| 代码复用率 | 41%(共享类型) | 18% | Code大优 |
第六部分:系列总结与最终建议
6.1 五篇实战的核心数据汇总
| 篇章 | 项目类型 | Code耗时 | 传统耗时 | 节省 |
| 第3篇 | Web前端 | 12小时 | 40小时 | 70% |
| 第4篇 | Python后端 | 16小时 | 50小时 | 68% |
| 第5篇 | 全栈项目 | 25小时 | 80小时 | 69% |
| 平均 | — | 17.7小时 | 56.7小时 | 69% |
6.2 最终使用建议
经过5篇的系统测评,我得出一个清晰的结论:
- 原型和新项目:Claude Code 是第一选择,节省 70% 时间
- 已有项目维护:传统 IDE 更稳,Claude Code 辅助生成特定模块
- 学习阶段:先用 IDE 手写建立理解,再用 Claude Code 提速
- 团队协作:Claude Code 生成框架,人工审查合并,建立提示词模板库
- 质量保证:Claude Code 的类型和文档往往比手写更完整,但需要人工把关逻辑
6.3 Claude Code 的边界
有一些事情 Claude Code 目前还做不好,需要你来补:
- 业务逻辑设计:数据库表结构、API 设计方案,这些需要你决策
- 性能优化:索引选择、缓存策略、SQL 优化,需要你主导
- 安全审查:生成的代码要过安全检查,不能无脑上线
- 复杂算法:涉及特定业务规则的复杂逻辑,还是手写更可控
感谢你跟完这 5 篇系列。如果这个系列对你有帮助,欢迎评论区分享你用 Claude Code 的实战经验!
更多推荐
所有评论(0)