前后端联动实现指南
Table of Contents
项目概览
本项目实现了一个前后端分离的用户管理系统,演示了如何使用 NestJS 和 React 进行前后端联动开发。
功能特性:
- 用户 CRUD 操作(创建、查询、更新、删除)
- 前后端类型安全
- Vite 开发代理配置
- Prisma ORM 数据库操作
- RESTful API 设计
技术栈
后端 (db-connection)
- 框架: NestJS 11.x
- ORM: Prisma 5.x
- 数据库: MySQL
- 语言: TypeScript 5.x
- 运行时: Node.js
前端 (db-connect-front)
- 框架: React 19.x
- 构建工具: Vite 7.x
- 语言: TypeScript 5.x
- 包管理器: pnpm
项目结构
front-back-end/├── db-connection/ # 后端项目│ ├── src/│ │ ├── main.ts # 应用入口│ │ ├── app.module.ts # 根模块│ │ ├── prisma/ # Prisma 配置│ │ │ ├── prisma.service.ts│ │ │ └── prisma.module.ts│ │ └── user/ # 用户模块│ │ ├── user.controller.ts│ │ ├── user.service.ts│ │ ├── user.module.ts│ │ └── dto/│ │ ├── create-user.dto.ts│ │ └── update-user.dto.ts│ ├── prisma/│ │ └── schema.prisma # 数据库模型定义│ └── package.json│└── db-connect-front/ # 前端项目 ├── src/ │ ├── App.tsx # 主应用组件 │ └── main.tsx # 应用入口 ├── vite.config.ts # Vite 配置(代理设置) └── package.json后端实现
1. 初始化 NestJS 项目
# 创建 NestJS 项目npm i -g @nestjs/clinest new db-connection
# 进入项目目录cd db-connection
# 安装 Prismapnpm add @prisma/clientpnpm add -D prisma
# 初始化 Prismanpx prisma init2. 配置数据库连接
创建或编辑 .env 文件:
DATABASE_URL="mysql://username:password@localhost:3306/database_name"3. 定义数据库模型
prisma/schema.prisma
generator client { provider = "prisma-client-js"}
datasource db { provider = "mysql" url = env("DATABASE_URL")}
model User { id Int @id @default(autoincrement()) email String @unique name String? createdAt DateTime @default(now())}4. 创建 Prisma Service
src/prisma/prisma.service.ts
import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common';import { PrismaClient } from '@prisma/client';
@Injectable()export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy{ async onModuleInit(): Promise<void> { await this.$connect(); }
async onModuleDestroy(): Promise<void> { await this.$disconnect(); }}src/prisma/prisma.module.ts
import { Global, Module } from '@nestjs/common';import { PrismaService } from './prisma.service';
@Global()@Module({ providers: [PrismaService], exports: [PrismaService],})export class PrismaModule {}5. 生成 User 模块
# 使用 NestJS CLI 生成用户模块nest g resource user6. 定义 DTO(数据传输对象)
src/user/dto/create-user.dto.ts
export class CreateUserDto { email: string; name?: string;}src/user/dto/update-user.dto.ts
import { PartialType } from '@nestjs/mapped-types';import { CreateUserDto } from './create-user.dto';
export class UpdateUserDto extends PartialType(CreateUserDto) {}7. 实现 User Service
src/user/user.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';import { PrismaService } from '../prisma/prisma.service';import { CreateUserDto } from './dto/create-user.dto';import { UpdateUserDto } from './dto/update-user.dto';
@Injectable()export class UserService { constructor(private readonly prisma: PrismaService) {}
create(createUserDto: CreateUserDto) { return this.prisma.user.create({ data: createUserDto }); }
findAll() { return this.prisma.user.findMany({ orderBy: { id: 'asc' } }); }
async findOne(id: number) { const user = await this.prisma.user.findUnique({ where: { id } }); if (!user) { throw new NotFoundException(`User ${id} not found`); } return user; }
async update(id: number, updateUserDto: UpdateUserDto) { await this.findOne(id); return this.prisma.user.update({ where: { id }, data: updateUserDto, }); }
async remove(id: number) { await this.findOne(id); await this.prisma.user.delete({ where: { id } }); return { success: true }; }}8. 实现 User Controller
src/user/user.controller.ts
import { Controller, Get, Post, Body, Patch, Param, Delete,} from '@nestjs/common';import { UserService } from './user.service';import { CreateUserDto } from './dto/create-user.dto';import { UpdateUserDto } from './dto/update-user.dto';
@Controller('users')export class UserController { constructor(private readonly userService: UserService) {}
@Post() create(@Body() createUserDto: CreateUserDto) { return this.userService.create(createUserDto); }
@Get() findAll() { return this.userService.findAll(); }
@Get(':id') findOne(@Param('id') id: string) { return this.userService.findOne(+id); }
@Patch(':id') update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) { return this.userService.update(+id, updateUserDto); }
@Delete(':id') remove(@Param('id') id: string) { return this.userService.remove(+id); }}9. 注册模块
src/user/user.module.ts
import { Module } from '@nestjs/common';import { UserService } from './user.service';import { UserController } from './user.controller';
@Module({ controllers: [UserController], providers: [UserService],})export class UserModule {}src/app.module.ts
import { Module } from '@nestjs/common';import { AppController } from './app.controller';import { AppService } from './app.service';import { PrismaModule } from './prisma/prisma.module';import { UserModule } from './user/user.module';
@Module({ imports: [PrismaModule, UserModule], controllers: [AppController], providers: [AppService],})export class AppModule {}10. 配置应用入口
src/main.ts
import { NestFactory } from '@nestjs/core';import { AppModule } from './app.module';
async function bootstrap() { const app = await NestFactory.create(AppModule); await app.listen(process.env.PORT ?? 3000);}bootstrap();11. 同步数据库
# 生成 Prisma Clientnpx prisma generate
# 推送数据库模式(开发环境)npx prisma db push
# 或者使用迁移(生产环境推荐)npx prisma migrate dev --name init
# 打开 Prisma Studio 查看数据npx prisma studio前端实现
1. 初始化 React 项目
# 创建 Vite + React + TypeScript 项目pnpm create vite db-connect-front --template react-ts
# 进入项目目录cd db-connect-front
# 安装依赖pnpm install2. 配置 Vite 代理
vite.config.ts
import { defineConfig } from 'vite'import react from '@vitejs/plugin-react'
// https://vite.dev/config/export default defineConfig({ plugins: [react()], server: { proxy: { '/api': { target: 'http://localhost:3000', // 后端服务地址 changeOrigin: true, rewrite: (path) => path.replace(/^\/api/, ''), }, }, },})代理工作原理:
- 前端请求
/api/users会被代理到http://localhost:3000/users rewrite函数移除了/api前缀changeOrigin: true修改请求头的 origin
3. 创建用户表单组件
src/App.tsx
import { useState } from 'react'import type { FormEvent } from 'react'
interface CreateUserDto { email: string name?: string}
export default function App() { const [email, setEmail] = useState('') const [name, setName] = useState('') const [loading, setLoading] = useState(false) const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null)
const handleSubmit = async (e: FormEvent) => { e.preventDefault() setLoading(true) setMessage(null)
try { const userData: CreateUserDto = { email, ...(name && { name }) }
const response = await fetch('/api/users', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(userData), })
if (!response.ok) { throw new Error('创建用户失败') }
const result = await response.json() setMessage({ type: 'success', text: '用户创建成功!' }) setEmail('') setName('') console.log('创建的用户:', result) } catch (error) { setMessage({ type: 'error', text: error instanceof Error ? error.message : '创建用户时发生错误' }) } finally { setLoading(false) } }
return ( <div style={{ maxWidth: '500px', margin: '50px auto', padding: '20px' }}> <h1>创建用户</h1>
<form onSubmit={handleSubmit} style={{ display: 'flex', flexDirection: 'column', gap: '15px' }}> <div> <label htmlFor="email" style={{ display: 'block', marginBottom: '5px' }}> 邮箱 <span style={{ color: 'red' }}>*</span> </label> <input id="email" type="email" value={email} onChange={(e) => setEmail(e.target.value)} required style={{ width: '100%', padding: '8px', fontSize: '14px', border: '1px solid #ccc', borderRadius: '4px' }} placeholder="请输入邮箱" /> </div>
<div> <label htmlFor="name" style={{ display: 'block', marginBottom: '5px' }}> 姓名 </label> <input id="name" type="text" value={name} onChange={(e) => setName(e.target.value)} style={{ width: '100%', padding: '8px', fontSize: '14px', border: '1px solid #ccc', borderRadius: '4px' }} placeholder="请输入姓名(可选)" /> </div>
<button type="submit" disabled={loading} style={{ padding: '10px', fontSize: '16px', backgroundColor: loading ? '#ccc' : '#007bff', color: 'white', border: 'none', borderRadius: '4px', cursor: loading ? 'not-allowed' : 'pointer' }} > {loading ? '创建中...' : '创建用户'} </button> </form>
{message && ( <div style={{ marginTop: '20px', padding: '10px', borderRadius: '4px', backgroundColor: message.type === 'success' ? '#d4edda' : '#f8d7da', color: message.type === 'success' ? '#155724' : '#721c24', border: `1px solid ${message.type === 'success' ? '#c3e6cb' : '#f5c6cb'}` }} > {message.text} </div> )} </div> )}前后端联动配置
关键配置点
1. 后端端口配置
后端默认运行在 http://localhost:3000(src/main.ts:6
)
2. 前端代理配置
Vite 配置文件中设置代理规则(vite.config.ts:8-13 ):
proxy: { '/api': { target: 'http://localhost:3000', changeOrigin: true, rewrite: (path) => path.replace(/^\/api/, ''), },}3. 前端 API 调用
前端统一使用 /api 前缀调用后端接口:
fetch('/api/users', { method: 'POST', ... })数据流转过程
[前端页面] ↓ 用户提交表单[fetch('/api/users')] ↓ Vite 代理拦截[重写为 http://localhost:3000/users] ↓ 发送 HTTP 请求[NestJS UserController @Post()] ↓ 调用 Service[UserService.create()] ↓ Prisma ORM[MySQL 数据库] ↓ 返回创建的用户[响应返回前端] ↓ 显示成功消息[用户界面更新]运行项目
1. 启动后端
cd db-connection
# 首次运行需要配置数据库# 1. 编辑 .env 文件配置 DATABASE_URL# 2. 生成 Prisma Clientpnpm prisma:generate
# 3. 同步数据库结构pnpm prisma:push
# 启动开发服务器pnpm start:dev
# 服务将运行在 http://localhost:3000后端可用脚本:
pnpm start:dev # 开发模式(热重载)pnpm start:prod # 生产模式pnpm prisma:generate # 生成 Prisma Clientpnpm prisma:push # 推送数据库架构pnpm prisma:studio # 打开 Prisma Studiopnpm build # 构建项目2. 启动前端
# 在新的终端窗口cd db-connect-front
# 安装依赖(如果还没有)pnpm install
# 启动开发服务器pnpm dev
# 服务将运行在 http://localhost:5173前端可用脚本:
pnpm dev # 开发模式pnpm build # 构建生产版本pnpm preview # 预览生产构建pnpm lint # 运行 ESLint3. 测试前后端联动
- 确保后端运行在
http://localhost:3000 - 确保前端运行在
http://localhost:5173 - 打开浏览器访问
http://localhost:5173 - 填写表单提交,观察控制台和数据库
API 接口文档
基础 URL
- 开发环境:
http://localhost:3000 - 前端代理:
/api(会被重写到后端)
用户接口
1. 创建用户
POST /usersContent-Type: application/json
{ "email": "user@example.com", "name": "张三" // 可选}响应示例:
{ "id": 1, "email": "user@example.com", "name": "张三", "createdAt": "2025-12-29T10:00:00.000Z"}2. 获取所有用户
GET /users响应示例:
[ { "id": 1, "email": "user@example.com", "name": "张三", "createdAt": "2025-12-29T10:00:00.000Z" }]3. 获取单个用户
GET /users/:id响应示例:
{ "id": 1, "email": "user@example.com", "name": "张三", "createdAt": "2025-12-29T10:00:00.000Z"}4. 更新用户
PATCH /users/:idContent-Type: application/json
{ "email": "newemail@example.com", "name": "李四"}5. 删除用户
DELETE /users/:id响应示例:
{ "success": true}完整代码示例
前端调用所有接口示例
// 创建用户const createUser = async (email: string, name?: string) => { const response = await fetch('/api/users', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, name }), }) return response.json()}
// 获取所有用户const getAllUsers = async () => { const response = await fetch('/api/users') return response.json()}
// 获取单个用户const getUserById = async (id: number) => { const response = await fetch(`/api/users/${id}`) return response.json()}
// 更新用户const updateUser = async (id: number, data: { email?: string; name?: string }) => { const response = await fetch(`/api/users/${id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }) return response.json()}
// 删除用户const deleteUser = async (id: number) => { const response = await fetch(`/api/users/${id}`, { method: 'DELETE', }) return response.json()}后端完整的 package.json
{ "name": "db-connection", "version": "0.0.1", "scripts": { "build": "nest build", "prisma:generate": "prisma generate", "prisma:push": "prisma db push", "prisma:studio": "prisma studio", "start": "nest start", "start:dev": "nest start --watch", "start:prod": "node dist/main" }, "dependencies": { "@nestjs/common": "^11.0.1", "@nestjs/core": "^11.0.1", "@nestjs/mapped-types": "*", "@nestjs/platform-express": "^11.0.1", "@prisma/client": "^5.21.1", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1" }, "devDependencies": { "@nestjs/cli": "^11.0.0", "@nestjs/schematics": "^11.0.0", "prisma": "^5.21.1", "typescript": "^5.7.3" }}常见问题
1. 前端请求后端失败(CORS 错误)
问题: 控制台显示 CORS 错误
解决方案:
- 确保 Vite 代理配置正确
- 或者在后端启用 CORS:
const app = await NestFactory.create(AppModule);app.enableCors(); // 启用 CORSawait app.listen(3000);2. Prisma Client 未生成
问题: 导入 @prisma/client 报错
解决方案:
cd db-connectionnpx prisma generate3. 数据库连接失败
问题: Prisma 无法连接数据库
解决方案:
- 检查
.env文件中的DATABASE_URL是否正确 - 确保 MySQL 服务已启动
- 确保数据库已创建
# 创建数据库(MySQL)mysql -u root -pCREATE DATABASE database_name;4. 端口冲突
问题: 端口 3000 或 5173 已被占用
解决方案:
# 后端修改端口(main.ts)await app.listen(3001);
# 前端修改端口(vite.config.ts)server: { port: 5174, proxy: { ... }}5. Vite 代理不生效
问题: 前端无法通过代理访问后端
检查清单:
- ✓ 后端是否已启动
- ✓ Vite dev server 是否已重启(修改配置后需要重启)
- ✓ 请求路径是否以
/api开头 - ✓
rewrite函数是否正确移除前缀
6. TypeScript 类型错误
问题: 类型不匹配或找不到类型
解决方案:
# 后端pnpm prisma generate # 重新生成 Prisma 类型
# 前端# 确保 tsconfig.json 配置正确开发最佳实践
1. 环境变量管理
# 创建 .env.example 作为模板DATABASE_URL="mysql://user:password@localhost:3306/dbname"PORT=3000
# .env 添加到 .gitignoreecho ".env" >> .gitignore2. 数据库迁移
# 开发环境使用 db push(快速迭代)pnpm prisma:push
# 生产环境使用迁移(版本控制)npx prisma migrate dev --name add_user_modelnpx prisma migrate deploy # 生产部署3. 错误处理
// 前端统一错误处理const handleApiError = (error: unknown) => { if (error instanceof Error) { console.error('API Error:', error.message) } // 显示用户友好的错误消息}
// 后端全局异常过滤器// 参考:https://docs.nestjs.com/exception-filters4. 类型共享(高级)
可以考虑创建共享的类型定义包,避免前后端类型重复定义:
# 创建共享包mkdir shared-types# 定义 User、CreateUserDto 等类型# 前后端都引用这个包调试技巧
后端调试
# 查看详细日志pnpm start:dev
# 使用 Prisma Studio 查看数据pnpm prisma:studio前端调试
// 在浏览器控制台查看网络请求// Network Tab -> 查看请求和响应
// 添加调试日志console.log('发送的数据:', userData)console.log('响应结果:', result)数据库调试
# 查看生成的 SQLnpx prisma studio
# 或直接连接数据库mysql -u root -pUSE database_name;SELECT * FROM User;总结
本文档详细介绍了如何使用 NestJS + Prisma + React + Vite 实现前后端分离项目。
核心知识点:
- NestJS 模块化架构(Controller、Service、Module)
- Prisma ORM 数据库操作
- Vite 开发代理配置
- RESTful API 设计
- TypeScript 类型安全
关键配置:
- 后端运行在
localhost:3000 - 前端运行在
localhost:5173 - Vite 代理
/api/*→http://localhost:3000/*
通过这个项目,你已掌握了现代前后端分离开发的核心技能。可以在此基础上扩展更多功能,如身份认证、文件上传、WebSocket 等。
文档创建时间: 2025-12-29 技术栈版本: NestJS 11.x | React 19.x | Prisma 5.x | Vite 7.x
评论 (0)
请先登录后再发表评论