项目模板与最佳实践总结
启动一个新的 Next.js SaaS 项目,从零配置到第一个功能上线,需要做几十个决策:目录结构怎么组织、技术栈怎么选、代码规范怎么定、CI/CD 怎么配。本章把这些决策整理成一个可复用的项目模板——你可以直接照着搭,也可以作为 Checklist 验证已有项目。
1. 项目模板
1.1 技术栈选择
经过前面 90 章的实践验证,推荐的 SaaS 技术栈:
| 层级 | 技术 | 理由 |
|---|---|---|
| 框架 | Next.js 15 (App Router) | 全栈、SSR/SSG/ISR、Server Actions |
| 语言 | TypeScript (strict) | 类型安全,减少运行时错误 |
| 样式 | Tailwind CSS v4 | 原子化、JIT、无 CSS 文件管理负担 |
| UI 组件 | shadcn/ui | 可定制、不锁定、复制即拥有 |
| 图标 | Lucide React | 轻量、统一、Tree-shaking |
| 数据库 | PostgreSQL + Drizzle ORM | 类型安全 ORM、迁移、性能 |
| 认证 | Clerk / NextAuth.js | Clerk 开箱即用、NextAuth 灵活 |
| 支付 | Stripe | 行业标准、Webhook 完善 |
| 邮件 | Resend + React Email | JSX 写邮件模板 |
| 文件存储 | S3 / UploadThing | 按需选择 |
| 部署 | Vercel / Docker | Vercel 零配置、Docker 自主可控 |
| 监控 | Sentry | 错误追踪 + 性能监控 |
| AI | Vercel AI SDK | 流式、多模型、Tool Calling |
1.2 目录结构
nextsaas/
├── app/ # Next.js App Router
│ ├── (marketing)/ # 营销页面(公共布局)
│ │ ├── layout.tsx
│ │ ├── page.tsx # 首页
│ │ ├── pricing/page.tsx
│ │ └── blog/[slug]/page.tsx
│ ├── (auth)/ # 认证页面
│ │ ├── layout.tsx
│ │ ├── login/page.tsx
│ │ └── register/page.tsx
│ ├── (dashboard)/ # 后台页面(需认证)
│ │ ├── layout.tsx
│ │ ├── page.tsx # Dashboard 首页
│ │ ├── projects/
│ │ │ ├── page.tsx # 项目列表
│ │ │ ├── [id]/page.tsx # 项目详情
│ │ │ └── new/page.tsx # 新建项目
│ │ ├── settings/
│ │ │ ├── page.tsx # 通用设置
│ │ │ ├── billing/page.tsx # 计费
│ │ │ └── members/page.tsx # 成员管理
│ │ └── admin/ # 超级管理员
│ │ └── page.tsx
│ ├── api/ # API Routes(仅 Webhook 等)
│ │ ├── stripe/webhook/route.ts
│ │ └── clerk/webhook/route.ts
│ ├── layout.tsx # 根布局
│ ├── not-found.tsx
│ ├── error.tsx
│ └── loading.tsx
├── components/ # 共享组件
│ ├── ui/ # shadcn/ui 基础组件
│ ├── forms/ # 表单组件
│ ├── layouts/ # 布局组件(Sidebar, Header)
│ └── shared/ # 业务共享组件
├── lib/ # 核心逻辑
│ ├── db/
│ │ ├── schema.ts # Drizzle schema
│ │ ├── index.ts # 数据库连接
│ │ └── migrations/ # 迁移文件
│ ├── actions/ # Server Actions
│ │ ├── project.ts
│ │ ├── user.ts
│ │ └── subscription.ts
│ ├── auth/ # 认证相关
│ │ └── index.ts
│ ├── email/ # 邮件模板
│ ├── stripe/ # 支付相关
│ └── utils.ts # 工具函数
├── public/ # 静态资源
├── .cursorrules # AI 工具规则
├── CLAUDE.md # Claude Code 规则
├── drizzle.config.ts
├── middleware.ts
├── next.config.ts
├── tailwind.config.ts
├── tsconfig.json
└── package.json
1.3 关键文件模板
package.json scripts:
{
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
"lint": "next lint",
"type-check": "tsc --noEmit",
"db:generate": "drizzle-kit generate",
"db:push": "drizzle-kit push",
"db:studio": "drizzle-kit studio",
"db:migrate": "drizzle-kit migrate",
"test": "vitest",
"test:e2e": "playwright test",
"format": "prettier --write ."
}
}tsconfig.json 关键配置:
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"paths": { "@/*": ["./*"] },
"plugins": [{ "name": "next" }]
}
}middleware.ts 模板:
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'
const isProtectedRoute = createRouteMatcher([
'/dashboard(.*)',
'/settings(.*)',
'/admin(.*)',
])
export default clerkMiddleware(async (auth, req) => {
if (isProtectedRoute(req)) await auth.protect()
})
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico|api/webhook).*)'],
}2. 技术栈 Checklist
2.1 新项目启动 Checklist
## 🚀 项目启动 Checklist
### 基础设施
- [ ] 创建 GitHub Repo
- [ ] 初始化 Next.js 项目 (create-next-app)
- [ ] 配置 TypeScript strict mode
- [ ] 安装 Tailwind CSS + shadcn/ui
- [ ] 配置路径别名 (@/*)
- [ ] 创建 .env.local 模板 (.env.example)
### 数据层
- [ ] 安装配置 Drizzle ORM
- [ ] 连接 PostgreSQL (Neon / Supabase / 本地)
- [ ] 创建基础 Schema (users, tenants)
- [ ] 运行首次迁移
### 认证
- [ ] 配置 Clerk / NextAuth
- [ ] 创建登录/注册页面
- [ ] 配置 middleware 路由保护
- [ ] 实现 Webhook 同步用户到数据库
### 布局
- [ ] 创建 (marketing) Route Group + 布局
- [ ] 创建 (dashboard) Route Group + 布局
- [ ] 实现 Sidebar + Header 组件
- [ ] 创建 loading.tsx / error.tsx / not-found.tsx
### 代码质量
- [ ] 配置 ESLint (next/core-web-vitals)
- [ ] 配置 Prettier
- [ ] 配置 Husky + lint-staged(预提交检查)
- [ ] 创建 .cursorrules / CLAUDE.md
### 部署
- [ ] 连接 Vercel / 配置 Docker
- [ ] 配置环境变量
- [ ] 配置域名
- [ ] 配置 Sentry 错误监控
### 支付(如需要)
- [ ] 注册 Stripe 账户
- [ ] 创建产品和价格
- [ ] 实现 Checkout 流程
- [ ] 配置 Webhook
### CI/CD
- [ ] GitHub Actions: lint + type-check + test
- [ ] Preview 部署(PR 自动部署预览)
- [ ] 生产部署(main 分支自动部署)2.2 功能开发 Checklist
每次开发新功能前过一遍:
## 📋 功能开发 Checklist
### 数据层
- [ ] Schema 设计完成(含索引)
- [ ] 迁移文件已生成并测试
- [ ] tenantId 字段和外键
### Server Actions
- [ ] 'use server' 声明
- [ ] Zod 输入验证
- [ ] 权限检查 (requirePermission)
- [ ] tenantId 查询过滤
- [ ] 错误处理 (try-catch, { error: string })
- [ ] revalidatePath 缓存失效
### 前端
- [ ] page.tsx 为 Server Component
- [ ] 交互部分抽成 Client Component
- [ ] loading.tsx Skeleton
- [ ] error.tsx 错误边界
- [ ] 表单 loading + error 状态
- [ ] 移动端响应式
### 测试
- [ ] 单元测试覆盖 Server Actions
- [ ] 边界情况测试(空数据、权限不足、无效输入)3. 代码规范
3.1 文件命名规范
组件文件:kebab-case
components/project-card.tsx ✅
components/ProjectCard.tsx ❌
Server Actions:按领域分文件
lib/actions/project.ts ✅
lib/actions/projectActions.ts ❌
页面文件:Next.js 约定
app/(dashboard)/projects/page.tsx ✅
类型文件:与源文件同名或统一 types.ts
lib/types.ts ✅
lib/actions/project.types.ts ✅(复杂时拆分)
3.2 组件规范
// 1. Server Component(默认)
// 不加 'use client',直接 async 获取数据
export default async function ProjectsPage() {
const projects = await getProjects()
return <ProjectList projects={projects} />
}
// 2. Client Component(最小粒度)
// 只把需要交互的部分标记为 'use client'
'use client'
export function DeleteProjectButton({ id }: { id: string }) {
const [isPending, startTransition] = useTransition()
return (
<button
disabled={isPending}
onClick={() => startTransition(() => deleteProject(id))}
>
{isPending ? 'Deleting...' : 'Delete'}
</button>
)
}
// 3. Props 类型
// 简单 props 用 inline,复杂 props 用 type/interface
function Card({ title, children }: { title: string; children: React.ReactNode }) {}
interface ProjectFormProps {
project?: Project
onSuccess?: () => void
mode: 'create' | 'edit'
}
function ProjectForm({ project, onSuccess, mode }: ProjectFormProps) {}3.3 Server Action 规范
'use server'
import { z } from 'zod'
import { revalidatePath } from 'next/cache'
import { requirePermission } from '@/lib/auth'
// 1. Schema 定义在 Action 旁边
const createProjectSchema = z.object({
name: z.string().min(1, 'Name is required').max(100),
description: z.string().max(500).optional(),
})
// 2. 统一返回格式
type ActionResult<T = void> = { data?: T; error?: string }
// 3. Action 实现
export async function createProject(
formData: FormData
): Promise<ActionResult<{ id: string }>> {
try {
// 权限检查
const { tenantId } = await requirePermission('project:create')
// 输入验证
const parsed = createProjectSchema.safeParse({
name: formData.get('name'),
description: formData.get('description'),
})
if (!parsed.success) {
return { error: parsed.error.issues[0].message }
}
// 业务逻辑
const [project] = await db
.insert(projects)
.values({ ...parsed.data, tenantId })
.returning({ id: projects.id })
// 缓存失效
revalidatePath('/projects')
return { data: { id: project.id } }
} catch (e) {
return { error: e instanceof Error ? e.message : 'Something went wrong' }
}
}3.4 数据库查询规范
// 1. 永远带 tenantId
const projects = await db
.select()
.from(projectsTable)
.where(eq(projectsTable.tenantId, tenantId)) // ← 必须
.orderBy(desc(projectsTable.updatedAt))
.limit(20)
// 2. 不用 SELECT *(选需要的字段)
const projectNames = await db
.select({ id: projectsTable.id, name: projectsTable.name })
.from(projectsTable)
.where(eq(projectsTable.tenantId, tenantId))
// 3. 分页查询用 offset + limit
const PAGE_SIZE = 20
const data = await db
.select()
.from(projectsTable)
.where(eq(projectsTable.tenantId, tenantId))
.offset((page - 1) * PAGE_SIZE)
.limit(PAGE_SIZE)
// 4. 复杂查询用事务
await db.transaction(async (tx) => {
const [project] = await tx.insert(projectsTable).values(data).returning()
await tx.insert(auditLogsTable).values({ action: 'project.created', ... })
return project
})4. 工程结构
4.1 环境管理
.env.example # 提交到 Git,团队共享
.env.local # 本地开发(不提交)
.env.development # 开发环境默认值
.env.production # 生产环境默认值(不含密钥)
# .env.example
DATABASE_URL=postgresql://user:pass@localhost:5432/nextsaas
CLERK_SECRET_KEY=sk_test_xxx
STRIPE_SECRET_KEY=sk_test_xxx
NEXT_PUBLIC_APP_URL=http://localhost:3000
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_xxx4.2 Git 规范
# Commit Message(Conventional Commits)
feat(auth): add Google OAuth login
fix(billing): handle subscription cancellation edge case
refactor(db): migrate from Prisma to Drizzle
docs(readme): add deployment instructions
chore(deps): update Next.js to 15.1
test(actions): add unit tests for project CRUD
# 分支命名
feat/notification-system
fix/hydration-date-mismatch
refactor/auth-middleware4.3 CI/CD Pipeline
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
quality:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
- uses: actions/setup-node@v4
with: { node-version: 20, cache: pnpm }
- run: pnpm install --frozen-lockfile
- run: pnpm lint
- run: pnpm type-check
- run: pnpm test
- run: pnpm build4.4 AI 工具配置
项目中应包含 AI 工具的规则文件:
.cursorrules # Cursor 规则
CLAUDE.md # Claude Code 规则
.windsurf/rules # Windsurf 规则
.mcp.json # MCP Server 配置
这些文件确保 AI 理解你的项目约定,生成的代码一致且高质量。详见第 81 章。
5. 项目初始化脚本
# 一键初始化完整项目
pnpm create next-app nextsaas --typescript --tailwind --app --src-dir=false
cd nextsaas
# 安装核心依赖
pnpm add drizzle-orm @neondatabase/serverless zod
pnpm add -D drizzle-kit
# 安装 UI 依赖
pnpm dlx shadcn@latest init
pnpm dlx shadcn@latest add button input card dialog form toast
# 安装认证
pnpm add @clerk/nextjs
# 安装工具
pnpm add lucide-react clsx tailwind-merge date-fns
pnpm add -D prettier eslint-config-prettier
# 安装测试
pnpm add -D vitest @testing-library/react playwright
# 创建目录结构
mkdir -p lib/{db,actions,auth,email,stripe}
mkdir -p components/{ui,forms,layouts,shared}
mkdir -p app/{\"(marketing)\",\"(auth)\",\"(dashboard)\"/projects,api/stripe/webhook}本章小结
- 技术栈:Next.js 15 + TypeScript + Tailwind + shadcn/ui + Drizzle + Clerk + Stripe
- 目录结构:Route Group 分区(marketing/auth/dashboard)、lib 按领域分模块
- 启动 Checklist:基础设施 → 数据层 → 认证 → 布局 → 代码质量 → 部署 → CI/CD
- 功能 Checklist:Schema → Action → 前端 → 测试,每步有明确验证点
- 代码规范:Server Component 默认、Client Component 最小粒度、Action 统一格式
- 查询规范:必带 tenantId、选需要的字段、分页、复杂操作用事务
- AI 配置:.cursorrules + CLAUDE.md + .mcp.json 确保 AI 理解项目约定