
1. 项目概述一个面向2025年的现代全栈开发样板如果你和我一样在过去几年里不断尝试各种技术栈组合试图找到一个既类型安全、开发体验流畅又易于维护和扩展的全栈方案那么你很可能已经对“选择困难症”深有体会。React生态的碎片化、后端API的类型安全与前端脱节、数据库迁移的繁琐、以及日益复杂的构建工具链常常让一个本应专注于业务逻辑的项目在初期就陷入技术选型的泥潭。Ians Stack (2025 Edition) 正是为了解决这个问题而生的——它是一个精心挑选、深度整合的现代全栈Web应用示例或者说一个可以直接“克隆即用”的现代化开发起点。这个项目的核心目标非常明确为构建生产级、类型安全的Web应用提供一个“固执己见”但高度灵活的最佳实践集合。它不是什么颠覆性的新框架而是将当前2025年社区中经过验证的、优秀的工具以一种合理的方式组合在一起。从使用TypeScript确保前后端类型一致到通过tRPC和TanStack Query实现端到端的类型安全API调用从用Kysely处理类型安全的SQL查询和迁移到采用shadcn/ui和Tailwind CSS v4构建高性能且可维护的UI——每一个技术选型背后都有其深思熟虑的理由。这个项目特别适合那些已经熟悉JavaScript/TypeScript基础希望快速启动一个具备完整CI/CD、测试、容器化能力的新项目的开发者无论是个人项目、创业原型还是内部工具都能从中获得一个坚实的脚手架。2. 技术栈深度解析与选型逻辑2.1 核心架构类型安全贯穿始终这个技术栈最鲜明的特点是对类型安全Type Safety的极致追求。这不仅仅是在代码里写上interface和type那么简单而是让类型信息在应用的每一个层级——数据库、后端业务逻辑、API层、前端状态管理乃至组件Props——都能无缝流动且被严格校验。为什么是TypeScript这似乎已成共识但关键在于“全栈同构”。项目前后端都使用TypeScript这意味着你可以共享类型定义例如一个User的类型减少重复代码和维护成本。更重要的是它为后续tRPC的端到端类型安全提供了语言基础。tRPC TanStack QueryAPI层的革命。这是替代传统REST和GraphQL的优雅方案。tRPC允许你像调用本地函数一样调用后端API并且函数的输入输出类型会被自动推断并同步到前端。TanStack Query则负责管理这些API调用的状态加载、成功、错误、缓存、重试。组合起来你不再需要手动编写API客户端、定义DTO数据传输对象、或者担心前后端字段名不一致。当后端修改了一个路由的返回类型时前端的TypeScript编译会立刻报错将运行时错误消灭在编译时。Kysely类型安全的SQL构建器。与重量级ORM如TypeORM、Sequelize不同Kysely是一个轻量级的查询构建器。它的优势在于纯类型安全基于你的数据库Schema生成TypeScript类型查询的select、where等条件都会进行严格的类型检查。贴近SQL你不会被ORM的抽象所困扰可以编写出高效、直观的SQL语句同时享受类型安全的好处。优秀的迁移工具内置的迁移系统简单可靠迁移文件就是普通的TypeScript/JavaScript易于理解和调试。注意很多开发者习惯了ORM的“对象式”操作初次接触Kysely可能需要适应其“查询构建器”模式。但一旦习惯你会发现在复杂查询和性能调优上它比ORM有更大的灵活性和可控性。2.2 前端与UI平衡性能与开发体验Next.js全栈React的默认选择但带有保留意见。项目使用了Next.js的App Router。Next.js提供了开箱即用的服务端渲染SSR、静态站点生成SSG、文件系统路由、图片优化等强大功能能极大提升应用性能和SEO。作者也直言不讳地指出了对Next.js日益复杂性的担忧并正在寻找替代品如Astro更适合内容型网站。但对于需要高度交互的动态应用Next.js目前仍是生态最完善的选择。shadcn/ui Tailwind CSS v4组件与样式的黄金组合。shadcn/ui不是一个通过npm install安装的组件库而是一套你可以“复制粘贴”到项目中的高质量组件代码。这种方式被称为“vendored”好处是零运行时开销组件是你代码的一部分没有额外的JavaScript bundle。完全可控你可以修改任何组件的源代码来满足定制化需求。与Tailwind深度集成样式完全由Tailwind CSS utility classes定义保证了极致的性能和一致性。 Tailwind CSS v4 在性能和开发体验上更进一步是构建自定义设计系统的绝佳基础。2.3 开发体验与质量保障pnpm高效的包管理器。相比npm和yarnpnpm通过硬链接和符号链接实现了更快的安装速度和更少的磁盘空间占用并且能严格保证依赖树的正确性避免“幽灵依赖”问题。Vitest超快的单元测试。作为Jest的替代品Vitest与Vite原生集成启动和热更新速度极快并且兼容Jest的大部分API迁移成本低。oxlint基于Rust的极速Linter。替代ESLint。在大型项目中ESLint的检查速度可能成为开发流程的瓶颈。oxlint用Rust重写速度提升了一个数量级能瞬间完成代码检查让“保存时自动lint”成为一种无感的愉悦体验。Playwright计划中可靠的E2E测试。虽然当前版本因与PGLite内存PostgreSQL的兼容性问题暂未集成但Playwright是跨浏览器、跨平台E2E测试的首选其强大的自动等待和录制功能能极大提升测试编写效率。3. 项目结构与核心模块实操指南3.1 目录结构深度解读项目的目录结构清晰地分离了关注点遵循了Next.js App Router的约定并强化了服务端逻辑的独立性。ian-stack-2025/ ├── src/ │ ├── app/ # Next.js 应用路由页面和API入口 │ ├── components/ # 可复用的React组件包括shadcn/ui组件 │ ├── lib/ # 前后端共享的工具函数和库如tRPC客户端初始化 │ └── server/ # **核心**所有服务端专属逻辑 │ ├── db/ # 数据库一切连接、迁移、类型生成 │ ├── models/ # 数据模型和业务逻辑如todos.ts │ └── trpc/ # tRPC路由定义和服务器配置关键设计清晰的“服务端”边界。将src/server/目录作为所有后端代码的容器是一个优秀实践。这明确告诉开发者和工具如打包器这里的代码永远不会被发送到客户端浏览器。你可以在这里安全地使用Node.js原生模块、数据库驱动、读写文件系统等。Next.js的构建过程会自动识别并正确处理这部分代码。3.2 数据库操作全流程从迁移到查询让我们以项目中自带的todos待办事项模型为例走一遍完整的数据库操作流程。第一步创建迁移。假设我们要为todos表添加一个priority优先级字段。在src/server/db/migrations/目录下创建一个新的迁移文件例如20250321_add_priority_to_todos.ts。Kysely的迁移文件需要导出up和down函数。// src/server/db/migrations/20250321_add_priority_to_todos.ts import { Kysely } from kysely export async function up(db: Kyselyany): Promisevoid { await db.schema .alterTable(todos) .addColumn(priority, integer, (col) col.defaultTo(0).notNull()) .execute() } export async function down(db: Kyselyany): Promisevoid { await db.schema.alterTable(todos).dropColumn(priority).execute() }第二步运行迁移并生成类型。pnpm db:latest这个命令会执行所有未应用的迁移更新你的物理数据库结构。pnpm db:codegen这个命令是关键。它会连接到数据库读取当前的Schema然后在src/server/db/types.ts中自动生成对应的TypeScript类型定义。现在todos表的类型就会包含priority字段。pnpm db:schema将当前数据库的Schema导出为SQL文件到docs/schema.sql。这个文件对于AI编程助手理解数据库结构非常有帮助。第三步在模型和API中使用。打开src/server/models/todos.ts你现在可以安全地使用带有priority字段的类型进行查询。// src/server/models/todos.ts import { db } from ../db // 引入数据库连接 import type { Database } from ../db/types // 引入自动生成的类型 // 插入一个带优先级的待办事项 export async function createTodo( data: PickDatabase[todos], title | priority ) { return await db .insertInto(todos) .values(data) .returningAll() .executeTakeFirstOrThrow() // 返回新创建的单条记录 } // 查询高优先级的待办事项 export async function getHighPriorityTodos() { return await db .selectFrom(todos) .selectAll() .where(priority, , 5) .orderBy(createdAt, desc) .execute() // 返回记录数组 }实操心得养成“修改数据库 → 运行迁移 → 生成类型”的肌肉记忆。这能保证你的TypeScript类型永远与数据库实际结构同步从根源上杜绝一大类“字段不存在”的运行时错误。3.3 tRPC路由与前端调用的无缝对接后端定义API前端像调用函数一样使用类型完全同步——这就是tRPC的魅力。后端路由定义 (src/server/trpc/routers/todos.ts):import { z } from zod // 用于输入验证 import { createTRPCRouter, publicProcedure } from ../trpc import * as todoModel from ../../../models/todos // 引入上面的模型 export const todoRouter createTRPCRouter({ // 查询所有待办事项 list: publicProcedure.query(async () { const todos await todoModel.listTodos() return todos }), // 创建待办事项带输入验证 create: publicProcedure .input( z.object({ title: z.string().min(1, 标题不能为空), priority: z.number().int().min(0).max(10).default(0), }) ) .mutation(async ({ input }) { const newTodo await todoModel.createTodo(input) return newTodo }), // 标记为完成 toggleComplete: publicProcedure .input(z.object({ id: z.number(), completed: z.boolean() })) .mutation(async ({ input }) { const updatedTodo await todoModel.updateTodo(input.id, { completed: input.completed, }) return updatedTodo }), })前端调用 (src/app/page.tsx或任何组件中):// 1. 引入tRPC客户端钩子 import { api } from ~/lib/trpc/client function TodoList() { // 2. 使用自动生成的钩子进行查询。todo.list对应后端的路由名。 // TypeScript会自动推断出返回值类型是 Todo[]。 const { data: todos, isLoading, error } api.todo.list.useQuery() // 3. 使用Mutation钩子进行数据修改。 const toggleMutation api.todo.toggleComplete.useMutation() const utils api.useUtils() // 用于在mutation后使缓存失效重新获取数据 const handleToggle async (id: number, completed: boolean) { await toggleMutation.mutateAsync({ id, completed }) // 操作成功后立即使todo.list查询的缓存失效触发重新获取 await utils.todo.list.invalidate() } if (isLoading) return div加载中.../div if (error) return div错误{error.message}/div return ( ul {todos?.map((todo) ( li key{todo.id} input typecheckbox checked{todo.completed} onChange{() handleToggle(todo.id, !todo.completed)} / {todo.title} (优先级: {todo.priority}) /li ))} /ul ) }魔法在哪里api.todo.list.useQuery()这个钩子是完全类型安全的。当你编写api.todo.时编辑器会自动补全所有在todoRouter中定义的路由list,create,toggleComplete。.useQuery()不需要你手动指定返回的数据类型因为它直接从后端路由的定义中推断出来。输入参数的类型也由Zod Schema严格约束。4. 开发、测试与部署工作流4.1 本地开发环境搭建详解安装依赖确保已安装Node.js 24和pnpm。运行pnpm install。pnpm会利用其独特的存储链接机制快速安装所有依赖。数据库准备推荐使用Dockerdocker run -p 5432:5432 -e POSTGRES_PASSWORDmysecretpassword postgres:16。这是最干净、可复现的方式。创建.env文件设置DATABASE_URLpostgresql://postgres:mysecretpasswordlocalhost:5432/postgres。初始化与运行pnpm db:latest应用数据库迁移创建表结构。pnpm dev启动Next.js开发服务器。默认在http://localhost:3000。4.2 自动化代码质量保障项目配置了一套强大的自动化工具链通过package.json中的脚本命令驱动pnpm everything这是你的“一键检查”命令。它会依次运行pnpm lint:fix用oxlint检查和修复代码风格问题。pnpm format:fix用Prettier重新格式化代码。pnpm type-check运行TypeScript编译器检查类型错误。pnpm test运行Vitest单元测试。强烈建议在提交代码前运行此命令或将其配置为Git的pre-commit钩子。pnpm lint/pnpm lint:fixoxlint的速度优势在这里体现得淋漓尽致。在大型代码库中它几乎能在你保存文件的瞬间完成检查毫无延迟感。pnpm test运行src/目录下所有以.test.ts或.spec.ts结尾的测试文件。Vitest的即时反馈和清晰的错误信息让测试驱动开发TDD变得可行。4.3 容器化与持续集成/持续部署 (CI/CD)Docker化项目的Dockerfile采用了多阶段构建以生成尽可能小的生产镜像。# 第一阶段依赖安装 FROM node:20-alpine AS deps WORKDIR /app COPY package.json pnpm-lock.yaml ./ RUN corepack enable pnpm pnpm install --frozen-lockfile # 第二阶段构建 FROM node:20-alpine AS builder WORKDIR /app COPY --fromdeps /app/node_modules ./node_modules COPY . . RUN corepack enable pnpm pnpm build # 第三阶段运行 FROM node:20-alpine AS runner WORKDIR /app ENV NODE_ENV production COPY --frombuilder /app/public ./public COPY --frombuilder /app/.next/standalone ./ COPY --frombuilder /app/.next/static ./.next/static EXPOSE 3000 ENV PORT 3000 CMD [node, server.js]这个配置利用了Next.js的output: standalone将应用打包成一个独立的Node.js服务无需安装node_modules极大地减小了镜像体积。GitHub Actions CI/CD项目预置了.github/workflows/下的YAML文件。tests.yml在每次推送或拉取请求时自动运行pnpm everything确保代码质量和测试通过。docker.yml在向主分支推送标签时自动构建Docker镜像并推送到GitHub Container Registry (GHCR)或Docker Hub。部署心得对于早期项目可以先用GitHub Actions构建镜像然后通过SSH命令在服务器上拉取并重启容器。当项目规模扩大后再考虑使用更成熟的平台如Railway、Fly.io或Kubernetes。这个栈的容器化非常友好迁移成本很低。5. 扩展指南与避坑实践5.1 如何为项目添加新功能模块假设我们要添加一个“用户评论”功能。数据库层在src/server/db/migrations/创建迁移文件定义comments表包含id,content,todo_id,user_id,created_at等字段。运行pnpm db:latest和pnpm db:codegen。模型层在src/server/models/下创建comments.ts编写创建、查询、删除评论的函数。API层在src/server/trpc/routers/下创建comments.ts定义createComment、getCommentsByTodo等路由。在src/server/trpc/routers/index.ts中导入并合并这个新的router。前端层在对应的页面组件中使用api.comment.getCommentsByTodo.useQuery({ todoId })和api.comment.create.useMutation()来调用API。创建src/components/comments/目录来存放评论相关的UI组件。整个过程中TypeScript会全程保驾护航。如果你在模型层查询了一个不存在的字段或者在API层传递了错误类型的参数编译器都会提前报错。5.2 集成第三方服务以身份认证为例项目推荐使用 Clerk 进行身份认证。集成步骤清晰安装并配置Clerk按照其文档在Clerk仪表板创建应用获取密钥并配置Next.js中间件。创建受保护的路由在src/middleware.ts中使用Clerk的中间件来保护特定页面。扩展tRPC上下文修改src/server/trpc/trpc.ts在创建tRPC上下文时将Clerk返回的auth对象注入进去。import { getAuth } from clerk/nextjs/server export const createContext async (opts: CreateNextContextOptions) { const { req } opts const auth getAuth(req) return { auth, // 现在可以在所有tRPC路由中通过 ctx.auth 访问用户信息 } }创建授权Procedure基于publicProcedure创建一个检查用户是否登录的protectedProcedure。import { TRPCError } from trpc/server export const protectedProcedure publicProcedure.use(({ ctx, next }) { if (!ctx.auth.userId) { throw new TRPCError({ code: UNAUTHORIZED }) } return next({ ctx: { ...ctx, auth: ctx.auth, // 此时TypeScript知道auth一定存在userId }, }) })在路由中使用将todoRouter中的create和toggleComplete等需要登录的路由从publicProcedure改为protectedProcedure。这样未登录用户调用这些API时会自动收到401错误。5.3 常见问题与排查技巧问题一运行pnpm db:latest时出现数据库连接错误。检查确保PostgreSQL服务正在运行psql -h localhost -U postgres测试连接。检查.env文件中的DATABASE_URL格式是否正确密码、主机名、端口是否匹配。解决对于Docker确保容器已启动且端口映射正确。问题二TypeScript报告“模块未找到”或类型错误但代码看起来没问题。检查首先运行pnpm type-check查看详细错误。可能原因pnpm db:codegen未运行导致数据库类型定义过期。运行它来重新生成src/server/db/types.ts。可能原因依赖未正确安装。尝试删除node_modules和pnpm-lock.yaml然后重新运行pnpm install。问题三tRPC前端调用成功但返回null或空数据。排查打开浏览器开发者工具的“网络”选项卡查看tRPC请求的实际响应。tRPC默认使用HTTP POST端点通常是/api/trpc/[procedure]。检查后端路由处理函数是否有正确的return语句。检查数据库查询是否真的返回了数据。可以在后端路由中临时添加console.log来调试。问题四Tailwind CSS样式未生效。检查确保组件中使用的样式类名在Tailwind的content配置中位于tailwind.config.ts被包含。项目通常配置为content: [‘./src/**/*.{js,ts,jsx,tsx,mdx}’]。检查是否在正确的文件中导入全局样式Next.js App Router中全局样式应在src/app/globals.css中导入。问题五如何调试一个复杂的数据库查询Kysely提供了.compile()方法可以输出最终生成的SQL语句和参数这对于调试非常有用。const query db.selectFrom(todos).selectAll().where(priority, , 5) const compiled query.compile() console.log(SQL:, compiled.sql) console.log(Parameters:, compiled.parameters)将输出的SQL复制到如pgAdmin或psql中直接运行可以验证查询逻辑是否正确。这个技术栈的威力在于其高度的集成性和自动化。一旦你熟悉了从数据库迁移到前端渲染的完整流程开发效率会有质的飞跃。它强制你遵循一系列最佳实践从而让代码库在项目增长时依然保持可维护性。虽然初始的学习曲线比简单的“Create React App Express”要陡峭一些但它为中型乃至大型应用所节省的调试时间和维护成本绝对是物超所值的投资。