Vue3+TS+Express 转 Nuxt4 全栈开发笔记(Drizzle ORM + MySQL)

适配人群:有 Express 后端 + Vue3 前端 + mysql2 原生数据库开发基础,转 Nuxt.js 一体化全栈开发

官网:Nuxt.js

前言

适用人群:Vue3 前端开发er,有 Express 后端项目接触经验,想一站式上手 Nuxt.js 一体化全栈;

本文分为两大模块:一、核心概念认知(前端视角理解Nuxt)二、小试牛刀:MySQL+Drizzle全栈实操

数据库相关代码全部隐藏敏感密码,可直接替换自身配置使用。

第一部分 核心概念讲解

1. 传统Vue3 + Express 分离架构 VS Nuxt一体化架构

1.1 传统分离模式(你熟悉的开发模式)

  1. 两个独立项目
    • 前端:Vue3 + Vite,仅负责页面渲染、接口请求,无后端能力;
    • 后端:Express/Koa,单独启动服务,写接口、操作数据库;
  2. 启动方式:两个终端分别启动前端、后端;
  3. 痛点:
    • 跨域配置繁琐;
    • 前后端类型需要手动同步,容易出现字段不匹配;
    • 首页白屏、SEO差(纯客户端渲染);
    • 新增接口需要前后端两边改代码。

1.2 Nuxt4 一体化全栈架构

Nuxt = Vue3 + 内置Nitro后端引擎,一个项目同时包含前端页面+后端接口

  1. 仅需一个项目,单命令 npm run dev 同时启动前后端;
  2. 内置服务端渲染SSR/静态渲染SSG,天然解决Vue单页白屏、SEO问题;
  3. 内置后端能力,无需额外搭建Express,直接在项目内写接口、操作MySQL;
  4. 全局共享TS类型,前端页面、后端数据库共用一套类型定义;
  5. 内置路径别名、路由自动生成、请求上下文,省去大量手动配置。

2. 关键名词翻译(Vue前端视角,结合Express经验理解)

2.1 Nitro

Nuxt内置后端服务引擎,替代Express ,负责处理接口请求、中间件、数据库交互;

不需要安装express包,Nuxt底层自带完整后端能力。

2.2 Server Routes(后端接口目录 server/api/

对标Express的路由文件:

  • Express:手动创建路由文件,app.get('/user', handler) 注册接口;
  • Nuxt:文件即接口,server/api/v1/user/index.get.ts 自动生成接口地址 GET /api/v1/user,零手动注册路由;
    文件命名规则:xxx.get.ts = GET请求、xxx.post.ts = POST请求。

2.3 Pages 目录(前端页面)

对标Vue Router:

  • 传统Vue:手动配置router/index.js定义页面路由;
  • Nuxt:pages/下的Vue文件路径自动映射浏览器路由;
    例:
    1. pages/index.vue → 浏览器路由 /(首页)
    2. pages/about/index.vue → 浏览器路由 /about
    3. pages/user/[id].vue → 动态路由 /user/:id
    4. pages/blog/2025/index.vue → 嵌套路由 /blog/2025

然后记得在app.vue里面引入<NuxtPage />

<NuxtPage />等价原生 Vue Router 的 <router-view>,是页面渲染的路由占位出口

Nuxt 全局内置组件,不需要手动导入、无需注册,直接在模板使用

作用:根据当前浏览器路由,自动匹配 pages/ 目录对应的页面组件并渲染到该位置)

同时app.vue 是 Nuxt 项目全局根布局,所有页面都会嵌套在它内部渲染,如果不写 <NuxtPage />,页面无法展示,只会空白。

2.4 ~/ 全局路径别名

Nuxt内置,等价项目根目录,解决Vue多层../../相对路径混乱问题;

后端导入数据库:import { db } from '~/server/db',无需计算层级。

2.5 .nuxt 缓存目录

Nuxt编译、类型生成缓存文件夹;

出现页面白屏、无限编译、TS爆红、接口404时,优先删除该文件夹重启项目

2.6 Drizzle ORM

轻量TS友好 ORM 工具(ORM 是把数据库表映射成代码里的对象,用面向对象语法替代手写 SQL 操作数据库的工具),底层封装mysql2,替代Express原生手写SQL字符串;

核心优势:表结构定义后自动生成TS类型,前端页面、后端接口共享字段类型,写错字段编辑器直接标红。

3. Nuxt完整项目目录总览(前后端一体)

复制代码
nuxt-demo/
├── .nuxt/                  # 编译缓存(报错直接删除)
├── drizzle.config.ts       # Drizzle数据库迁移配置
├── tsconfig.json           # TS全局配置,优化编译速度
├── package.json
├── pages/                  # 【前端】页面目录,自动生成路由
│   └── index.vue           # 首页页面
│   └── user
│   		└── index.vue   # /user 路由
├── server/                 # 【后端】等价Express项目根目录
│   ├── db/                 # 数据库核心目录
│   │   ├── index.ts        # mysql2连接池 + Drizzle实例
│   │   └── schema.ts       # 数据库表结构定义
│   ├── utils/              # 后端通用工具
│   │   └── response.ts     # 接口统一返回格式封装
│   └── api/                # 后端接口路由,自动生成API地址
│       └── v1/
│           └── user/
│               └── index.get.ts  # GET /api/v1/user 接口
└── node_modules/

4. Vue前端开发者开发习惯转换

  1. 路由开发:不用写vue-router,新增页面直接丢进pages文件夹;
  2. 接口开发:不用单独启动Express,在server/api新建文件即接口;
  3. 类型管理:数据库表定义后全局复用,不用前后端分别写interface;
  4. 启动项目 :仅一条npm run dev,同时跑页面和后端接口;
  5. 渲染模式:默认SSR服务端渲染,解决Vue首屏白屏;
  6. 数据库:同一项目内直接操作MySQL,无需跨域、不用额外后端服务。

第二部分 小试牛刀:Nuxt + MySQL + Drizzle ORM 全栈实操(前端页面 + 后端接口完整流程)

可借助 vibe-coding 简单尝试一下

阶段1:脚手架从零创建纯净最小化Nuxt4项目

1. 执行创建命令

打开终端,进入存放代码的文件夹,执行:

bash 复制代码
pnpm create nuxt@latest nuxt4-practice-demo

2. 交互式选项(Nuxt4新版真实流程,选minimal最小模板)

bash 复制代码
1. Select a template(选择模板)
选择:`minimal -- Minimal setup for Nuxt 4 (recommended)`> 
> 最小纯净模板,无多余示例页面、冗余插件,适合我们从零写接口、数据库逻辑
2. Which package manager would you like to use?
选择:`pnpm`
3. Initialize git repository?(是否初始化Git)
按需:`Yes` / `No` 都可以
4. Install extra modules?(是否安装官方额外模块)
选择:`No`,全部模块后续手动按需安装,保持项目干净

3. 进入项目目录,启动验证基础环境

bash 复制代码
# 进入项目
cd nuxt4-practice-demo
# 启动开发服务
pnpm run dev

浏览器打开 http://localhost:3000,出现空白极简页面代表项目创建成功,关闭终端继续数据库开发。

阶段2:前置数据库准备

  1. 本地MySQL服务启动,DataGrip可视化工具连接测试正常;
  2. DataGrip新建查询窗口,执行以下SQL创建测试库与表(无敏感信息)
sql 复制代码
CREATE DATABASE nuxt_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE nuxt_db;

CREATE TABLE `user` (
  `id` INT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
  `name` VARCHAR(50) NOT NULL COMMENT '用户名',
  `age` INT NOT NULL COMMENT '年龄'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

INSERT INTO `user` (name, age) VALUES ('张三',18),('李四',22),('王五',26);
  1. 安装MySQL与Drizzle相关依赖
bash 复制代码
# 运行依赖:mysql2连接池、drizzle核心ORM
pnpm add drizzle-orm mysql2
# 开发依赖:drizzle-kit 表结构迁移工具
pnpm add -D drizzle-kit

阶段3:后端数据库 & 接口开发

步骤1:新建数据表结构定义文件 server/db/schema.ts

作用:用代码描述数据库表,自动生成TS类型,前后端页面、接口可以共用类型约束

ts 复制代码
import { mysqlTable, int, varchar } from 'drizzle-orm/mysql-core';

// 映射数据库内user表
export const userTable = mysqlTable('user', {
  id: int('id').primaryKey().autoincrement(),
  name: varchar('name', 50).notNull(),
  age: int('age').notNull()
});

// 自动推导用户表TS类型,前端页面可导入使用
export type User = typeof userTable.$inferSelect;

步骤2:初始化MySQL连接池与Drizzle实例 server/db/index.ts

底层基于mysql2连接池,配置和Express后端写法完全通用,适配新版Drizzle修复TS重载报错

ts 复制代码
import { drizzle } from 'drizzle-orm/mysql2';
import mysql from 'mysql2/promise';
import { userTable } from './schema';

// mysql2原生连接池配置
const pool = mysql.createPool({
  host: '127.0.0.1',
  port: 3306,
  user: 'root',
  password: '你的本地MySQL密码', // 自行替换为自己数据库密码
  database: 'nuxt_db',
  waitForConnections: true,
  connectionLimit: 10,
  queueLimit: 0,
  idleTimeout: 60000, // 延长空闲连接保活时间,避免连接意外断开
  connectTimeout: 10000
});

// 创建Drizzle操作实例,mode字段是新版本必填参数,解决TS类型匹配报错
export const db = drizzle(pool, {
  schema: { userTable },
  mode: 'default'
});

步骤3:后端统一返回工具 server/utils/response.ts

封装固定接口返回格式,不用在每个接口重复手写code/data/msg结构,对标Express统一返回中间件

ts 复制代码
// 成功返回封装
export function success<T>(data: T, msg = '操作成功') {
  return {
    code: 200,
    data,
    msg
  };
}

// 失败返回封装
export function fail(msg = '操作失败', code = 500) {
  return {
    code,
    data: null,
    msg
  };
}

步骤4:编写后端查询接口 server/api/v1/user/index.get.ts

Nuxt约定式接口文件,文件名.get.ts代表GET请求,文件路径自动映射/api/v1/user接口地址,替代Express手动注册路由

ts 复制代码
import { db } from '~/server/db';
import { userTable } from '~/server/db/schema';
import { success, fail } from '~/server/utils/response';

// defineEventHandler:Nuxt内置后端请求处理函数,替代Express的(req,res)回调
export default defineEventHandler(async () => {
  try {
    // Drizzle ORM链式查询,等价原生SQL:SELECT * FROM user
    const userList = await db.select().from(userTable);
    return success(userList);
  } catch (err)
    // 统一捕获数据库异常,不在连接池监听error(存在TS类型冲突)
    console.error('数据库查询异常:', err);
    return fail('数据库查询失败,请检查MySQL服务');
  }
});

步骤5:Drizzle迁移配置(可选,用于同步表结构)drizzle.config.ts

文件作用说明

这是给drizzle-kit迁移工具的配置文件,读取schema.ts内的表结构代码,对比真实MySQL数据库,自动生成/执行表变更SQL,不用手动写CREATE/ALTER TABLE语句。

ts 复制代码
import type { Config } from 'drizzle-kit';

export default {
  schema: './server/db/schema.ts', // 读取表结构定义文件
  out: './drizzle', // 自动生成迁移SQL脚本存放目录
  driver: 'mysql2', // 指定数据库驱动
  dbCredentials: { // 工具连接数据库的账号配置
    host: '127.0.0.1',
    user: 'root',
    password: '你的本地MySQL密码',
    database: 'nuxt_db',
    port: 3306
  }
} satisfies Config;
配套迁移常用命令
复制代码
# 对比代码表结构与数据库,生成迁移SQL文件
pnpm drizzle-kit generate
# 自动连接MySQL,执行迁移脚本同步表结构
pnpm drizzle-kit migrate

临时接口测试(先验证后端可用)

  1. 终端启动项目
bash 复制代码
pnpm run dev
  1. 浏览器访问接口地址:http://localhost:3000/api/v1/user
  2. 正常输出用户数组JSON,代表后端数据库接口开发完成。

阶段4:前端页面开发(Nuxt页面路由 + 请求后端接口)

1. 根布局文件 app.vue(全局布局 + 路由出口 <NuxtPage />

app.vue是Nuxt全局根组件,所有页面都会渲染在<NuxtPage />内部,等价Vue项目根App.vue + <router-view>

html 复制代码
<template>
  <!-- 全局公共导航栏 -->
  <nav style="padding:16px; background:#eee;">
    <!-- Nuxt内置跳转组件,替代a标签、vue-router的router-link -->
    <NuxtLink to="/" style="margin-right:20px">用户列表首页</NuxtLink>
  </nav>

  <!-- 路由渲染出口,pages目录页面全部挂载在此,必须存在否则页面空白 -->
  <NuxtPage />

  <!-- 全局底部 -->
  <footer style="padding:16px; margin-top:30px; border-top:1px solid #eee;">
    Nuxt4 全栈演示 | Vue3+TS+Drizzle+MySQL
  </footer>
</template>

2. 首页页面 pages/index.vue(前端调用后端接口展示数据库数据)

Nuxt约定路由:pages/index.vue 自动映射浏览器路由 /

使用Nuxt内置useFetch请求本地后端接口,自带TS类型推断,无需手动安装axios。

html 复制代码
<template>
  <div style="padding:0 20px;">
    <h2>用户列表(从MySQL数据库读取)</h2>

    <!-- 加载状态 -->
    <div v-if="pending">加载中...</div>

    <!-- 错误提示 -->
    <div v-else-if="error" style="color:red">
      请求失败:{{ error.message }}
    </div>

    <!-- 数据表格渲染 -->
    <table border="1" cellpadding="8" v-else>
      <thead>
        <tr>
          <th>ID</th>
          <th>用户名</th>
          <th>年龄</th>
        </tr>
      </thead>
      <tbody>
        <tr v-for="item in data.data" :key="item.id">
          <td>{{ item.id }}</td>
          <td>{{ item.name }}</td>
          <td>{{ item.age }}</td>
        </tr>
      </tbody>
    </table>
  </div>
</template>

<script setup lang="ts">
// 导入后端定义好的User类型,前后端类型统一,杜绝字段拼写错误
import type { User } from '~/server/db/schema';

// 定义接口返回完整类型
interface ApiRes {
  code: number
  data: User[]
  msg: string
}

// useFetch Nuxt内置请求函数,直接访问同项目后端/api接口
const { data, pending, error } = useFetch<ApiRes>('/api/v1/user')
</script>

阶段5:完整全栈项目运行测试

  1. 终端执行启动命令
bash 复制代码
pnpm run dev
  1. 浏览器打开首页 http://localhost:3000
  2. 页面自动请求本地后端 /api/v1/user,读取MySQL数据并渲染表格;
  3. 流程闭环:前端页面 → Nuxt内置Nitro后端 → mysql2连接池 → MySQL数据库。

补充:完成所有文件后的完整目录核对

复制代码
nuxt4-practice-demo/
├── app.vue                     # 全局根布局、<NuxtPage>路由出口
├── pages/
│   └── index.vue               # 前端首页页面
├── server/
│   ├── db/
│   │   ├── index.ts
│   │   └── schema.ts
│   ├── utils/
│   │   └── response.ts
│   └── api/v1/user/index.get.ts # 后端接口
├── drizzle.config.ts           # Drizzle迁移配置
└── package.json

最后小小补充:Nuxt.js 使用 Element-Plus 和 Vue 使用略有不同,详细请参考官网说明@element-plus/nuxt


ok,大致就这样,接下来快去简单试试吧,happy coding~

相关推荐
IT_陈寒15 小时前
Vite静态资源路径这个坑差点让我加班到凌晨
前端·人工智能·后端
掘金酱16 小时前
「TRAE Work 实战帮」征文启动!你沉淀的经验,值得被看见!
前端·人工智能·后端
橙子家16 小时前
Windows 上同时安装多个 node 版本
前端
晓说前端16 小时前
TypeScript 高级特性 —— 类型断言与泛型
前端·typescript
爱勇宝16 小时前
DeepSeek V4-Flash 更新:代码与 Agent 能力全面增强
前端·后端·deepseek
GuWenyue16 小时前
90%前端写React+TS都踩坑!从组件类型、单向数据流到本地存储完整实战
前端·react.js
妙码生花16 小时前
从 PHP 到 AI + Golang,程序员自救转型手记(四十五):前端远程下拉输入组件
前端·javascript·vue.js
明月_清风17 小时前
全栈工程师必会技术栈:从入门到架构的完整成长地图 🗺️
前端·后端·全栈
windliang17 小时前
Claude Code 源码分析(三):一次模型回答如何流进 Agent
前端·算法·ai编程