首页
/ 用 NestJS + Refine 构建快速且高度可定制的 Admin 管理面板

用 NestJS + Refine 构建快速且高度可定制的 Admin 管理面板

2026-09-05 15:20:37作者:曹令琨Iris

本文基于 Refine 仓库中的博客教程 2021-10-4-admin-panel-with-nestjs.md 展开,完整讲解如何搭建一个 job-posting(职位发布)应用:后端用 NestJS + TypeORM + nestjsx/crud 快速生成 CRUD REST 接口,前端用 Refine 框架构建配套的管理面板。读完后你将掌握 NestJS 的迁移脚本配置、CRUD 自动端点机制,以及 Refine 的 Data Provider、Resources、表格页与路由组织的完整实战方案,并能对照仓库中最新的 examples/blog-job-posting 示例理解版本演进后的写法。

项目结构:api 与 admin 两部分

整个项目拆分为两个独立部分:

  • api:NestJS 编写的 REST 后端,使用 TypeORM 连接 MySQL,借助 nestjsx/crud 自动生成 /companies/jobs 等 CRUD 端点;
  • admin:Refine 构建的管理面板,通过 @refinedev/nestjsx-crud 数据 Provider 调用上述 API,渲染公司(companies)与职位(jobs)两个资源的管理页面。

仓库中保留的管理端示例位于 examples/blog-job-posting,包含 src/pages/companiessrc/pages/jobs 两个资源目录与入口文件 src/App.tsx,其接口模型定义在 src/interfaces/index.d.tsICompany 包含 idnamelocationisActive 及若干可选社媒链接字段,IJob 包含 idtitlelocationcontentisActive 以及关联的 company 对象——这正是文章示例页面的数据契约。

需要说明:原文档基于 Refine 3.x 编写,仓库中的示例已升级到新版本 API(组件从 @refinedev/antd 导入、资源使用路由路径而非组件)。本文以原文档为骨架,并用当前仓库源码补充 Provider 的底层实现。

构建 NestJS REST API

初始化项目

前置要求:Node(>= 10.13.0,v13 除外)与 npm。通过 Nest CLI 创建项目:

mkdir job-posting-app
cd job-posting-app
npm i -g @nestjs/cli
nest new api

选择 TypeORM 作为 ORM(TypeScript 编写,与 Nest 配合成熟,支持 MySQL、MariaDB、Postgres 等多种数据库),本文选用 MySQL:

npm install --save @nestjs/typeorm @nestjs/config typeorm mysql2

数据库配置与迁移脚本

教程要求完成以下配置(这些文件位于教程配套仓库的 api/ 目录,此处说明其职责与内容):

  1. .env.example:保存数据库连接信息(主机、端口、用户名、密码、库名),实际 .env 从该模板复制而来;
  2. docker-compose.yml:为 MySQL 提供容器化运行环境,保证本地一键启动数据库;
  3. ormconfig.ts:TypeORM CLI 的配置文件,供迁移(migration)命令读取;
  4. package.json 中的迁移脚本
"scripts": {
  "typeorm": "ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli.js",
  "db:migration:generate": "npm run typeorm -- migration:generate",
  "db:migration:run": "npm run typeorm -- migration:run",
  "db:migration:revert": "npm run typeorm -- migration:revert",
  "db:refresh": "npm run typeorm schema:drop && npm run db:migration:run"
}

db:migration:generate 根据实体定义生成增量迁移文件,db:refresh 则直接重建整个 schema,适合开发期快速迭代。

  1. 导入 TypeOrmModule:在 app.module.ts 中通过 TypeOrmModule.forRoot(配置项指向 ormconfig.ts / 环境变量)注册 TypeORM。

安装 nestjsx/crud 生成 CRUD 端点

npm i @nestjsx/crud @nestjsx/crud-typeorm class-transformer class-validator

nestjsx/crud 会在每个实体的 Controller 上自动挂载标准 REST 端点(GET /resource 列表、GET /resource/:id 详情、POST /resource 创建、PATCH /resource/:id 更新、DELETE /resource/:id 删除、POST /resource/bulk 批量创建等),并支持通过查询参数实现过滤、排序、分页与联表。Entity、Controller、Service 的具体创建过程在教程中略过,可参考配套示例仓库。

构建 Refine Admin Panel

用脚手架生成项目

npm create refine-app@latest admin -- -b v3

交互选项按如下回答(对应原文档的问卷输出):

Select your project type › refine-react
What will be the name of your app · admin
Package manager: · Npm
Do you want to use a UI Framework? · Ant Design
Do you want a customized theme?: · Yes (Custom Variables)
Router Provider: · React Router v6
Data Provider: · nestjsx-crud
Auth Provider: · None
Do you want to add example pages? · Yes (Recommended)
Do you want a customized layout? · Yes
i18n - Internationalization: · No

随后 cd admin && npm run dev 即可看到 Refine 示例应用。在 App.tsx 中把后端地址指向前面搭建的 API:

const API_URL = "http://localhost:3000";

编写 companies 列表页

companies 端点添加列表页 admin/src/pages/companies/list.tsx(继承原文档完整代码,并补充关键注释):

import {
  List,
  Table,
  TextField,
  useTable,
  IResourceComponentsProps,
  getDefaultSortOrder,
  Space,
  EditButton,
  DeleteButton,
  TagField,
  ShowButton,
} from "@pankod/refine";
import { ICompany } from "interfaces";

export const CompanyList: React.FC<IResourceComponentsProps> = () => {
  // useTable 内部调用 dataProvider.getList,
  // 将 pagination/sorters/filters 序列化为 URL 查询参数
  const { tableProps, sorter } = useTable<ICompany>({
    sorters: {
      initial: [{ field: "id", order: "desc" }], // 默认按 id 倒序
    },
  });

  return (
    <List>
      {/* tableProps 展开出 dataSource、pagination、onChange 等 */}
      <Table {...tableProps} rowKey="id">
        <Table.Column
          dataIndex="id"
          key="id"
          title="ID"
          render={(value) => <TextField value={value} />}
          defaultSortOrder={getDefaultSortOrder("id", sorter)}
          sorter
        />
        <Table.Column
          dataIndex="name"
          key="name"
          title="Name"
          render={(value) => <TextField value={value} />}
          defaultSortOrder={getDefaultSortOrder("name", sorter)}
          sorter
        />
        <Table.Column
          dataIndex="location"
          key="location"
          title="Location"
          render={(value) => <TextField value={value} />}
          defaultSortOrder={getDefaultSortOrder("location", sorter)}
          sorter
        />
        <Table.Column
          dataIndex="isActive"
          key="isActive"
          title="Is Active"
          render={(value) => <TagField value={value} />}
          defaultSortOrder={getDefaultSortOrder("status", sorter)}
          sorter
        />
        <Table.Column<ICompany>
          title="Actions"
          dataIndex="actions"
          render={(_, record) => (
            <Space>
              {/* recordItemId 指定操作对象,按钮跳转/请求对应记录 */}
              <EditButton hideText size="small" recordItemId={record.id} />
              <ShowButton hideText size="small" recordItemId={record.id} />
              <DeleteButton hideText size="small" recordItemId={record.id} />
            </Space>
          )}
        />
      </Table>
    </List>
  );
};

核心机制:useTable 钩子返回 tableProps(含 dataSource、分页状态与 onChange 处理),把 Ant Design Table 的交互状态与 Refine 的数据请求打通;getDefaultSortOrder 将 Refine 侧的排序状态同步回列头,实现受控排序。创建、编辑页(create.tsxedit.tsx)同理使用 useForm/useUpdate 等钩子实现,jobs 资源照此办理。

在 App.tsx 中定义资源

import { Refine } from "@pankod/refine";
import routerProvider from "@pankod/refine-react-router";
import nestjsxCrudDataProvider from "@refinedev/nestjsx-crud";

import "styles/antd.less";

import {
    CompanyList,
    CompanyShow,
    CompanyCreate,
    CompanyEdit,
} from "./pages/companies";
import {
    Title,
    Header,
    Sider,
    Footer,
    Layout,
    OffLayoutArea,
} from "components";
import { JobList, JobCreate, JobEdit } from "pages/jobs";

function App() {
    const API_URL = "http://localhost:3000";
    const dataProvider = nestjsxCrudDataProvider(API_URL);

    return (
        <Refine
            dataProvider={dataProvider}
            Title={Title}
            Header={Header}
            Sider={Sider}
            Footer={Footer}
            Layout={Layout}
            OffLayoutArea={OffLayoutArea}
            routerProvider={routerProvider}
            resources={[
                {
                    name: "companies",
                    list: CompanyList,
                    create: CompanyCreate,
                    edit: CompanyEdit,
                    show: CompanyShow,
                },
                {
                    name: "jobs",
                    list: JobList,
                    create: JobCreate,
                    edit: JobEdit,
                    show: CompanyShow,
                },
            ]}
        />
    );
}

name 字段必须与后端端点路径一致(/companies/jobs),因为 Data Provider 正是以 resources/name 拼接请求 URL;TitleHeaderSiderFooterLayoutOffLayoutArea 六个布局组件构成可定制化的面板骨架。

深入 @refinedev/nestjsx-crud:Provider 如何对话 nestjsx/crud

上文示例中 nestjsxCrudDataProvider(API_URL) 的实际实现位于 packages/nestjsx-crud/src/provider.ts。它接收 apiUrl 与一个可选的 AxiosInstance(默认使用库内置实例),返回一个实现 Required<DataProvider> 全部方法的对象,核心设计是:用 nestjsx 官方的 RequestQueryBuilder(来自 @nestjsx/crud-request)把 Refine 的查询语义翻译成服务端能识别的查询参数。以 getList 为例:

getList: async ({ resource, pagination, filters, sorters, meta }) => {
    const url = `${apiUrl}/${resource}`;
    let query = RequestQueryBuilder.create();
    query = handleFilter(query, filters);      // 过滤条件
    query = handleJoin(query, meta?.join);    // 联表
    query = handlePagination(query, pagination); // 分页
    query = handleSort(query, sorters);       // 排序
    const { data } = await httpClient.get(`${url}?${query.query()}`);
    // 兼容返回裸数组(无分页)与 { data, total }(有分页)两种响应
    if (Array.isArray(data)) {
      return { data, total: data.length };
    }
    return { data: data.data, total: data.total };
},

这套转换链路正是 useTablesorters、分页与过滤"即插即用"的底层原因,各环节实现位于 packages/nestjsx-crud/src/utils

  • 过滤handleFilter.ts 将 Refine 的 CrudFilters 递归转换为 nestjsx 的 SCondition,其中叶子条件形如 { field: { [operator]: value } }and/or 组合条件则递归映射为 $and/$or;算子转换由 mapOperator 负责(例如 $gt 与 nestjsx 的 gt 对齐)。test/utils/handleFilter.spec.ts 等测试用例覆盖了这一映射;
  • 其余方法getManyid IN [...] 条件批量查询;update 发送 PATCH /resource/:idupdateManydeleteManyPromise.all 并发循环逐条请求;createMany 走 nestjsx 的 POST /resource/bulk 批量端点;deleteOneDELETE /resource/:id;错误统一经 transformHttpError 转成 Refine 的 HttpError 结构(HTTP 状态码 + 消息),供 notificationProvider 展示;
  • custom 方法:额外支持 useCustom 场景,可传自定义 urlmethod(put/post/patch/delete/get),get 请求会同样应用 filter/join/sorter 参数。

也就是说,nestjsx/crud 在服务端自动生成端点,@refinedev/nestjsx-crud 在前端把 Refine 的抽象查询翻译成与之匹配的参数约定,两端由 @nestjsx/crud-request 的查询语言精确对接,这是本方案"前后端零胶水代码"的关键。

当前仓库示例的版本演进

对照 examples/blog-job-posting/src/App.tsx 可以看到教程代码在仓库中的最新形态:

  • 组件改从 @refinedev/antd 导入(ListTextFieldTagField 等),核心能力来自 @refinedev/core
  • resources 声明由"资源名 → 组件"改为"资源名 → 路由路径"(如 list: "/companies"edit: "/companies/edit/:id"),页面通过 <Routes>/<Route> 显式挂载;
  • 布局改用 <ThemedLayout> 包裹 <Outlet> 的方式,配合 RefineThemes.Blue 主题、useNotificationProvider 通知与 UnsavedChangesNotifierDocumentTitleHandler 等辅助组件;
  • 数据 Provider 的用法保持不变,仍是一行 nestjsxCrudDataProvider(API_URL)

新版列表页 examples/blog-job-posting/src/pages/companies/list.tsx 与原文档的 v3 版本在逻辑上几乎一一对应:同样的 useTable 初始排序、同样的五列布局与操作按钮,只是导入来源更新。本地快速体验可用:

npm create refine-app@latest -- --example blog-job-posting

小结

这篇教程展示了一条高效的管理面板开发路线:NestJS + TypeORM 迁移脚本 + nestjsx/crud 自动端点负责后端,Refine + nestjsx-crud Data Provider 负责前端,两者通过 nestjsx 的请求查询语言无缝衔接。仓库中的 packages/nestjsx-crud 源码(provider 实现与单元测试)和 examples/blog-job-posting 示例是理解该方案的最佳一手资料;需要注意原文档基于 Refine v3 编写,实际落地时应以仓库内最新示例的 API 形态为准。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
588
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
906
1.83 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
891
5.79 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.53 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.34 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
988
506
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384