前后端联动实现指南

15 min read
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 项目

bashTerminal window
# 创建 NestJS 项目
npm i -g @nestjs/cli
nest new db-connection
# 进入项目目录
cd db-connection
# 安装 Prisma
pnpm add @prisma/client
pnpm add -D prisma
# 初始化 Prisma
npx prisma init

2. 配置数据库连接

创建或编辑 .env 文件:

DATABASE_URL="mysql://username:password@localhost:3306/database_name"

3. 定义数据库模型

prisma/schema.prisma

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

typescript
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

typescript
import { Global, Module } from '@nestjs/common';
import { PrismaService } from './prisma.service';
@Global()
@Module({
providers: [PrismaService],
exports: [PrismaService],
})
export class PrismaModule {}

5. 生成 User 模块

bashTerminal window
# 使用 NestJS CLI 生成用户模块
nest g resource user

6. 定义 DTO(数据传输对象)

src/user/dto/create-user.dto.ts

typescript
export class CreateUserDto {
email: string;
name?: string;
}

src/user/dto/update-user.dto.ts

typescript
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

typescript
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

typescript
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

typescript
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

typescript
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

typescript
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. 同步数据库

bashTerminal window
# 生成 Prisma Client
npx prisma generate
# 推送数据库模式(开发环境)
npx prisma db push
# 或者使用迁移(生产环境推荐)
npx prisma migrate dev --name init
# 打开 Prisma Studio 查看数据
npx prisma studio

前端实现

1. 初始化 React 项目

bashTerminal window
# 创建 Vite + React + TypeScript 项目
pnpm create vite db-connect-front --template react-ts
# 进入项目目录
cd db-connect-front
# 安装依赖
pnpm install

2. 配置 Vite 代理

vite.config.ts

typescript
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

typescript
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 ):

typescript
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},
}

3. 前端 API 调用

前端统一使用 /api 前缀调用后端接口:

typescript
fetch('/api/users', { method: 'POST', ... })

数据流转过程

[前端页面]
↓ 用户提交表单
[fetch('/api/users')]
↓ Vite 代理拦截
[重写为 http://localhost:3000/users]
↓ 发送 HTTP 请求
[NestJS UserController @Post()]
↓ 调用 Service
[UserService.create()]
↓ Prisma ORM
[MySQL 数据库]
↓ 返回创建的用户
[响应返回前端]
↓ 显示成功消息
[用户界面更新]

运行项目

1. 启动后端

bashTerminal window
cd db-connection
# 首次运行需要配置数据库
# 1. 编辑 .env 文件配置 DATABASE_URL
# 2. 生成 Prisma Client
pnpm prisma:generate
# 3. 同步数据库结构
pnpm prisma:push
# 启动开发服务器
pnpm start:dev
# 服务将运行在 http://localhost:3000

后端可用脚本:

bashTerminal window
pnpm start:dev # 开发模式(热重载)
pnpm start:prod # 生产模式
pnpm prisma:generate # 生成 Prisma Client
pnpm prisma:push # 推送数据库架构
pnpm prisma:studio # 打开 Prisma Studio
pnpm build # 构建项目

2. 启动前端

bashTerminal window
# 在新的终端窗口
cd db-connect-front
# 安装依赖(如果还没有)
pnpm install
# 启动开发服务器
pnpm dev
# 服务将运行在 http://localhost:5173

前端可用脚本:

bashTerminal window
pnpm dev # 开发模式
pnpm build # 构建生产版本
pnpm preview # 预览生产构建
pnpm lint # 运行 ESLint

3. 测试前后端联动

  1. 确保后端运行在 http://localhost:3000
  2. 确保前端运行在 http://localhost:5173
  3. 打开浏览器访问 http://localhost:5173
  4. 填写表单提交,观察控制台和数据库

API 接口文档

基础 URL

  • 开发环境: http://localhost:3000
  • 前端代理: /api (会被重写到后端)

用户接口

1. 创建用户

http
POST /users
Content-Type: application/json
{
"email": "user@example.com",
"name": "张三" // 可选
}

响应示例:

json
{
"id": 1,
"email": "user@example.com",
"name": "张三",
"createdAt": "2025-12-29T10:00:00.000Z"
}

2. 获取所有用户

http
GET /users

响应示例:

json
[
{
"id": 1,
"email": "user@example.com",
"name": "张三",
"createdAt": "2025-12-29T10:00:00.000Z"
}
]

3. 获取单个用户

http
GET /users/:id

响应示例:

json
{
"id": 1,
"email": "user@example.com",
"name": "张三",
"createdAt": "2025-12-29T10:00:00.000Z"
}

4. 更新用户

http
PATCH /users/:id
Content-Type: application/json
{
"email": "newemail@example.com",
"name": "李四"
}

5. 删除用户

http
DELETE /users/:id

响应示例:

json
{
"success": true
}

完整代码示例

前端调用所有接口示例

typescript
// 创建用户
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

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:
tsmain.ts
const app = await NestFactory.create(AppModule);
app.enableCors(); // 启用 CORS
await app.listen(3000);

2. Prisma Client 未生成

问题: 导入 @prisma/client 报错

解决方案:

bashTerminal window
cd db-connection
npx prisma generate

3. 数据库连接失败

问题: Prisma 无法连接数据库

解决方案:

  • 检查 .env 文件中的 DATABASE_URL 是否正确
  • 确保 MySQL 服务已启动
  • 确保数据库已创建
bashTerminal window
# 创建数据库(MySQL)
mysql -u root -p
CREATE DATABASE database_name;

4. 端口冲突

问题: 端口 3000 或 5173 已被占用

解决方案:

bashTerminal window
# 后端修改端口(main.ts)
await app.listen(3001);
# 前端修改端口(vite.config.ts)
server: {
port: 5174,
proxy: { ... }
}

5. Vite 代理不生效

问题: 前端无法通过代理访问后端

检查清单:

  • ✓ 后端是否已启动
  • ✓ Vite dev server 是否已重启(修改配置后需要重启)
  • ✓ 请求路径是否以 /api 开头
  • rewrite 函数是否正确移除前缀

6. TypeScript 类型错误

问题: 类型不匹配或找不到类型

解决方案:

bashTerminal window
# 后端
pnpm prisma generate # 重新生成 Prisma 类型
# 前端
# 确保 tsconfig.json 配置正确

开发最佳实践

1. 环境变量管理

bashTerminal window
# 创建 .env.example 作为模板
DATABASE_URL="mysql://user:password@localhost:3306/dbname"
PORT=3000
# .env 添加到 .gitignore
echo ".env" >> .gitignore

2. 数据库迁移

bashTerminal window
# 开发环境使用 db push(快速迭代)
pnpm prisma:push
# 生产环境使用迁移(版本控制)
npx prisma migrate dev --name add_user_model
npx prisma migrate deploy # 生产部署

3. 错误处理

typescript
// 前端统一错误处理
const handleApiError = (error: unknown) => {
if (error instanceof Error) {
console.error('API Error:', error.message)
}
// 显示用户友好的错误消息
}
// 后端全局异常过滤器
// 参考:https://docs.nestjs.com/exception-filters

4. 类型共享(高级)

可以考虑创建共享的类型定义包,避免前后端类型重复定义:

bashTerminal window
# 创建共享包
mkdir shared-types
# 定义 User、CreateUserDto 等类型
# 前后端都引用这个包

调试技巧

后端调试

bashTerminal window
# 查看详细日志
pnpm start:dev
# 使用 Prisma Studio 查看数据
pnpm prisma:studio

前端调试

typescript
// 在浏览器控制台查看网络请求
// Network Tab -> 查看请求和响应
// 添加调试日志
console.log('发送的数据:', userData)
console.log('响应结果:', result)

数据库调试

bashTerminal window
# 查看生成的 SQL
npx prisma studio
# 或直接连接数据库
mysql -u root -p
USE database_name;
SELECT * FROM User;

总结

本文档详细介绍了如何使用 NestJS + Prisma + React + Vite 实现前后端分离项目。

核心知识点:

  1. NestJS 模块化架构(Controller、Service、Module)
  2. Prisma ORM 数据库操作
  3. Vite 开发代理配置
  4. RESTful API 设计
  5. 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

后记

My avatar

感谢你读到这里。

这座小站更像一份持续维护的“终端笔记”:记录我解决问题的过程,也记录走过的弯路。

如果这篇内容对你有一点点帮助:

  • 点个赞 / 收藏一下,方便你下次回来继续翻
  • 欢迎在评论区补充你的做法(或者指出我的疏漏)
  • 想持续收到更新:可以订阅 RSS(在页面底部)

我们下篇见。


More Posts

评论

评论 (0)

请先登录后再发表评论

加载中...