news 2026/10/12 14:33:33

Egg + TypeScript 项目接入 egg-mongoose:从类型声明到 CRUD 的完整教程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Egg + TypeScript 项目接入 egg-mongoose:从类型声明到 CRUD 的完整教程

1. Egg TypeScript 项目里 egg-mongoose 到底解决什么问题

如果你正在写 Egg + TypeScript 的后端,又想让 MongoDB 的读写像调用本地方法一样顺手,那 egg-mongoose 就是那个把两者粘起来的插件。它做的事情很朴素:在 Egg 启动时帮你建立 MongoDB 连接,把连接实例挂到app.mongoose上,再让app/model目录下的文件自动被加载成 Model。你不需要在每个 Service 里手动mongoose.connect,也不用担心连接池被重复创建。

适合谁?适合已经用npm init egg --type=ts初始化过项目、手里有一个能连通的 MongoDB(本地 Docker 或云端都行)、并且希望 Model 和 Service 分层清晰的人。如果你还在用 JavaScript 写 Egg,这篇的 TS 类型声明部分同样能帮你把this.ctx.model.User的提示补全,只是写法要换成对应的.d.ts。

我试过在一个订单系统里直接裸用 mongoose,结果是每个 Service 都要 import 一次连接,热重载时连接数还会涨。换成 egg-mongoose 后,连接生命周期交给框架管,Service 只关心查询逻辑,代码量少了一截。下面按“初始化插件 → 补类型声明 → 定义 Model 与 Service → 本地 CRUD 验证”的顺序走一遍,每一步都给可复制的配置。

先明确一个检索词:Egg TypeScript 集成 egg-mongoose 的完整配置与 CRUD 验证。这篇文章就是围绕它展开,从config/plugin.ts到typings,再到一次真实的本地请求。

在开始之前,确认三件事:Node.js 建议 14.x 以上;MongoDB 已运行(本地mongodb://127.0.0.1:27017或云端连接串);项目已用 TS 模板初始化。如果 MongoDB 还没起,用 Docker 最快:

docker pull mongo:latest docker run -d --name egg-mongo -p 27017:27017 mongo:latest

启动后用 Navicat 或mongosh能连上127.0.0.1:27017就说明服务正常。这一步别跳过,后面连接失败十有八九是这里没起。

2. 安装 egg-mongoose 并补齐 TypeScript 类型声明

装插件本身一条命令:

npm i egg-mongoose --save

但 TypeScript 项目里,光装插件不够。Egg 的 TS 模板默认不认识app.mongoose、app.model、ctx.model这些属性,编辑器会飘红。所以第二步是补类型声明。

先启用插件。config/plugin.ts里加上:

// config/plugin.ts import { EggPlugin } from 'egg'; const plugin: EggPlugin = { mongoose: { enable: true, package: 'egg-mongoose', }, }; export default plugin;

注意 TS 模板导出的是default,不是module.exports。如果你从旧教程复制了module.exports = {...},在 TS 项目里会报类型不匹配,改成上面这样。

接着配置连接。config/config.default.ts:

// config/config.default.ts import { EggAppConfig, EggAppInfo, PowerPartial } from 'egg'; export default (appInfo: EggAppInfo) => { const config = {} as PowerPartial<EggAppConfig>; config.mongoose = { client: { url: 'mongodb://127.0.0.1:27017/egg_mongoose_demo', options: { useNewUrlParser: true, useUnifiedTopology: true, }, }, }; return { ...config, }; };

这里config.mongoose在默认类型里是没有的,所以下一步必须补声明,否则config.mongoose这行就报错。

在项目根目录建typings/index.d.ts(TS 模板通常已有typings目录),写入:

// typings/index.d.ts import mongoose from 'mongoose'; import { EggAppConfig } from 'egg'; declare module 'egg' { interface EggAppConfig { mongoose: { client?: { url: string; options?: mongoose.ConnectOptions; }; clients?: Record<string, { url: string; options?: mongoose.ConnectOptions; }>; }; } interface Application { mongoose: typeof mongoose; model: { User: mongoose.Model<any>; Order: mongoose.Model<any>; }; } interface Context { model: { User: mongoose.Model<any>; Order: mongoose.Model<any>; }; } }

这段声明做了三件事:让config.mongoose合法;让app.mongoose有 mongoose 的类型;让app.model.User和ctx.model.User在 Service/Controller 里有提示。Model<any>里的any后面可以换成具体的接口类型,比如IUser,提示会更精确。

如果你用多数据库,配置改成clients:

config.mongoose = { clients: { db1: { url: 'mongodb://127.0.0.1:27017/db1', options: { useNewUrlParser: true, useUnifiedTopology: true }, }, db2: { url: 'mongodb://127.0.0.1:27017/db2', options: { useNewUrlParser: true, useUnifiedTopology: true }, }, }, };

多库时 Model 里要用app.mongooseDB.get('db1')拿连接,这个后面 Model 部分会写。

补完声明后,重启一次 TS 服务(VS Code 里Ctrl+Shift+P→ Restart TS Server),app.mongoose的飘红应该消失。这一步是 TS 项目最容易卡住的地方,很多人装完插件发现类型报错就以为插件不兼容,其实只是声明没补。

3. 定义 Model 与 Service 分层:可复制的配置片段

Egg 约定 Model 放app/model,Service 放app/service。egg-mongoose 会自动加载app/model下的文件,文件名即 Model 名(首字母大写)。

先建用户模型app/model/user.ts:

// app/model/user.ts import { Application } from 'egg'; export default (app: Application) => { const mongoose = app.mongoose; const Schema = mongoose.Schema; const UserSchema = new Schema({ username: { type: String, required: true, unique: true }, password: { type: String, required: true }, status: { type: Number, default: 1 }, createdAt: { type: Date, default: Date.now }, }); return mongoose.model('User', UserSchema, 'user'); };

第三个参数'user'指定集合名。不写的话 mongoose 会按users复数化,容易和已有集合对不上,建议显式写。

订单模型app/model/order.ts:

// app/model/order.ts import { Application } from 'egg'; export default (app: Application) => { const mongoose = app.mongoose; const Schema = mongoose.Schema; const OrderSchema = new Schema({ order_id: { type: String, required: true }, uid: { type: Number }, total_price: { type: Number }, createdAt: { type: Date, default: Date.now }, }); return mongoose.model('Order', OrderSchema, 'order'); };

多数据库场景下,Model 要指定连接:

// app/model/user.ts(多库版) import { Application } from 'egg'; export default (app: Application) => { const mongoose = app.mongoose; const Schema = mongoose.Schema; const conn = app.mongooseDB.get('db1'); const UserSchema = new Schema({ username: { type: String, required: true }, password: { type: String, required: true }, }); return conn.model('User', UserSchema); };

Service 层app/service/user.ts,把 CRUD 收在这里:

// app/service/user.ts import { Service } from 'egg'; export default class UserService extends Service { async getUserList(page = 1, pageSize = 10) { const skip = (page - 1) * pageSize; return this.ctx.model.User.find({}) .sort({ createdAt: -1 }) .skip(skip) .limit(pageSize); } async addUser(userData: { username: string; password: string; status?: number }) { const user = new this.ctx.model.User(userData); return user.save(); } async updateUser(id: string, updateData: Record<string, unknown>) { return this.ctx.model.User.updateOne({ _id: id }, { $set: updateData }); } async deleteUser(id: string) { return this.ctx.model.User.deleteOne({ _id: id }); } }

Controller 层app/controller/user.ts:

// app/controller/user.ts import { Controller } from 'egg'; export default class UserController extends Controller { async index() { const { ctx } = this; const { page = 1, pageSize = 10 } = ctx.query; const userList = await ctx.service.user.getUserList(Number(page), Number(pageSize)); ctx.body = { code: 200, data: userList, msg: '获取用户列表成功' }; } async create() { const { ctx } = this; const result = await ctx.service.user.addUser(ctx.request.body); ctx.body = { code: 200, data: result, msg: '添加用户成功' }; } async update() { const { ctx } = this; const { id } = ctx.params; const result = await ctx.service.user.updateUser(id, ctx.request.body); ctx.body = { code: 200, data: result, msg: '更新用户成功' }; } async destroy() { const { ctx } = this; const { id } = ctx.params; const result = await ctx.service.user.deleteUser(id); ctx.body = { code: 200, data: result, msg: '删除用户成功' }; } }

路由app/router.ts:

// app/router.ts import { Application } from 'egg'; export default (app: Application) => { const { router, controller } = app; router.get('/users', controller.user.index); router.post('/users', controller.user.create); router.put('/users/:id', controller.user.update); router.delete('/users/:id', controller.user.destroy); };

这里有个坑:Egg 默认开启 CSRF 保护,POST/PUT/DELETE 请求不带 token 会返回 403。开发阶段可以临时关掉,在config/config.default.ts里加:

config.security = { csrf: { enable: false, }, };

生产环境别这么干,应该走ctx.csrf拿 token 再带上。或者针对单条路由关:

router.post('/users', controller.user.create, { csrf: false });

配置片段到这里就齐了:config/plugin.ts启用插件,config/config.default.ts配连接,typings/index.d.ts补类型,app/model定义 Schema,app/service收 CRUD,app/router.ts暴露接口。这套结构的好处是 Controller 只做参数转发,Service 只做数据操作,Model 只做结构定义,职责不混。

4. 本地启动与 CRUD 请求验证:类型提示是否真的生效

配置写完,启动项目:

npm run dev

看到egg started on http://127.0.0.1:7001就说明连接和插件加载都过了。如果启动时报Cannot read property 'Schema' of undefined,说明app.mongoose没挂上,回去检查config/plugin.ts是否 enable,以及typings是否补了Application.mongoose。

先测新增。用 curl:

curl -X POST http://127.0.0.1:7001/users \ -H "Content-Type: application/json" \ -d '{"username":"john_doe1","password":"password1231","status":1}'

返回类似:

{ "code": 200, "data": { "_id": "665f1a2b3c4d5e6f7a8b9c0d", "username": "john_doe1", "password": "password1231", "status": 1, "createdAt": "2024-06-04T10:00:00.000Z", "__v": 0 }, "msg": "添加用户成功" }

拿到_id后测查询:

curl http://127.0.0.1:7001/users

应该返回刚才那条记录。再测更新:

curl -X PUT http://127.0.0.1:7001/users/665f1a2b3c4d5e6f7a8b9c0d \ -H "Content-Type: application/json" \ -d '{"status":0}'

返回modifiedCount: 1说明更新成功。最后测删除:

curl -X DELETE http://127.0.0.1:7001/users/665f1a2b3c4d5e6f7a8b9c0d

返回deletedCount: 1即删除成功。

类型提示是否生效,看两个地方:在app/service/user.ts里敲this.ctx.model.User.时,编辑器应该弹出find、findOne、updateOne等方法;敲this.ctx.model.User.find({时,参数类型应该是FilterQuery。如果没提示,说明typings/index.d.ts没被 TS 识别,检查tsconfig.json的include是否包含typings/**/*.d.ts。

分页查询也顺手验一下:

curl "http://127.0.0.1:7001/users?page=1&pageSize=5"

Service 里的skip和limit会生效,返回条数不超过 5。

关联查询用aggregate的$lookup,在 Controller 里直接调:

const orderResult = await ctx.model.Order.aggregate([ { $lookup: { from: 'order_item', localField: 'order_id', foreignField: 'order_id', as: 'items', }, }, { $match: { total_price: { $gte: 90 } } }, ]);

from写集合名,不是 Model 名。$match放在$lookup后面可以过滤结果,放在前面能减少关联数据量,按场景选。

到这里,一次完整的本地 CRUD 请求就验证完了:连接建立、Model 加载、Service 调用、类型提示、增删改查全部走通。

5. 常见报错排查:401、local proxy failed、reading choices 与 OAuth

接入过程中最容易撞上的几类报错,逐个说清楚。

MongooseServerSelectionError: connect ECONNREFUSED 127.0.0.1:27017。这是连接被拒,不是代码问题。先确认 MongoDB 容器在跑:docker ps看egg-mongo是否 Up。如果容器在跑还报这个,检查config.mongoose.client.url里的端口和容器映射端口是否一致。Docker 默认映射27017:27017,如果你改过宿主机端口,url 也要跟着改。

Cannot read property 'Schema' of undefined。app.mongoose没挂上。三个检查点:config/plugin.ts里mongoose.enable是否为true;package是否为'egg-mongoose';typings/index.d.ts里是否声明了Application.mongoose。TS 项目里如果只补了Context.model没补Application.mongoose,Model 文件里app.mongoose就是 undefined。

401 Unauthorized / local proxy failed。如果你在 Service 里调用了外部 API(比如模型服务),401 通常是 Key 没带或带错。检查请求头里的Authorization是否为Bearer <你的Key>,Key 是否过期。local proxy failed一般是本地代理配置和实际网络环境不匹配,检查HTTP_PROXY/HTTPS_PROXY环境变量是否指向了一个不可用的地址,清掉再试。这类外部调用建议把 Base URL、Key、Model ID 三件套写全,缺一个都会 401。

reading 'choices' of undefined。这个报错常见于调用模型接口后解析响应体时,response.choices为 undefined。原因通常是响应体不是预期的 JSON 结构,可能是错误信息被当成正常响应返回了。排查方法:先把原始响应console.log出来,看status和body。如果是 4xx/5xx,先解决状态码问题;如果是 200 但结构不对,检查请求的model参数是否拼错,或者接口版本是否匹配。

OAuth 相关报错。如果你用 OAuth 方式接入外部服务,报invalid_grant或redirect_uri_mismatch,检查回调地址是否和注册时填的一致,token 是否已过期。OAuth 的 token 有有效期,过期后要重新走授权流程,不能复用旧 token。

CSRF token missing。POST/PUT/DELETE 返回 403,body 里提示missing csrf token。开发阶段在config.security.csrf.enable设为false,或者请求时带上ctx.csrf返回的 token。生产环境必须带 token,别关。

Model 找不到:this.ctx.model.User is not a constructor。app/model/user.ts文件名和mongoose.model('User', ...)的第一个参数要对应。文件名user.ts对应User,如果写成users.ts但 model 名是User,加载会失败。另外确认app/model目录在tsconfig.json的编译范围内。

类型提示不生效。this.ctx.model.User没有方法提示。检查typings/index.d.ts是否在tsconfig.json的include里;改完声明后重启 TS Server;确认egg的版本和@types/egg匹配。如果用的是egg-mongoose自带的类型,可能和手写声明冲突,以手写声明为准。

多数据库时 model 挂错连接。app.mongooseDB.get('db1')返回 undefined,说明config.mongoose.clients里没有db1,或者拼写不一致。检查配置里的 key 和get的参数是否完全一致,大小写敏感。

这些报错里,连接类和类型类占大多数。连接类看 Docker 和 url,类型类看typings和tsconfig。把这两块理顺,剩下的就是业务逻辑问题。

6. 从本地验证到长期编码:把接入流程固化下来

本地 CRUD 跑通只是第一步。真正在项目里用,还有几件事值得做。

第一,把typings/index.d.ts里的Model<any>换成具体接口。比如定义IUser:

export interface IUser { username: string; password: string; status: number; createdAt: Date; }

然后model.User: mongoose.Model<IUser>。这样find返回的文档就有字段提示,updateOne的$set也会校验字段名,写错字段编译期就能发现。

第二,Service 层加错误处理。现在addUser里user.save()如果 username 重复会抛E11000 duplicate key error,直接冒到 Controller 会返回 500。在 Service 里 try-catch,转成业务错误码:

async addUser(userData: { username: string; password: string }) { try { const user = new this.ctx.model.User(userData); return await user.save(); } catch (err: any) { if (err.code === 11000) { this.ctx.throw(409, '用户名已存在'); } throw err; } }

第三,敏感信息走环境变量。config.mongoose.client.url里的密码不要硬编码,用process.env.MONGO_URL:

url: process.env.MONGO_URL || 'mongodb://127.0.0.1:27017/egg_mongoose_demo',

本地开发用默认值,线上注入环境变量。

第四,常用查询字段加索引。username已经unique: true,自带唯一索引。如果经常按status过滤,在 Schema 里加index: true,或者用UserSchema.index({ status: 1 })。索引在数据量大时对查询速度影响明显。

第五,分页查询的skip在深分页时性能会下降。数据量上万后,skip(10000).limit(10)会扫描前 10000 条。可以改用基于_id的游标分页:find({ _id: { $gt: lastId } }).limit(10),性能稳定。

如果你后续要接模型服务做 AI 能力(比如给订单加智能分类),外部调用的 Base URL、Key、Model ID 三件套要配全,Key 走环境变量,别写进代码。模型对话调试可以用网页版快速验证参数,长期跑编码任务或 Agent 则适合用 Coding Plan 这类按量方案,避免每次手动换 Key。

接入文档里对配置项和错误码有更细的说明,遇到本文没覆盖的报错可以去查。API Keys 管理页可以创建和轮换 Key,建议给不同环境用不同的 Key,方便排查和回收。

整套流程走下来,核心就三块:插件启用和连接配置、类型声明补齐、Model/Service 分层。把这三块固化到项目模板里,下次新项目直接复制,十分钟就能跑通 CRUD。真正花时间的不是配置本身,而是遇到报错时知道去哪查——连接问题看 Docker 和 url,类型问题看 typings 和 tsconfig,业务问题看 Service 的 try-catch。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/12 14:31:18

Pomotroid 的 Tooltip 组件体系:组件契约、交互模型与全量挂载清单

【免费下载链接】pomotroid :tomato: Simple and visually-pleasing Pomodoro timer 项目地址&#xff1a; https://gitcode.com/gh_mirrors/po/pomotroid 点击查看 免费下载 Pomotroid 是一款基于 Tauri Svelte 的番茄钟桌面应用&#xff0c;其计时器窗口大量使用纯图标按钮…

作者头像 李华
网站建设 2026/10/12 14:29:02

近红外光谱回归建模:物理约束驱动的深度学习方法

简介&#xff1a;本资源是一套面向计算机专业本科生的毕业设计与课程作业级深度学习实践项目&#xff0c;聚焦近红外光谱&#xff08;NIR&#xff09;数据的回归建模任务&#xff0c;适用于化学分析、食品检测、生物医学等交叉领域中的成分定量预测场景。压缩包共9个文件&#…

作者头像 李华
网站建设 2026/10/12 14:28:38

SEIR+LSTM混合模型:新冠疫情预测的机理与数据融合实战

简介&#xff1a;基于传染病动力学模型SEIR与LSTM神经网络实现的新型冠状病毒肺炎&#xff08;COVID-19&#xff09;预测Python项目&#xff0c;面向计科、数据科学与大数据、人工智能等专业的课程设计、期末大作业及初期项目立项场景&#xff0c;适合入门进阶与二次开发。项目…

作者头像 李华
网站建设 2026/10/12 14:28:36

金融AI落地合规实战:为什么合规比模型选型更难?

1. 一周三家方案齐发&#xff0c;这个节奏说明了什么 上周跟几个做金融科技的朋友吃饭&#xff0c;聊到一个很有意思的现象&#xff1a;短短一周之内&#xff0c;三家不同背景的技术团队先后发布了面向金融行业的AI解决方案。有的是大厂金融云部门出的&#xff0c;有的是垂直领…

作者头像 李华
网站建设 2026/10/12 14:24:05

机器学习量化投资实战:从数据到回测的完整策略搭建指南

简介&#xff1a;一套帮助初学者快速入门的机器学习量化投资演示源码&#xff0c;以A股市场为例&#xff0c;面向已有一定Python基础、但对炒股和量化投资了解不深的读者。量化投资的核心是通过历史数据分析发现并利用市场规律&#xff0c;在风险可控前提下追求超额收益&#x…

作者头像 李华