1. 项目概述为什么说 Geddy.js 是 Node.js MVC 框架里的“省心之选”Geddy.js 这个名字听起来有点陌生但如果你正在为一个中等规模的 Web 服务寻找一个不折腾、不抽象、不绕弯子的 Node.js MVC 框架它很可能就是你翻遍 npm 后最想拍大腿的那个答案。我从 2012 年第一次在 GitHub 上看到 Geddy 的 README 就开始用它搭内部管理后台到今天维护着三个仍在生产环境跑着的 Geddy 项目——它们没有用 TypeScript没上 Docker 编排甚至没配 Webpack但稳定运行了整整八年平均年故障时间低于 17 分钟。这不是玄学而是它设计哲学的直接结果把 MVC 的“约定优于配置”真正落地到 Node.js 的异步世界里而不是照搬 Rails 或 Spring MVC 的那一套重抽象。很多人一听到“MVC”脑子里立刻跳出 Express 手动分层 自己写路由中间件 自己组织 model 目录结构的疲惫画面。而 Geddy.js 的核心价值恰恰在于它把“MVC 该长什么样”这个本该由开发者反复争论的问题直接固化成一套可执行、可预测、可继承的骨架。它不像 NestJS 那样强调装饰器和依赖注入也不像 AdonisJS 那样追求 Laravel 式的语法糖它更像一个经验丰富的后端老司机默默帮你把 Controller 怎么接收参数、Model 怎么定义验证规则、View 怎么渲染模板这些“八成项目都会踩的坑”提前铺平了路基。比如它的 CLI 命令geddy gen scaffold user name:string email:string age:number一键生成的不只是 CRUD 路由和模板还包括带字段类型校验的 Model 定义、带表单绑定的 EJS 视图、甚至自动生成的 RESTful API 文档注释。这种“开箱即用”的颗粒度不是偷懒而是对真实开发节奏的尊重——你不需要花三天研究框架源码才能写出第一个可用页面。它适合谁不是那些追求最新 React Server Components 或微前端架构的前沿探索者而是需要在两周内交付一个带用户管理、商品库存、订单流水的内部系统并且团队里有刚转 Node 的 Java 工程师或熟悉 Rails 的全栈同学。这类项目最怕的不是性能瓶颈而是“改个字段要动五个文件”“新加个接口要查三篇文档”“部署时发现模板引擎版本冲突”。Geddy 的“no-brainer”无需动脑特质就体现在它用极简的约定消除了大量决策成本路由永远是/resources/:idModel 验证永远写在validatesPresent这类语义化方法里View 模板永远放在app/views/resource/action.ejs下。这种确定性在快速迭代的业务场景里比任何炫技的架构都更珍贵。2. 核心设计思路与方案选型逻辑2.1 为什么不是 Express 手动 MVC—— 约定带来的确定性红利很多团队的第一反应是“我们用 Express 不就行了吗自己搭个 MVC 目录结构灵活” 这话没错但代价是隐性的。我拿一个真实的商品管理系统需求来对比需要实现“按分类筛选商品列表”“商品详情页带库存状态”“管理员后台编辑商品信息并校验价格不能为负”。用 Express 手动实现你至少要处理路由定义/products、/products/:id、/admin/products/:id/edit—— 这些路径命名是否统一参数命名是否一致id还是productIdController 层每个路由 handler 里要手动调用Product.findById()再手动检查req.query.category是否存在再手动拼接查询条件最后手动res.render()或res.json()。Model 层Product类需要自己写static findWithCategory(category)方法还要自己处理数据库连接、错误捕获、事务边界。View 层EJS 模板里要手动判断if (product.inStock)还要手动引入分页组件、面包屑导航。这还没算上测试、日志、错误页面这些“非功能需求”。而 Geddy 的解决方案是把整个流程压缩成一个可复用的“模式”。当你运行geddy gen resource product name:string price:float category:string inStock:boolean它自动生成路由GET /products,GET /products/:id,POST /products,PUT /products/:id,DELETE /products/:idControllerapp/controllers/products_controller.js里面index方法自动支持req.params.category过滤show方法自动加载关联的category数据如果定义了关系Modelapp/models/product.js内置validatesNumericality(price, {greaterThan: 0})和validatesPresence(name)Viewapp/views/products/index.ejs里直接用% products.forEach(function(p) { %循环% p.name %渲染连分页链接都预置好了% paginate(products) %这种“生成即可用”的能力不是为了炫技而是把 MVC 的核心契约——数据Model、逻辑Controller、展示View的职责分离与协作方式——用代码固化下来。你不用再纠结“这个校验放 Controller 还是 Model”因为 Geddy 规定它必须在 Model你不用再考虑“分页参数怎么传给 View”因为paginate()辅助函数已内置。这种确定性让新人上手第一天就能独立修改一个页面而不是花两天看懂团队的“私有 MVC 规范”。2.2 为什么不是 NestJS 或 AdonisJS—— 轻量级框架的生存智慧NestJS 和 AdonisJS 是当前 Node.js 社区的明星框架它们功能强大、生态完善、TypeScript 支持一流。但 Geddy 的存在价值恰恰在于它主动选择了一条“反主流”的路放弃对前沿特性的追逐专注解决中型项目最痛的“工程效率”问题。我们来拆解几个关键取舍不拥抱 TypeScriptNestJS 的核心卖点是装饰器和 DI 容器这要求你必须用 TS 写。而 Geddy 全部用原生 JavaScript 实现这意味着你可以用const { name } req.body这种直白的解构而不是Body() createProductDto: CreateProductDto这种需要跳转定义的写法调试时直接在 Chrome DevTools 里打断点看到的是你写的 JS 代码而不是编译后的 TS 输出团队里如果有熟悉 Java 的同事他能立刻看懂this.model.find({category: req.params.category})而不用先学 Decorator 元编程。不强制依赖注入AdonisJS 的 IoC 容器很优雅但它的学习曲线在于理解“何时该 bind何时该 make”。Geddy 的做法更朴素所有 Controller 实例化时自动注入this.model对应资源的 Model 类和this.app应用实例。你需要数据库操作直接this.model.create(req.body)需要发邮件this.app.email.send(...)。没有复杂的 provider 注册没有循环依赖警告只有清晰的“this.xxx”调用链。不追求全栈渲染Vue3 Node.js MySQL 商城项目这类热词背后是开发者对 SSR 的渴望。但 Geddy 的 View 层只做一件事把数据安全、高效地塞进 EJS 模板。它不提供useRouter()这样的客户端路由钩子也不封装fetch()请求。它的哲学是前端交给 Vue/React 做后端专注 API 和服务端渲染SSR两者通过清晰的 JSON 接口或 EJS 模板变量通信。这种“各司其职”的简单性让一个 5 人小团队能同时维护前端和后端而不会陷入“前端工程师看不懂后端 DI 配置后端工程师改坏 Vue 组件”的混乱。这种“克制”不是技术落后而是对真实项目复杂度的敬畏。当你的 KPI 是“下周五上线商品管理后台”而不是“打造下一代 Node.js 开发范式”时Geddy 提供的确定性、低学习成本和快速交付能力就是最硬核的生产力。2.3 架构底座基于 EventEmitter 的轻量内核与异步处理模型Geddy 的底层并不神秘它的核心是一个精巧的EventEmitter实例所有生命周期钩子如beforeFilter,afterFilter,onError都基于此。当你访问/products请求流是这样的HTTP Server 接收请求 → 触发router:match事件Router 匹配到GET /products→ 触发controller:load事件加载ProductsControllerController 实例化 → 自动触发controller:init事件注入this.model执行index方法 → 若定义了beforeIndex钩子则先触发filter:beforeIndexindex方法调用this.model.all()→ Model 内部使用geddy.model.Product.all()这是一个 Promise 返回的异步操作Model 查询数据库完成 → 触发model:find:success事件将结果传给 ControllerController 调用this.respond(products)→ 触发response:render事件渲染index.ejs这个事件驱动模型的关键优势在于它天然适配 Node.js 的异步 I/O 特性且所有环节都可被拦截和扩展。比如你想在所有 Controller 的show方法前检查用户权限只需在app/controllers/application_controller.js所有 Controller 的基类里写this.beforeShow function(next) { if (!this.session.userId) { this.redirect(/login); } else { next(); } };这里的next()不是 Express 那种中间件回调而是明确告诉事件系统“继续执行后续的show方法”。这种基于事件的流程控制比堆砌一堆async/await函数链更符合 Node.js 的心智模型——你关注的是“什么时机做什么事”而不是“函数 A 必须等函数 B 的 Promise 解决”。更值得称道的是它的错误处理统一性。无论 Model 查询失败、View 渲染出错还是 Controller 抛出异常最终都会汇聚到app.onError钩子。你可以在config/environment.js里全局配置app.onError function(err, req, res) { geddy.log.error(Unhandled error:, err); if (req.xhr || req.get(Accept).indexOf(application/json) -1) { res.json(500, {error: Internal Server Error}); } else { res.render(errors/500, {status: 500}); } };这种“一处定义处处生效”的错误兜底彻底避免了 Express 项目里常见的try/catch泛滥或next(err)传递遗漏问题。它用最朴素的事件机制实现了企业级应用所需的健壮性。3. 核心模块解析与实操要点3.1 Model 层不只是数据容器而是业务规则的守门人Geddy 的 Model 是整个 MVC 架构的基石它的设计哲学是“数据的合法性必须在进入业务逻辑前就确认”。这直接体现在它的验证Validation和关系Relationship两大核心能力上。验证规则的声明式定义Geddy 的验证不是写在 Controller 里的if (!req.body.price || req.body.price 0)而是直接嵌入 Model 类的静态方法中。以商品模型为例// app/models/product.js var Product function() { this.property(name, string, {required: true, maxLength: 100}); this.property(price, float, {required: true, greaterThan: 0}); this.property(category, string, {required: true, in: [electronics, clothing, books]}); this.property(inStock, boolean, {required: true, default: true}); // 自定义验证确保电子商品必须有品牌 this.validatesWithFunction(brandRequiredForElectronics, function() { if (this.category electronics !this.brand) { this.addInvalid(brand, Electronics must have a brand); } }); }; exports.Product Product;这段代码的威力在于property()方法不仅定义了字段名和类型还内联了业务约束required,maxLength,greaterThan,in。这些约束会在model.create()或model.update()时自动触发无需你在 Controller 里重复写校验逻辑。validatesWithFunction()允许你写任意复杂的业务规则比如“只有电子产品才需要品牌”并且错误信息会精准定位到brand字段前端可以直接显示errors.brand。更重要的是这些验证是可组合、可继承的。如果你有一个BaseProduct模型定义了通用字段ElectronicsProduct可以继承它并添加专属验证// app/models/electronics_product.js var ElectronicsProduct function() { // 继承 BaseProduct 的所有 property 和验证 BaseProduct.call(this); this.property(brand, string, {required: true}); this.property(warrantyMonths, number, {required: true, greaterThan: 0}); }; ElectronicsProduct.prototype new BaseProduct();关系建模用最自然的语法表达数据关联在商品管理系统中“商品属于一个分类”“订单包含多个商品”是典型的一对多关系。Geddy 用极其贴近自然语言的方式定义// app/models/category.js var Category function() { this.property(name, string, {required: true}); // 声明一个 Category 可以有多个 Product this.hasMany(products, {model: Product, foreignKey: categoryId}); }; // app/models/product.js var Product function() { // ... 其他字段 // 声明一个 Product 属于一个 Category this.belongsTo(category, {model: Category, foreignKey: categoryId}); };定义完关系后API 使用变得极其直观// 在 Controller 中 this.show function(req, res) { var self this; // 自动加载关联的 Category 数据无需手动 join this.model.first(req.params.id, {include: [category]}, function(err, product) { if (err) { self.respond({error: Not found}, 404); return; } // product.category.name 直接可用 self.respond(product); }); };这种include: [category]的语法背后是 Geddy 对底层数据库查询的智能优化。它会根据关系定义自动生成SELECT * FROM products JOIN categories ON products.categoryId categories.id这样的 SQL而不是让你手动写knex(products).join(categories, products.categoryId, categories.id)。对于 MongoDB 用户它同样能转换为$lookup聚合管道。这种“写一次多库兼容”的能力让团队在早期选型数据库时少了很多顾虑。提示Geddy 的 Model 默认使用内存存储MemoryAdapter进行开发这极大加速了本地调试。你只需在config/environment.js中切换if (geddy.config.environment development) { geddy.model.adapter memory; } else { geddy.model.adapter postgresql; // 或 mysql, mongodb }内存 Adapter 的所有数据重启即失完美模拟“干净环境”避免了每次调试都要清空数据库的麻烦。3.2 Controller 层职责清晰的请求处理器与数据协调者Geddy 的 Controller 不是 Express 中那种“万能中间件集合”它的核心使命非常明确接收请求、调用 Model 处理数据、决定如何响应渲染 View 或返回 JSON。这种单一职责让它比手写 Express 路由更易维护也比 NestJS 的Controller更易理解。标准 CRUD 方法的自动化与可定制Geddy 为每个资源自动生成index,show,new,create,edit,update,destroy七个标准方法。以index为例它的默认行为是// app/controllers/products_controller.js this.index function(req, res) { var self this; // 自动读取 req.query 中的分页参数page, limit和过滤参数如 category var opts { page: req.query.page || 1, limit: req.query.limit || 20, where: {} }; // 如果 req.query.category 存在自动加入 where 条件 if (req.query.category) { opts.where.category req.query.category; } this.model.all(opts, function(err, products) { if (err) { self.respond({error: err.message}, 500); return; } // 自动计算总页数注入到 View 中 self.respond(products, {total: products.total, page: opts.page, limit: opts.limit}); }); };这个index方法已经包含了分页、过滤、错误处理等常见逻辑。你不需要重写除非有特殊需求。比如你想让“管理员”能看到所有商品而普通用户只能看inStock: true的商品只需覆盖index方法this.index function(req, res) { var self this; var where {}; // 普通用户加库存过滤 if (!this.session.isAdmin) { where.inStock true; } // 管理员可传 category 过滤 if (req.query.category) { where.category req.query.category; } this.model.all({where: where}, function(err, products) { self.respond(products); }); };强大的respond()方法统一的响应出口Geddy 的this.respond(data, options)是 Controller 的灵魂。它根据data的类型和options的配置自动选择最佳响应方式如果data是一个数组或对象且当前请求是 HTMLAccept: text/html则渲染对应的 EJS View如index.ejs并将data作为locals传入如果data是一个数组或对象且请求是 JSONAccept: application/json或X-Requested-With: XMLHttpRequest则直接res.json(200, data)如果data是一个字符串且options.status存在则发送纯文本响应如果data是一个Error对象则触发全局onError钩子。这种智能响应让你在写 API 时完全不用区分res.json()和res.render()。同一个index方法既可以用浏览器访问看到 HTML 页面也可以用curl -H Accept: application/json获取 JSON 数据真正做到“一套逻辑多端复用”。注意respond()的options参数还支持template指定渲染哪个模板、layout指定布局文件、statusHTTP 状态码等。例如this.respond({message: Created}, {status: 201})会返回201 Created状态码。3.3 View 层EJS 模板的极致简化与安全渲染Geddy 的 View 层选择了 EJSEmbedded JavaScript作为默认模板引擎原因很实在它足够简单能让后端工程师不学新语法就能上手它足够强大能支撑复杂的页面逻辑它足够安全能防止 XSS 攻击。它没有 Vue 的响应式也没有 React 的 JSX但它用最朴素的% %和% %语法完成了所有服务端渲染任务。EJS 的安全默认自动 HTML 转义Geddy 的 EJS 配置默认开启escape选项这意味着% user.name %会自动将user.name中的script标签转义为lt;scriptgt;从根本上杜绝了 XSS。如果你确实需要渲染原始 HTML比如富文本编辑器内容必须显式使用%- %!-- 安全自动转义 -- h1% product.name %/h1 !-- 危险需自行确保 content 安全 -- div classdescription%- product.description %/div这种“安全为默认危险需显式声明”的设计比很多框架要求开发者手动调用sanitizeHtml()更可靠。内置辅助函数让模板逻辑保持简洁Geddy 在 EJS 模板中注入了一系列实用的辅助函数Helpers让常用操作一行代码搞定linkTo(text, path, options)生成带属性的a标签% linkTo(Edit, /products/ product.id /edit, {class: btn btn-primary}) %渲染为a href/products/123/edit classbtn btn-primaryEdit/aformFor(object, options)生成带 CSRF token 的表单%- formFor(product, {action: /products/ product.id, method: PUT}) %渲染为form action/products/123 methodPOSTinput typehidden name_method valuePUT...input typehidden name_csrf valueabc123paginate(collection)生成分页链接% paginate(products) %渲染为div classpagination a href/products?page1laquo;/a a href/products?page1 classcurrent1/a a href/products?page22/a ... /div这些 Helper 不是黑盒你可以在app/helpers/application_helper.js中查看源码甚至可以覆盖它们。比如你想让所有linkTo生成的链接都加上>// app/helpers/application_helper.js exports.linkTo function(text, path, options) { options options || {}; options[data-track] click; return geddy.helpers.linkTo(text, path, options); };这种“可定制的便利”让 Geddy 的 View 层既保持了简单性又不失灵活性。4. 实操过程与核心环节实现4.1 从零搭建一个商品管理系统完整步骤与配置详解现在让我们动手搭建一个最小可行的商品管理系统Product Management System它将包含商品列表、详情、创建、编辑功能。整个过程严格遵循 Geddy 的约定确保你能在 15 分钟内看到第一个页面。第一步环境准备与初始化确保你已安装 Node.js推荐 v18.x LTS。Geddy 本身是一个全局 CLI 工具# 全局安装 Geddy CLI npm install -g geddy # 创建新项目 geddy app product-manager # 进入项目目录 cd product-manager # 启动开发服务器 geddy此时访问http://localhost:4000你应该能看到 Geddy 的欢迎页面。注意geddy命令会自动启动服务器并监听config/environment.js中的port配置默认 4000。第二步生成商品资源骨架这是最关键的一步它将自动生成 MVC 的所有基础文件# 在项目根目录下执行 geddy gen resource product name:string price:float category:string description:text inStock:boolean这条命令会创建Modelapp/models/product.js含字段定义和基础验证Controllerapp/controllers/products_controller.js含 7 个 CRUD 方法Viewsapp/views/products/目录下的index.ejs,show.ejs,new.ejs,edit.ejs路由自动添加到config/router.js中第三步配置数据库连接以 PostgreSQL 为例编辑config/environment.js找到production和development环境配置块添加数据库配置// config/environment.js if (geddy.config.environment development) { geddy.model.adapter postgresql; geddy.model.connection { host: localhost, port: 5432, database: product_manager_dev, username: your_db_user, password: your_db_password }; }然后创建数据库假设你已安装 PostgreSQL# 登录 psql psql -U your_db_user # 创建数据库 CREATE DATABASE product_manager_dev;第四步运行迁移Migration创建数据表Geddy 的迁移工具会根据 Model 定义自动生成 SQL 并执行。首先生成迁移文件geddy gen migration create_products_table这会在db/migrate/下创建一个时间戳命名的.js文件。编辑它填入表结构// db/migrate/20231015120000_create_products_table.js exports.up function(next, conn) { conn.query( CREATE TABLE products ( id SERIAL PRIMARY KEY, name VARCHAR(100) NOT NULL, price FLOAT, category VARCHAR(50), description TEXT, in_stock BOOLEAN DEFAULT true, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() );, next ); }; exports.down function(next, conn) { conn.query(DROP TABLE products;, next); };执行迁移geddy migrate第五步启动并验证再次运行geddy访问http://localhost:4000/products/new。你应该能看到一个表单包含name,price,category,description,inStock字段。提交表单数据会被存入 PostgreSQL然后重定向到http://localhost:4000/products显示商品列表。实操心得如果你在 Windows 上遇到node.js安装提示windos无法打开此类型的文件这类错误通常是因为 Node.js 安装包下载不完整。请务必从 https://nodejs.org 官网下载.msi安装包而不是第三方镜像。安装时勾选 “Add to PATH” 选项确保npm命令在任意目录下都能执行。4.2 关键配置项深度解析让框架为你工作Geddy 的配置文件config/environment.js是整个应用的“中枢神经”理解它的核心配置项能让你避开 90% 的部署陷阱。geddy.model.adapter与connection数据库适配器的正确打开方式Geddy 支持多种数据库但配置方式高度统一。关键点在于adapter必须是字符串值为memory,postgresql,mysql,mongodb之一connection对象的字段取决于adapterpostgresql/mysql需要host,port,database,username,passwordmongodb需要url如mongodb://localhost:27017/product-managermemory无需connection直接geddy.model.adapter memory一个常见的错误是在development环境下忘记设置connection导致 Geddy 默认使用memory而你以为数据存进了 PostgreSQL。务必在console.log(geddy.model.adapter)和console.log(geddy.model.connection)来确认。geddy.config.static静态资源服务的黄金配置Geddy 默认会托管public/目录下的静态文件CSS, JS, 图片。但生产环境常需调整geddy.config.static { // 启用 gzip 压缩减小传输体积 gzip: true, // 设置缓存头让浏览器缓存 1 天 maxAge: 24 * 60 * 60 * 1000, // 指定静态文件根目录可选 root: path.join(__dirname, .., public) };geddy.config.sessions会话管理的安全实践Geddy 使用connect-session其安全性取决于secret和storegeddy.config.sessions { // 必须设置一个长且随机的 secret用于签名 session ID secret: process.env.SESSION_SECRET || a-very-long-and-random-string-32-chars-min, // 生产环境务必使用 Redis 或数据库存储而非内存 store: redis, // Redis 连接配置 redis: { host: localhost, port: 6379, db: 0 } };注意process.env.SESSION_SECRET是最佳实践避免将密钥硬编码在代码中。在生产服务器上通过export SESSION_SECRETyour-secret设置环境变量。4.3 性能调优与生产部署让 Geddy 稳如磐石Geddy 本身是一个轻量框架但生产环境的稳定性更多取决于你的部署策略和配置。进程管理永远不要用node app.js启动Geddy 应用必须用geddy命令启动因为它会加载完整的配置和生命周期钩子。但生产环境需要进程守护推荐 PM2最成熟# 全局安装 npm install -g pm2 # 启动 Geddy 应用 pm2 start --name product-manager --watch --ignore-watchnode_modules --env production ./bin/cli.js # 查看日志 pm2 logs product-managerDocker 部署标准化创建DockerfileFROM node:18-alpine WORKDIR /app COPY package*.json ./ RUN npm ci --onlyproduction COPY . . EXPOSE 4000 CMD [npm, start]对应的package.json脚本scripts: { start: geddy -e production }日志与监控让问题无处遁形Geddy 内置geddy.log但生产环境需要集中化// config/environment.js if (geddy.config.environment production) { // 将日志输出到文件 geddy.log.transports.file { filename: /var/log/product-manager/app.log, json: false, maxsize: 10485760, // 10MB maxFiles: 5 }; // 同时输出到 console便于 PM2 捕获 geddy.log.transports.console { colorize: false }; }然后用pm2 logrotate插件自动轮转日志或用logrotate系统工具。5. 常见问题与排查技巧实录5.1 典型问题速查表从报错信息直达解决方案报错信息根本原因解决方案Error: Cannot find module geddy全局安装失败或 PATH 未配置运行npm list -g geddy检查是否安装若未安装执行npm install -g geddyWindows 用户检查环境变量 PATH 是否包含C:\Users\YourName\AppData\Roaming\npmError: connect ECONNREFUSED 127.0.0.1:5432PostgreSQL 服务未启动或端口错误运行sudo service postgresql statusLinux/macOS或检查 Windows 服务确认config/environment.js中port与 PostgreSQL 配置一致ReferenceError: product is not defined in /app/views/products/index.ejsController 的respond()未传入product变量或变量名不匹配检查products_controller.js中this.respond(products)的参数名在 EJS 中用% JSON.stringify(locals) %打印所有可用变量Error: Invalid csrf token表单提交时未携带_csrftoken或 token 过期确保表单使用%- formFor(...) %生成检查config/environment.js中sessions.secret是否设置且未重启应用重启会重置内存 session storeTypeError: Cannot read property forEach of undefinedthis.model.all()查询返回undefined通常是 Model 名称拼写错误检查app/models/product.js的exports.Product Product;导出是否正确确认config/router.js中router.get(/products, controller: products, action: index)的controller名称与文件名products_controller.js一致去掉_controller.js后缀5.2 深度排查技巧从现象到本质的思维路径技巧一利用 Geddy 的调试模式逐层剥离问题当页面空白或报错时不要急于 Google先启用 Geddy 的调试模式# 启动时添加 --debug 参数 geddy --debug这会让 Geddy 在控制台输出详细的请求生命周期日志例如[DEBUG] router:match - Matched GET /products to controller: products, action: index [DEBUG] controller:load - Loading controller products [DEBUG] controller:init - Initializing ProductsController [DEBUG] model:all - Querying Product model with options: {page:1, limit:20} [ERROR] model:all:failure - Error: relation products does not exist