nodejs-express-mongodb-backend-pattern

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Node.js Express MongoDB Backend Pattern

Node.js Express MongoDB 后端模板

Purpose

用途

This skill helps the agent scaffold, explain, or adapt the nodejs-express-mongodb-backend-pattern template — a Node.js REST API boilerplate with observability, security, and resilience built in. The agent should use it when the user wants a backend with Express, MongoDB, Redis, Sentry, JWT authentication, and production-oriented middleware.
该技能帮助Agent生成、解释或适配 nodejs-express-mongodb-backend-pattern 模板——这是一个内置可观测性、安全性和弹性能力的Node.js REST API样板。当用户需要基于Express、MongoDB、Redis、Sentry、JWT鉴权和生产导向中间件搭建后端时,Agent应使用该技能。

When to use this skill

何时使用该技能

  • The user wants to create a new REST API backend with Node.js and Express.
  • The user asks for a production-ready or "observable" Node.js API template.
  • The user mentions MongoDB/Mongoose, Redis, Sentry, rate limiting, or JWT auth and wants a starter.
  • The user wants to clone, fork, or understand the structure of this repo (nodejs-express-mongodb-backend-pattern).
  • The user needs steps to set up env vars, run the app, or add routes/controllers following this template's conventions.
  • 用户想要使用Node.js和Express创建新的REST API后端。
  • 用户需要生产就绪或「可观测」的Node.js API模板。
  • 用户提到MongoDB/Mongoose、Redis、Sentry、限流或JWT鉴权,需要启动项目的基础模板。
  • 用户想要克隆、复刻或理解该仓库(nodejs-express-mongodb-backend-pattern)的结构。
  • 用户需要按照该模板的规范完成环境变量配置、启动应用、新增路由/控制器的步骤指导。

When NOT to use this skill

何时不使用该技能

  • The user wants a frontend or full-stack framework (Next.js, Nuxt, etc.).
  • The user explicitly wants a different DB (PostgreSQL, MySQL) without Mongoose.
  • The user wants a serverless/lambda architecture instead of a long-running Express server.
  • 用户想要前端或全栈框架(Next.js、Nuxt等)。
  • 用户明确要求使用不包含Mongoose的其他数据库(PostgreSQL、MySQL)。
  • 用户想要Serverless/Lambda架构,而非长期运行的Express服务。

How to use this template

如何使用该模板

1. Clone and install

1. 克隆并安装依赖

bash
git clone https://github.com/laskar-ksatria/building-observable-nodejs-api.git
cd building-observable-nodejs-api
npm install
bash
git clone https://github.com/laskar-ksatria/building-observable-nodejs-api.git
cd building-observable-nodejs-api
npm install

2. Environment setup

2. 环境配置

  • Create a
    .env
    file in the project root.
  • Required variables:
    PORT
    ,
    MONGODB_URI
    ,
    PRIVATE_KEY
    ,
    TOKEN_EXPIRED
    . Optional:
    SENTRY_DSN
    ,
    REDIS_HOST
    ,
    REDIS_PORT
    ,
    REDIS_PASSWORD
    . App exits on startup if required vars are missing. Redis is used for optional caching (e.g. cache GET /api/user for 60s); if Redis is not set, the app runs without cache.
  • Generate a JWT secret for
    PRIVATE_KEY
    :
    • Node:
      node -e "console.log(require('crypto').randomBytes(64).toString('base64'))"
    • OpenSSL:
      openssl rand -hex 64
  • 在项目根目录创建
    .env
    文件。
  • 必需变量:
    PORT
    MONGODB_URI
    PRIVATE_KEY
    TOKEN_EXPIRED
    。可选变量:
    SENTRY_DSN
    REDIS_HOST
    REDIS_PORT
    REDIS_PASSWORD
    。如果缺少必需变量,应用启动时会自动退出。Redis用于可选缓存(例如将GET /api/user接口缓存60秒);如果未配置Redis,应用会跳过缓存正常运行。
  • PRIVATE_KEY
    生成JWT密钥:
    • Node命令:
      node -e "console.log(require('crypto').randomBytes(64).toString('base64'))"
    • OpenSSL命令:
      openssl rand -hex 64

3. Run the app

3. 运行应用

  • Development:
    npm run dev
  • Build:
    npm run build
    then
    npm start
  • 开发环境:
    npm run dev
  • 生产构建:
    npm run build
    后执行
    npm start

4. API surface

4. API接口

  • Base path:
    /api
  • POST /api/user/register
    — register user (email, full_name, password)
  • POST /api/user/login
    — login, returns JWT
  • GET /api/user
    — current user (header:
    Authorization: Bearer <token>
    )
  • GET /
    — health check
  • 基础路径:
    /api
  • POST /api/user/register
    —— 用户注册(参数:email、full_name、password)
  • POST /api/user/login
    —— 用户登录,返回JWT
  • GET /api/user
    —— 获取当前用户信息(请求头:
    Authorization: Bearer <token>
  • GET /
    —— 健康检查接口

Key features

核心特性

  • Stack: Express 5, TypeScript, Mongoose, Redis (ioredis), Sentry, Helmet, CORS.
  • Security: NoSQL injection prevention, rejection of HTML in input, Helmet, CORS.
  • Auth: JWT + bcrypt; password is hashed on save and never returned in JSON.
  • Resilience: Per-route rate limiting, overload detection (toobusy), structured HttpError and fallback to Sentry for unexpected errors.
  • Structure:
    src/
    with config, controllers, errors, lib, middlewares, models, routes, services, types; entry in
    server.ts
    with Sentry init.

  • 技术栈:Express 5、TypeScript、Mongoose、Redis(ioredis)、Sentry、Helmet、CORS。
  • 安全能力:NoSQL注入防护、输入内容HTML拦截、Helmet安全头、CORS配置。
  • 鉴权体系:JWT + bcrypt;密码存储时自动哈希,永远不会在JSON响应中返回密码字段。
  • 弹性能力:路由粒度限流、过载检测(toobusy)、结构化HttpError,未预期错误自动上报Sentry。
  • 项目结构
    src/
    目录下包含config、controllers、errors、lib、middlewares、models、routes、services、types等模块;入口文件为
    server.ts
    ,包含Sentry初始化逻辑。

Conventions (IMPORTANT — follow these when generating code)

开发规范(重要——生成代码时请严格遵守)

File naming

文件命名规范

  • Models:
    src/models/<entity>.model.ts
  • Controllers:
    src/controllers/<entity>.controller.ts
  • Routes:
    src/routes/<entity>.route.ts
  • Middlewares:
    src/middlewares/<name>.ts
  • Services:
    src/services/<name>.ts
  • All file names use kebab-case.
  • 模型:
    src/models/<实体名>.model.ts
  • 控制器:
    src/controllers/<实体名>.controller.ts
  • 路由:
    src/routes/<实体名>.route.ts
  • 中间件:
    src/middlewares/<名称>.ts
  • 服务:
    src/services/<名称>.ts
  • 所有文件名使用 短横线命名法(kebab-case)

Response format

响应格式规范

Every API response follows this shape:
Success:
json
{ "success": true, "data": { ... } }
Error:
json
{ "success": false, "error": { "error_id": 0, "message": "...", "errors": [] } }
所有API响应都遵循以下结构:
成功响应:
json
{ "success": true, "data": { ... } }
错误响应:
json
{ "success": false, "error": { "error_id": 0, "message": "...", "errors": [] } }

Middleware order in
app.ts

app.ts
中的中间件顺序

Order matters. Follow this exact sequence:
  1. helmet()
    — security headers
  2. cors()
    — cross-origin config
  3. express.json()
    — parse JSON body (with size limit)
  4. express.urlencoded()
    — parse URL-encoded body
  5. securityMiddleware
    — NoSQL injection + HTML sanitization
  6. toobusy
    check — overload detection (HTTP 529)
  7. Cache-Control header —
    no-store
  8. Routes
    app.use("/api", indexRoute)
  9. Health check —
    app.get("/")
  10. ErrorHandling
    must be last (global error handler)
中间件的顺序会影响功能生效逻辑,请严格遵循以下顺序:
  1. helmet()
    —— 安全响应头
  2. cors()
    —— 跨域配置
  3. express.json()
    —— 解析JSON请求体(带大小限制)
  4. express.urlencoded()
    —— 解析URL编码的请求体
  5. securityMiddleware
    —— NoSQL注入防护 + HTML内容清理
  6. toobusy
    检查 —— 服务过载检测(返回HTTP 529)
  7. Cache-Control响应头 —— 设置为
    no-store
  8. 路由注册 ——
    app.use("/api", indexRoute)
  9. 健康检查接口 ——
    app.get("/")
  10. ErrorHandling
    —— 必须放在最后(全局错误处理器)

Error handling convention

错误处理规范

  • Known/expected errors: throw
    new HttpError(errorStates.someError)
    . These return the configured HTTP code and message to the client.
  • Adding a new error state: add an entry to
    errorStates
    in
    src/errors/index.ts
    with a unique
    error_id
    ,
    message
    , and
    http_code
    .
  • Sentry reporting: unexpected errors (anything that is NOT an
    HttpError
    ) are automatically sent to Sentry. For known errors that you still want to report, set
    sentry: true
    in the error state.
  • Mongoose validation errors: automatically collected and returned in the
    errors
    array.
  • 已知/预期错误:抛出
    new HttpError(errorStates.someError)
    。这类错误会返回配置好的HTTP状态码和提示信息给客户端。
  • 新增错误状态:在
    src/errors/index.ts
    errorStates
    中新增条目,包含唯一的
    error_id
    message
    http_code
  • Sentry上报:非
    HttpError
    类型的未预期错误会自动上报到Sentry。如果需要上报已知错误,可以在错误状态配置中设置
    sentry: true
  • Mongoose校验错误:会自动收集并返回在
    errors
    数组中。

Adding a new environment variable

新增环境变量步骤

  1. Add the key to
    .env
  2. Add it to
    src/env.ts
    in the
    env
    object
  3. Use it via
    import env from "../env"
    then
    env.YOUR_VAR
  1. .env
    文件中添加变量名
  2. src/env.ts
    env
    对象中添加对应配置
  3. 使用时通过
    import env from "../env"
    引入,再调用
    env.YOUR_VAR

Dependencies

依赖列表

json
{
  "dependencies": {
    "@sentry/node": "^8.x",
    "bcrypt": "^5.x",
    "cors": "^2.x",
    "dotenv": "^17.x",
    "express": "^5.x",
    "express-rate-limit": "^8.x",
    "helmet": "^8.x",
    "ioredis": "^5.x",
    "jsonwebtoken": "^9.x",
    "mongoose": "^9.x",
    "toobusy-js": "^0.5.x"
  },
  "devDependencies": {
    "@types/bcrypt": "^5.x",
    "@types/cors": "^2.x",
    "@types/express": "^4.x",
    "@types/ioredis": "^4.x",
    "@types/jsonwebtoken": "^9.x",
    "@types/toobusy-js": "^0.5.x",
    "nodemon": "^3.x",
    "ts-node": "^10.x",
    "tsx": "^4.x",
    "typescript": "^5.x"
  }
}

json
{
  "dependencies": {
    "@sentry/node": "^8.x",
    "bcrypt": "^5.x",
    "cors": "^2.x",
    "dotenv": "^17.x",
    "express": "^5.x",
    "express-rate-limit": "^8.x",
    "helmet": "^8.x",
    "ioredis": "^5.x",
    "jsonwebtoken": "^9.x",
    "mongoose": "^9.x",
    "toobusy-js": "^0.5.x"
  },
  "devDependencies": {
    "@types/bcrypt": "^5.x",
    "@types/cors": "^2.x",
    "@types/express": "^4.x",
    "@types/ioredis": "^4.x",
    "@types/jsonwebtoken": "^9.x",
    "@types/toobusy-js": "^0.5.x",
    "nodemon": "^3.x",
    "ts-node": "^10.x",
    "tsx": "^4.x",
    "typescript": "^5.x"
  }
}

How to add a new resource (step-by-step)

新增资源步骤(分步指南)

When the user asks to add a new entity (e.g. "Product"), follow these steps in order:
当用户要求新增实体(例如「Product」)时,按以下顺序操作:

Step 1 — Define types in
src/types/index.ts

步骤1 —— 在
src/types/index.ts
中定义类型

ts
// Product
export interface IProduct {
  name: string;
  price: number;
  description: string;
}

export interface IProductDocument extends IProduct, Document {}
ts
// Product
export interface IProduct {
  name: string;
  price: number;
  description: string;
}

export interface IProductDocument extends IProduct, Document {}

Step 2 — Create model
src/models/product.model.ts

步骤2 —— 创建模型文件
src/models/product.model.ts

ts
import { Schema, model } from "mongoose";
import { IProductDocument } from "../types";

const productSchema = new Schema<IProductDocument>(
  {
    name: { type: String, required: true },
    price: { type: Number, required: true },
    description: { type: String, required: true },
  },
  { versionKey: false, timestamps: true },
);

export const ProductModel = model<IProductDocument>("Product", productSchema);
ts
import { Schema, model } from "mongoose";
import { IProductDocument } from "../types";

const productSchema = new Schema<IProductDocument>(
  {
    name: { type: String, required: true },
    price: { type: Number, required: true },
    description: { type: String, required: true },
  },
  { versionKey: false, timestamps: true },
);

export const ProductModel = model<IProductDocument>("Product", productSchema);

Step 3 — Create controller
src/controllers/product.controller.ts

步骤3 —— 创建控制器文件
src/controllers/product.controller.ts

ts
import { Request, Response, NextFunction } from "express";
import { ProductModel } from "../models/product.model";
import HttpError, { errorStates } from "../errors";

class ProductController {
  static async create(req: Request, res: Response, next: NextFunction) {
    try {
      const product = await ProductModel.create(req.body);
      return res.status(201).json({ success: true, data: { product } });
    } catch (error) {
      next(error);
    }
  }

  static async getAll(req: Request, res: Response, next: NextFunction) {
    try {
      const products = await ProductModel.find();
      return res.status(200).json({ success: true, data: { products } });
    } catch (error) {
      next(error);
    }
  }
}

export default ProductController;
ts
import { Request, Response, NextFunction } from "express";
import { ProductModel } from "../models/product.model";
import HttpError, { errorStates } from "../errors";

class ProductController {
  static async create(req: Request, res: Response, next: NextFunction) {
    try {
      const product = await ProductModel.create(req.body);
      return res.status(201).json({ success: true, data: { product } });
    } catch (error) {
      next(error);
    }
  }

  static async getAll(req: Request, res: Response, next: NextFunction) {
    try {
      const products = await ProductModel.find();
      return res.status(200).json({ success: true, data: { products } });
    } catch (error) {
      next(error);
    }
  }
}

export default ProductController;

Step 4 — Create route
src/routes/product.route.ts

步骤4 —— 创建路由文件
src/routes/product.route.ts

ts
import ProductController from "../controllers/product.controller";
import { Router } from "express";
import { RateLimit } from "../services/rate-limit";
import Authentication from "../middlewares/auth";

const router = Router();

router.post("/", RateLimit({ max: 10, ms: 60000 }), Authentication, ProductController.create);
router.get("/", RateLimit({ max: 20, ms: 60000 }), ProductController.getAll);

export default router;
ts
import ProductController from "../controllers/product.controller";
import { Router } from "express";
import { RateLimit } from "../services/rate-limit";
import Authentication from "../middlewares/auth";

const router = Router();

router.post("/", RateLimit({ max: 10, ms: 60000 }), Authentication, ProductController.create);
router.get("/", RateLimit({ max: 20, ms: 60000 }), ProductController.getAll);

export default router;

Step 5 — Mount in
src/routes/index.ts

步骤5 —— 在
src/routes/index.ts
中挂载路由

ts
import { Router } from "express";
import userRoute from "./user.route";
import productRoute from "./product.route";

const router = Router();
router.use("/user", userRoute);
router.use("/product", productRoute);
export default router;

ts
import { Router } from "express";
import userRoute from "./user.route";
import productRoute from "./product.route";

const router = Router();
router.use("/user", userRoute);
router.use("/product", productRoute);
export default router;

Code examples

代码示例

The following snippets are the actual code from this template. Use them as the reference pattern when scaffolding a new project.
以下代码片段是该模板的实际源码,生成新项目时可以作为参考规范使用。

Project structure

项目结构

src/
├── config/
│   └── mongodb.ts          # MongoDB connection
├── controllers/
│   └── user.controller.ts  # Request handlers
├── errors/
│   └── index.ts            # HttpError, errorStates
├── lib/
│   └── utils.ts            # bcrypt, emailRegex
├── middlewares/
│   ├── auth.ts             # JWT verification
│   ├── error-handling.ts   # Global error + Sentry
│   └── security.ts         # NoSQL/HTML sanitization
├── models/
│   └── user.model.ts       # Mongoose schema
├── routes/
│   ├── index.ts            # Mounts sub-routes
│   └── user.route.ts       # /api/user routes
├── services/
│   ├── jwt.ts              # GenerateToken, VerifyToken
│   ├── rate-limit.ts       # Per-route rate limiter
│   └── redis.ts            # Optional cache (getCache, setCache, deleteCache)
├── types/
│   └── index.ts            # Shared interfaces
├── app.ts                  # Express app + middleware
├── env.ts                  # Load and export env
└── server.ts               # Entry, Sentry.init, listen
src/
├── config/
│   └── mongodb.ts          # MongoDB连接配置
├── controllers/
│   └── user.controller.ts  # 请求处理逻辑
├── errors/
│   └── index.ts            # HttpError类、错误状态定义
├── lib/
│   └── utils.ts            # bcrypt工具、邮箱正则校验
├── middlewares/
│   ├── auth.ts             # JWT校验中间件
│   ├── error-handling.ts   # 全局错误处理 + Sentry上报
│   └── security.ts         # NoSQL/HTML内容清理
├── models/
│   └── user.model.ts       # Mongoose schema定义
├── routes/
│   ├── index.ts            # 子路由统一挂载
│   └── user.route.ts       # /api/user相关路由
├── services/
│   ├── jwt.ts              # 生成Token、校验Token方法
│   ├── rate-limit.ts       # 路由粒度限流工具
│   └── redis.ts            # 可选缓存方法(getCache、setCache、deleteCache)
├── types/
│   └── index.ts            # 全局共享接口定义
├── app.ts                  # Express实例初始化 + 中间件注册
├── env.ts                  # 环境变量加载与导出
└── server.ts               # 项目入口、Sentry初始化、服务启动

Env config (
src/env.ts
)

环境变量配置(
src/env.ts

ts
import { config } from "dotenv";
config();

const env = {
  PORT: `${process.env.PORT}`,
  REDIS_HOST: `${process.env.REDIS_HOST}`,
  REDIS_PORT: `${process.env.REDIS_PORT}`,
  SENTRY_DSN: `${process.env.SENTRY_DSN}`,
  TOKEN_EXPIRED: `${process.env.TOKEN_EXPIRED}`,
  PRIVATE_KEY: `${process.env.PRIVATE_KEY}`,
  MONGODB_URI: process.env.MONGODB_URI!,
  REDIS_PASSWORD: `${process.env.REDIS_PASSWORD}`,
};

export default env;
ts
import { config } from "dotenv";
config();

const env = {
  PORT: `${process.env.PORT}`,
  REDIS_HOST: `${process.env.REDIS_HOST}`,
  REDIS_PORT: `${process.env.REDIS_PORT}`,
  SENTRY_DSN: `${process.env.SENTRY_DSN}`,
  TOKEN_EXPIRED: `${process.env.TOKEN_EXPIRED}`,
  PRIVATE_KEY: `${process.env.PRIVATE_KEY}`,
  MONGODB_URI: process.env.MONGODB_URI!,
  REDIS_PASSWORD: `${process.env.REDIS_PASSWORD}`,
};

export default env;

Server entry (
src/server.ts
)

服务入口(
src/server.ts

ts
import * as Sentry from "@sentry/node";
import { server } from "./app";
import toobusy from "toobusy-js";
import env from "./env";
import dbConnect from "./config/mongodb";

if (env.SENTRY_DSN) {
  Sentry.init({ dsn: env.SENTRY_DSN, environment: process.env.NODE_ENV ?? "development" });
}

async function main() {
  await dbConnect();
  server.listen(env.PORT, () => {
    console.log(`Server running on http://localhost:${env.PORT}`);
  });
  process.on("SIGINT", () => { toobusy.shutdown(); process.exit(); });
  process.on("exit", () => toobusy.shutdown());
}

main().catch((err) => { console.error(err); process.exit(1); });
ts
import * as Sentry from "@sentry/node";
import { server } from "./app";
import toobusy from "toobusy-js";
import env from "./env";
import dbConnect from "./config/mongodb";

if (env.SENTRY_DSN) {
  Sentry.init({ dsn: env.SENTRY_DSN, environment: process.env.NODE_ENV ?? "development" });
}

async function main() {
  await dbConnect();
  server.listen(env.PORT, () => {
    console.log(`Server running on http://localhost:${env.PORT}`);
  });
  process.on("SIGINT", () => { toobusy.shutdown(); process.exit(); });
  process.on("exit", () => toobusy.shutdown());
}

main().catch((err) => { console.error(err); process.exit(1); });

Database config (
src/config/mongodb.ts
)

数据库配置(
src/config/mongodb.ts

ts
import mongoose from "mongoose";
import env from "../env";
import * as Sentry from "@sentry/node";

export default async function dbConnect(): Promise<void> {
  try {
    await mongoose.connect(env.MONGODB_URI);
    mongoose.connection.on("error", (err) => {
      Sentry.captureException(err);
      console.error("MongoDB connection error:", err);
    });
    console.log("Connected to MongoDB");
  } catch (error) {
    console.error("Failed to connect to MongoDB:", error);
    Sentry.captureException(error);
    process.exit(1);
  }
}
ts
import mongoose from "mongoose";
import env from "../env";
import * as Sentry from "@sentry/node";

export default async function dbConnect(): Promise<void> {
  try {
    await mongoose.connect(env.MONGODB_URI);
    mongoose.connection.on("error", (err) => {
      Sentry.captureException(err);
      console.error("MongoDB connection error:", err);
    });
    console.log("Connected to MongoDB");
  } catch (error) {
    console.error("Failed to connect to MongoDB:", error);
    Sentry.captureException(error);
    process.exit(1);
  }
}

Types (
src/types/index.ts
)

类型定义(
src/types/index.ts

ts
import { Document, Types } from "mongoose";
import { Request } from "express";

export type TGenerateToken = { id: Types.ObjectId };

export interface IAuthRequest extends Request {
  decoded?: TGenerateToken;
}

export interface IErrorMessage {
  message: string;
  error_id: number;
  http_code: number;
  sentry?: boolean;
}

export type CreateLimitType = { max: number; ms: number };

export interface IUser {
  email: string;
  full_name: string;
  password: string;
}

export interface IUserModel extends IUser {
  _id: Types.ObjectId;
}

export interface IUserDocument extends IUser, Document {}
ts
import { Document, Types } from "mongoose";
import { Request } from "express";

export type TGenerateToken = { id: Types.ObjectId };

export interface IAuthRequest extends Request {
  decoded?: TGenerateToken;
}

export interface IErrorMessage {
  message: string;
  error_id: number;
  http_code: number;
  sentry?: boolean;
}

export type CreateLimitType = { max: number; ms: number };

export interface IUser {
  email: string;
  full_name: string;
  password: string;
}

export interface IUserModel extends IUser {
  _id: Types.ObjectId;
}

export interface IUserDocument extends IUser, Document {}

Model (
src/models/user.model.ts
)

模型定义(
src/models/user.model.ts

ts
import { emailRegex, hashPassword } from "../lib/utils";
import { Schema, model } from "mongoose";
import { IUserDocument } from "../types";

const userSchema = new Schema<IUserDocument>(
  {
    email: {
      type: String,
      required: true,
      unique: true,
      index: true,
      validate: {
        validator: (value: string) => emailRegex.test(value),
        message: "Invalid email address",
      },
    },
    full_name: { type: String, required: true },
    password: { type: String, required: true },
  },
  { versionKey: false, timestamps: true },
);

userSchema.pre("save", async function () {
  if (!this.isModified("password")) return;
  this.password = await hashPassword(this.password);
});

export const UserModel = model<IUserDocument>("User", userSchema);
ts
import { emailRegex, hashPassword } from "../lib/utils";
import { Schema, model } from "mongoose";
import { IUserDocument } from "../types";

const userSchema = new Schema<IUserDocument>(
  {
    email: {
      type: String,
      required: true,
      unique: true,
      index: true,
      validate: {
        validator: (value: string) => emailRegex.test(value),
        message: "Invalid email address",
      },
    },
    full_name: { type: String, required: true },
    password: { type: String, required: true },
  },
  { versionKey: false, timestamps: true },
);

userSchema.pre("save", async function () {
  if (!this.isModified("password")) return;
  this.password = await hashPassword(this.password);
});

export const UserModel = model<IUserDocument>("User", userSchema);

Controller (
src/controllers/user.controller.ts
)

控制器定义(
src/controllers/user.controller.ts

ts
import { Request, Response, NextFunction } from "express";
import { UserModel } from "../models/user.model";
import { comparePassword } from "../lib/utils";
import { IAuthRequest, IUserDocument } from "../types";
import HttpError, { errorStates } from "../errors";
import { GenerateToken } from "../services/jwt";
import { getCache, setCache, CACHE_USER } from "../services/redis";

class UserController {
  static async createUser(req: Request, res: Response, next: NextFunction) {
    try {
      const { email, full_name, password } = req.body;
      const user = (await UserModel.create({ email, full_name, password })) as IUserDocument;
      const access_token = GenerateToken({ id: user._id });
      return res.status(201).json({
        success: true,
        data: {
          access_token,
          user: { _id: user._id, full_name: user.full_name, email: user.email },
        },
      });
    } catch (error) {
      next(error);
    }
  }

  static async loginUser(req: Request, res: Response, next: NextFunction) {
    try {
      const { email, password } = req.body;
      const user = (await UserModel.findOne({ email })) as IUserDocument;
      if (!user) throw new HttpError(errorStates.invalidEmailOrPassword);
      const valid = await comparePassword(password, user.password);
      if (!valid) throw new HttpError(errorStates.invalidEmailOrPassword);
      const access_token = GenerateToken({ id: user._id });
      const { password: _p, ...safeUser } = user.toObject();
      return res.status(200).json({ success: true, data: { user: safeUser, access_token } });
    } catch (error) {
      next(error);
    }
  }

  static async getUser(req: IAuthRequest, res: Response, next: NextFunction) {
    try {
      const userId = req?.decoded?.id;
      if (!userId) throw new HttpError(errorStates.failedAuthentication);

      const idStr = String(userId);
      const cacheKey = CACHE_USER(idStr);
      const cached = await getCache(cacheKey);
      if (cached) {
        const user = JSON.parse(cached);
        return res.status(200).json({ success: true, data: { user } });
      }

      const user = await UserModel.findById(userId);
      if (!user) throw new HttpError(errorStates.failedAuthentication);
      const { password: _p, ...safeUser } = user.toObject();
      await setCache(cacheKey, JSON.stringify(safeUser));

      return res.status(200).json({ success: true, data: { user: safeUser } });
    } catch (error) {
      next(error);
    }
  }
}

export default UserController;
ts
import { Request, Response, NextFunction } from "express";
import { UserModel } from "../models/user.model";
import { comparePassword } from "../lib/utils";
import { IAuthRequest, IUserDocument } from "../types";
import HttpError, { errorStates } from "../errors";
import { GenerateToken } from "../services/jwt";
import { getCache, setCache, CACHE_USER } from "../services/redis";

class UserController {
  static async createUser(req: Request, res: Response, next: NextFunction) {
    try {
      const { email, full_name, password } = req.body;
      const user = (await UserModel.create({ email, full_name, password })) as IUserDocument;
      const access_token = GenerateToken({ id: user._id });
      return res.status(201).json({
        success: true,
        data: {
          access_token,
          user: { _id: user._id, full_name: user.full_name, email: user.email },
        },
      });
    } catch (error) {
      next(error);
    }
  }

  static async loginUser(req: Request, res: Response, next: NextFunction) {
    try {
      const { email, password } = req.body;
      const user = (await UserModel.findOne({ email })) as IUserDocument;
      if (!user) throw new HttpError(errorStates.invalidEmailOrPassword);
      const valid = await comparePassword(password, user.password);
      if (!valid) throw new HttpError(errorStates.invalidEmailOrPassword);
      const access_token = GenerateToken({ id: user._id });
      const { password: _p, ...safeUser } = user.toObject();
      return res.status(200).json({ success: true, data: { user: safeUser, access_token } });
    } catch (error) {
      next(error);
    }
  }

  static async getUser(req: IAuthRequest, res: Response, next: NextFunction) {
    try {
      const userId = req?.decoded?.id;
      if (!userId) throw new HttpError(errorStates.failedAuthentication);

      const idStr = String(userId);
      const cacheKey = CACHE_USER(idStr);
      const cached = await getCache(cacheKey);
      if (cached) {
        const user = JSON.parse(cached);
        return res.status(200).json({ success: true, data: { user } });
      }

      const user = await UserModel.findById(userId);
      if (!user) throw new HttpError(errorStates.failedAuthentication);
      const { password: _p, ...safeUser } = user.toObject();
      await setCache(cacheKey, JSON.stringify(safeUser));

      return res.status(200).json({ success: true, data: { user: safeUser } });
    } catch (error) {
      next(error);
    }
  }
}

export default UserController;

Routes

路由定义

Mount (
src/routes/index.ts
):
ts
import { Router } from "express";
import userRoute from "./user.route";

const router = Router();
router.use("/user", userRoute);
export default router;
User routes (
src/routes/user.route.ts
):
ts
import UserController from "../controllers/user.controller";
import { Router } from "express";
import { RateLimit } from "../services/rate-limit";
import Authentication from "../middlewares/auth";

const router = Router();

router.post("/register", RateLimit({ max: 10, ms: 60000 }), UserController.createUser);
router.post("/login", RateLimit({ max: 10, ms: 60000 }), UserController.loginUser);
router.get("/", RateLimit({ max: 3, ms: 1000 }), Authentication, UserController.getUser);

export default router;
路由挂载(
src/routes/index.ts
):
ts
import { Router } from "express";
import userRoute from "./user.route";

const router = Router();
router.use("/user", userRoute);
export default router;
用户路由(
src/routes/user.route.ts
):
ts
import UserController from "../controllers/user.controller";
import { Router } from "express";
import { RateLimit } from "../services/rate-limit";
import Authentication from "../middlewares/auth";

const router = Router();

router.post("/register", RateLimit({ max: 10, ms: 60000 }), UserController.createUser);
router.post("/login", RateLimit({ max: 10, ms: 60000 }), UserController.loginUser);
router.get("/", RateLimit({ max: 3, ms: 1000 }), Authentication, UserController.getUser);

export default router;

Errors (
src/errors/index.ts
)

错误定义(
src/errors/index.ts

ts
import { IErrorMessage } from "../types";

export const errorStates = {
  internalservererror: { message: "Oops! Something's off-track.", error_id: 0, http_code: 500 },
  highTraffic: { message: "Too many steps at once! Try again soon.", error_id: 1, http_code: 503 },
  rateLimit: { message: "Whoa, slow down! Try again later.", error_id: 2, http_code: 429 },
  failedAuthentication: { message: "Not Authenticated", error_id: 3, http_code: 401 },
  invalidEmailOrPassword: { message: "Invalid email or password", error_id: 4, http_code: 401 },
  tokenExpired: { message: "Session expired—log in again!", error_id: 5, http_code: 401 },
} as const;

class HttpError extends Error {
  statusCode: number;
  error_id: number;
  constructor(args: IErrorMessage) {
    super(args.message);
    this.statusCode = args.http_code;
    this.error_id = args.error_id;
    Object.setPrototypeOf(this, HttpError.prototype);
  }
}

export default HttpError;
ts
import { IErrorMessage } from "../types";

export const errorStates = {
  internalservererror: { message: "Oops! Something's off-track.", error_id: 0, http_code: 500 },
  highTraffic: { message: "Too many steps at once! Try again soon.", error_id: 1, http_code: 503 },
  rateLimit: { message: "Whoa, slow down! Try again later.", error_id: 2, http_code: 429 },
  failedAuthentication: { message: "Not Authenticated", error_id: 3, http_code: 401 },
  invalidEmailOrPassword: { message: "Invalid email or password", error_id: 4, http_code: 401 },
  tokenExpired: { message: "Session expired—log in again!", error_id: 5, http_code: 401 },
} as const;

class HttpError extends Error {
  statusCode: number;
  error_id: number;
  constructor(args: IErrorMessage) {
    super(args.message);
    this.statusCode = args.http_code;
    this.error_id = args.error_id;
    Object.setPrototypeOf(this, HttpError.prototype);
  }
}

export default HttpError;

Middleware – Security (
src/middlewares/security.ts
)

安全中间件(
src/middlewares/security.ts

ts
import { NextFunction, Request, Response } from "express";

const dangerousKeyPattern = /^\$|\.|\$/;
const htmlTagPattern = /<[^>]*>/;

const sanitizeObject = <T>(value: T): T => {
  const inner = (val: unknown): unknown => {
    if (Array.isArray(val)) return val.map(inner);
    if (val && typeof val === "object") {
      const obj = val as Record<string, unknown>;
      const sanitized: Record<string, unknown> = {};
      Object.keys(obj).forEach((key) => {
        if (dangerousKeyPattern.test(key)) return;
        sanitized[key] = inner(obj[key]);
      });
      return sanitized;
    }
    if (typeof val === "string") {
      if (htmlTagPattern.test(val)) throw new Error("HTML content is not allowed in input.");
      return val;
    }
    return val;
  };
  return inner(value) as T;
};

export const securityMiddleware = (req: Request, res: Response, next: NextFunction): void => {
  try {
    req.body = sanitizeObject(req.body);

    const sanitizedQuery = sanitizeObject(req.query);
    const sanitizedParams = sanitizeObject(req.params);

    Object.keys(req.query).forEach((key) => delete (req.query as any)[key]);
    Object.assign(req.query as any, sanitizedQuery as any);

    Object.keys(req.params).forEach((key) => delete (req.params as any)[key]);
    Object.assign(req.params as any, sanitizedParams as any);

    next();
  } catch (err) {
    res.status(400).json({ message: (err as Error).message || "Invalid input." });
  }
};
ts
import { NextFunction, Request, Response } from "express";

const dangerousKeyPattern = /^\$|\.|\$/;
const htmlTagPattern = /<[^>]*>/;

const sanitizeObject = <T>(value: T): T => {
  const inner = (val: unknown): unknown => {
    if (Array.isArray(val)) return val.map(inner);
    if (val && typeof val === "object") {
      const obj = val as Record<string, unknown>;
      const sanitized: Record<string, unknown> = {};
      Object.keys(obj).forEach((key) => {
        if (dangerousKeyPattern.test(key)) return;
        sanitized[key] = inner(obj[key]);
      });
      return sanitized;
    }
    if (typeof val === "string") {
      if (htmlTagPattern.test(val)) throw new Error("HTML content is not allowed in input.");
      return val;
    }
    return val;
  };
  return inner(value) as T;
};

export const securityMiddleware = (req: Request, res: Response, next: NextFunction): void => {
  try {
    req.body = sanitizeObject(req.body);

    const sanitizedQuery = sanitizeObject(req.query);
    const sanitizedParams = sanitizeObject(req.params);

    Object.keys(req.query).forEach((key) => delete (req.query as any)[key]);
    Object.assign(req.query as any, sanitizedQuery as any);

    Object.keys(req.params).forEach((key) => delete (req.params as any)[key]);
    Object.assign(req.params as any, sanitizedParams as any);

    next();
  } catch (err) {
    res.status(400).json({ message: (err as Error).message || "Invalid input." });
  }
};

Middleware – Error handling (
src/middlewares/error-handling.ts
)

错误处理中间件(
src/middlewares/error-handling.ts

ts
import { Request, Response, NextFunction } from "express";
import * as Sentry from "@sentry/node";
import HttpError, { errorStates } from "../errors";

export const ErrorHandling = (error: unknown, req: Request, res: Response, next: NextFunction): void => {
  if (error instanceof HttpError) {
    const statusCode = error.statusCode ?? errorStates.internalservererror.http_code;
    if ((error as any).sentry) Sentry.captureException(error);
    res.status(statusCode).json({
      success: false,
      error: { error_id: error.error_id, message: error.message, errors: [] },
    });
    return;
  }

  const validationErrors: Array<Record<string, string>> = [];
  if ((error as any)?.errors) {
    Object.entries((error as any).errors).forEach(([key, value]: [string, any]) => {
      validationErrors.push({ [key]: value.message });
    });
  }

  Sentry.captureException(error);

  const fallback = errorStates.internalservererror;
  res.status(fallback.http_code).json({
    success: false,
    error: { error_id: fallback.error_id, message: fallback.message, errors: validationErrors },
  });
};
ts
import { Request, Response, NextFunction } from "express";
import * as Sentry from "@sentry/node";
import HttpError, { errorStates } from "../errors";

export const ErrorHandling = (error: unknown, req: Request, res: Response, next: NextFunction): void => {
  if (error instanceof HttpError) {
    const statusCode = error.statusCode ?? errorStates.internalservererror.http_code;
    if ((error as any).sentry) Sentry.captureException(error);
    res.status(statusCode).json({
      success: false,
      error: { error_id: error.error_id, message: error.message, errors: [] },
    });
    return;
  }

  const validationErrors: Array<Record<string, string>> = [];
  if ((error as any)?.errors) {
    Object.entries((error as any).errors).forEach(([key, value]: [string, any]) => {
      validationErrors.push({ [key]: value.message });
    });
  }

  Sentry.captureException(error);

  const fallback = errorStates.internalservererror;
  res.status(fallback.http_code).json({
    success: false,
    error: { error_id: fallback.error_id, message: fallback.message, errors: validationErrors },
  });
};

Middleware – Auth (
src/middlewares/auth.ts
)

鉴权中间件(
src/middlewares/auth.ts

ts
import HttpError, { errorStates } from "../errors";
import { VerifyToken } from "../services/jwt";
import { Response, NextFunction } from "express";
import { IAuthRequest } from "../types";

export default function Authentication(req: IAuthRequest, res: Response, next: NextFunction) {
  try {
    const token = req?.headers?.authorization;
    if (!token) throw new HttpError(errorStates.failedAuthentication);
    const decoded = VerifyToken(token.split("Bearer ")[1]);
    req.decoded = decoded;
    next();
  } catch (error: unknown) {
    if ((error as { name?: string })?.name === "TokenExpiredError") {
      return next(new HttpError(errorStates.tokenExpired));
    }
    next(error);
  }
}
ts
import HttpError, { errorStates } from "../errors";
import { VerifyToken } from "../services/jwt";
import { Response, NextFunction } from "express";
import { IAuthRequest } from "../types";

export default function Authentication(req: IAuthRequest, res: Response, next: NextFunction) {
  try {
    const token = req?.headers?.authorization;
    if (!token) throw new HttpError(errorStates.failedAuthentication);
    const decoded = VerifyToken(token.split("Bearer ")[1]);
    req.decoded = decoded;
    next();
  } catch (error: unknown) {
    if ((error as { name?: string })?.name === "TokenExpiredError") {
      return next(new HttpError(errorStates.tokenExpired));
    }
    next(error);
  }
}

Services

服务工具

Rate limit (
src/services/rate-limit.ts
):
ts
import rateLimit from "express-rate-limit";
import { errorStates } from "../errors";
import { CreateLimitType } from "../types";

export const RateLimit = ({ max, ms }: CreateLimitType) =>
  rateLimit({
    windowMs: ms,
    max,
    message: { error_id: errorStates.rateLimit.error_id, message: errorStates.rateLimit.message },
  });
JWT (
src/services/jwt.ts
):
ts
import jwt from "jsonwebtoken";
import env from "../env";
import { TGenerateToken } from "../types";

export const GenerateToken = (payload: TGenerateToken): string =>
  jwt.sign(payload, env.PRIVATE_KEY, { expiresIn: env.TOKEN_EXPIRED });

export const VerifyToken = (token: string): TGenerateToken =>
  jwt.verify(token, env.PRIVATE_KEY) as TGenerateToken;
Redis (
src/services/redis.ts
) — optional:
Redis is used as a cache layer. If
REDIS_HOST
is not set, cache is skipped and the app runs without Redis.
ts
import Redis from "ioredis";
import env from "../env";

export const CACHE_USER = (id: string) => `user:${id}`;
const USER_CACHE_TTL_SEC = 60;

class RedisClient {
  private static instance: Redis | null = null;

  public static getInstance(): Redis | null {
    if (this.instance !== null) return this.instance;
    if (!env.REDIS_HOST || env.REDIS_HOST === "") return null;
    try {
      this.instance = new Redis({
        host: env.REDIS_HOST,
        port: Number(env.REDIS_PORT) || 6379,
        password: env.REDIS_PASSWORD || undefined,
      });
      this.instance.on("error", (err) => console.error("Redis error:", err));
      return this.instance;
    } catch {
      return null;
    }
  }
}

export const setCache = async (key: string, value: string, expirySeconds = 60): Promise<void> => {
  const redis = RedisClient.getInstance();
  if (!redis) return;
  try {
    await redis.set(key, value, "EX", expirySeconds);
  } catch (err) {
    console.error("Redis setCache error:", err);
  }
};

export const getCache = async (key: string): Promise<string | null> => {
  const redis = RedisClient.getInstance();
  if (!redis) return null;
  try {
    return await redis.get(key);
  } catch (err) {
    console.error("Redis getCache error:", err);
    return null;
  }
};

export const deleteCache = async (key: string): Promise<void> => {
  const redis = RedisClient.getInstance();
  if (!redis) return;
  try {
    await redis.del(key);
  } catch (err) {
    console.error("Redis deleteCache error:", err);
  }
};
Sample: cache-aside in
getUser
  • Try
    getCache(CACHE_USER(userId))
    . If hit, return cached JSON.
  • Else load user from DB, then
    setCache(cacheKey, JSON.stringify(safeUser), 60)
    and return.
  • When Redis is not configured,
    getCache
    /
    setCache
    no-op; the app still works without cache.
限流工具(
src/services/rate-limit.ts
):
ts
import rateLimit from "express-rate-limit";
import { errorStates } from "../errors";
import { CreateLimitType } from "../types";

export const RateLimit = ({ max, ms }: CreateLimitType) =>
  rateLimit({
    windowMs: ms,
    max,
    message: { error_id: errorStates.rateLimit.error_id, message: errorStates.rateLimit.message },
  });
JWT工具(
src/services/jwt.ts
):
ts
import jwt from "jsonwebtoken";
import env from "../env";
import { TGenerateToken } from "../types";

export const GenerateToken = (payload: TGenerateToken): string =>
  jwt.sign(payload, env.PRIVATE_KEY, { expiresIn: env.TOKEN_EXPIRED });

export const VerifyToken = (token: string): TGenerateToken =>
  jwt.verify(token, env.PRIVATE_KEY) as TGenerateToken;
Redis工具(
src/services/redis.ts
) —— 可选:
Redis作为缓存层使用。如果未配置
REDIS_HOST
,缓存逻辑会被跳过,应用无需Redis也可正常运行。
ts
import Redis from "ioredis";
import env from "../env";

export const CACHE_USER = (id: string) => `user:${id}`;
const USER_CACHE_TTL_SEC = 60;

class RedisClient {
  private static instance: Redis | null = null;

  public static getInstance(): Redis | null {
    if (this.instance !== null) return this.instance;
    if (!env.REDIS_HOST || env.REDIS_HOST === "") return null;
    try {
      this.instance = new Redis({
        host: env.REDIS_HOST,
        port: Number(env.REDIS_PORT) || 6379,
        password: env.REDIS_PASSWORD || undefined,
      });
      this.instance.on("error", (err) => console.error("Redis error:", err));
      return this.instance;
    } catch {
      return null;
    }
  }
}

export const setCache = async (key: string, value: string, expirySeconds = 60): Promise<void> => {
  const redis = RedisClient.getInstance();
  if (!redis) return;
  try {
    await redis.set(key, value, "EX", expirySeconds);
  } catch (err) {
    console.error("Redis setCache error:", err);
  }
};

export const getCache = async (key: string): Promise<string | null> => {
  const redis = RedisClient.getInstance();
  if (!redis) return null;
  try {
    return await redis.get(key);
  } catch (err) {
    console.error("Redis getCache error:", err);
    return null;
  }
};

export const deleteCache = async (key: string): Promise<void> => {
  const redis = RedisClient.getInstance();
  if (!redis) return;
  try {
    await redis.del(key);
  } catch (err) {
    console.error("Redis deleteCache error:", err);
  }
};
示例:
getUser
接口的旁路缓存实现
  • 先调用
    getCache(CACHE_USER(userId))
    ,如果命中缓存直接返回缓存的JSON数据。
  • 未命中则从数据库加载用户数据,调用
    setCache(cacheKey, JSON.stringify(safeUser), 60)
    写入缓存后再返回。
  • 未配置Redis时,
    getCache
    /
    setCache
    方法为空实现,应用无需缓存仍可正常运行。

Lib – utils (
src/lib/utils.ts
)

工具函数(
src/lib/utils.ts

ts
import bcrypt from "bcrypt";

export const emailRegex = /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/;
const SALT_ROUNDS = 10;

export const hashPassword = async (password: string): Promise<string> =>
  bcrypt.hash(password, SALT_ROUNDS);

export const comparePassword = async (password: string, hashed: string): Promise<boolean> =>
  bcrypt.compare(password, hashed);
ts
import bcrypt from "bcrypt";

export const emailRegex = /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/;
const SALT_ROUNDS = 10;

export const hashPassword = async (password: string): Promise<string> =>
  bcrypt.hash(password, SALT_ROUNDS);

export const comparePassword = async (password: string, hashed: string): Promise<boolean> =>
  bcrypt.compare(password, hashed);

App (
src/app.ts
)

应用初始化(
src/app.ts

ts
import express, { Express, NextFunction, Request, Response } from "express";
import cors from "cors";
import http from "http";
import toobusy from "toobusy-js";
import helmet from "helmet";
import { securityMiddleware } from "./middlewares/security";
import indexRoute from "./routes";
import { ErrorHandling } from "./middlewares/error-handling";

toobusy.maxLag(120);

const app: Express = express();
const server = http.createServer(app);

app.use(helmet());
app.use(cors({
  origin: ["http://localhost:3005", "http://localhost:3000"],
  methods: ["GET", "POST", "PUT", "DELETE"],
  allowedHeaders: ["Content-Type", "Authorization"],
}));
app.use(express.json({ limit: "100kb" }));
app.use(express.urlencoded({ extended: false }));
app.use(securityMiddleware);

app.use((req, res, next) => {
  if (toobusy()) return res.status(529).json({ message: "High Traffic" });
  else next();
});

app.use((req, res, next) => {
  res.setHeader("Cache-Control", "no-store");
  next();
});

app.use("/api", indexRoute);

app.get("/", (req: Request, res: Response, next: NextFunction) => {
  res.send("Our Backend Running Correctly");
});

app.use(ErrorHandling);

export { app, server };

ts
import express, { Express, NextFunction, Request, Response } from "express";
import cors from "cors";
import http from "http";
import toobusy from "toobusy-js";
import helmet from "helmet";
import { securityMiddleware } from "./middlewares/security";
import indexRoute from "./routes";
import { ErrorHandling } from "./middlewares/error-handling";

toobusy.maxLag(120);

const app: Express = express();
const server = http.createServer(app);

app.use(helmet());
app.use(cors({
  origin: ["http://localhost:3005", "http://localhost:3000"],
  methods: ["GET", "POST", "PUT", "DELETE"],
  allowedHeaders: ["Content-Type", "Authorization"],
}));
app.use(express.json({ limit: "100kb" }));
app.use(express.urlencoded({ extended: false }));
app.use(securityMiddleware);

app.use((req, res, next) => {
  if (toobusy()) return res.status(529).json({ message: "High Traffic" });
  else next();
});

app.use((req, res, next) => {
  res.setHeader("Cache-Control", "no-store");
  next();
});

app.use("/api", indexRoute);

app.get("/", (req: Request, res: Response, next: NextFunction) => {
  res.send("Our Backend Running Correctly");
});

app.use(ErrorHandling);

export { app, server };

Repository and docs

仓库与文档

Validation / done checklist

验证/完成检查清单

When helping the user run or extend this template, confirm:
  • .env
    exists with at least
    PORT
    ,
    MONGODB_URI
    ,
    PRIVATE_KEY
    ,
    TOKEN_EXPIRED
    .
  • MongoDB is reachable (and Redis if used).
  • SENTRY_DSN
    is set if error monitoring is desired.
  • After
    npm run dev
    ,
    GET /
    returns a success message.
  • Auth routes (
    /api/user/register
    ,
    /api/user/login
    ,
    GET /api/user
    ) respond correctly.
  • New resources follow the convention: types -> model -> controller -> route -> mount in index.
帮助用户运行或扩展该模板时,请确认以下事项:
  • .env
    文件存在,且至少包含
    PORT
    MONGODB_URI
    PRIVATE_KEY
    TOKEN_EXPIRED
    变量。
  • MongoDB可正常连接(如果使用Redis则Redis也需要可连接)。
  • 如果需要错误监控功能,已配置
    SENTRY_DSN
  • 执行
    npm run dev
    后,
    GET /
    接口返回成功提示。
  • 鉴权相关接口(
    /api/user/register
    /api/user/login
    GET /api/user
    )可正常响应。
  • 新增资源遵循规范:定义类型 -> 创建模型 -> 实现控制器 -> 配置路由 -> 在路由入口挂载。