项目模板与最佳实践总结

启动一个新的 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.jsClerk 开箱即用、NextAuth 灵活
支付Stripe行业标准、Webhook 完善
邮件Resend + React EmailJSX 写邮件模板
文件存储S3 / UploadThing按需选择
部署Vercel / DockerVercel 零配置、Docker 自主可控
监控Sentry错误追踪 + 性能监控
AIVercel 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_xxx

4.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-middleware

4.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 build

4.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 理解项目约定