Node.js后端技能 nodejs-backend

Node.js后端技能涵盖了使用Express/Fastify和仓库模式构建高效、可维护的后端服务的关键实践,包括项目结构、API设计、错误处理、数据库交互、配置管理和测试策略。

后端开发 0 次安装 0 次浏览 更新于 3/5/2026

Node.js 后端技能

加载方式:base.md + typescript.md

项目结构

project/
├── src/
│   ├── core/                   # 纯业务逻辑
│   │   ├── types.ts            # 领域类型
│   │   ├── errors.ts           # 领域错误
│   │   └── services/           # 纯函数
│   │       ├── user.ts
│   │       └── order.ts
│   ├── infra/                  # 副作用
│   │   ├── http/               # HTTP层
│   │   │   ├── server.ts       # 服务器设置
│   │   │   ├── routes/         # 路由处理器
│   │   │   └── middleware/     # Express中间件
│   │   ├── db/                 # 数据库
│   │   │   ├── client.ts       # 数据库连接
│   │   │   ├── repositories/   # 数据访问
│   │   │   └── migrations/     # 模式迁移
│   │   └── external/           # 第三方API
│   ├── config/                 # 配置
│   │   └── index.ts            # 环境变量,已验证
│   └── index.ts                # 入口点
├── tests/
│   ├── unit/
│   └── integration/
├── package.json
└── CLAUDE.md

API设计

路由处理器模式

// routes/users.ts
import { Router } from 'express';
import { z } from 'zod';
import { createUser } from '../../core/services/user';
import { UserRepository } from '../db/repositories/user';

const CreateUserSchema = z.object({
  email: z.string().email(),
  name: z.string().min(1).max(100),
});

export function createUserRoutes(userRepo: UserRepository): Router {
  const router = Router();

  router.post('/', async (req, res, next) => {
    try {
      const input = CreateUserSchema.parse(req.body);
      const user = await createUser(input, userRepo);
      res.status(201).json(user);
    } catch (error) {
      next(error);
    }
  });

  return router;
}

组合根处的依赖注入

// index.ts
import { createApp } from './infra/http/server';
import { createDbClient } from './infra/db/client';
import { UserRepository } from './infra/db/repositories/user';
import { createUserRoutes } from './infra/http/routes/users';

async function main(): Promise<void> {
  const db = await createDbClient();
  const userRepo = new UserRepository(db);
  
  const app = createApp({
    userRoutes: createUserRoutes(userRepo),
  });
  
  app.listen(3000);
}

错误处理

领域错误

// core/errors.ts
export class DomainError extends Error {
  constructor(
    message: string,
    public readonly code: string,
    public readonly statusCode: number = 400
  ) {
    super(message);
    this.name = 'DomainError';
  }
}

export class NotFoundError extends DomainError {
  constructor(resource: string, id: string) {
    super(`${resource} with id ${id} not found`, 'NOT_FOUND', 404);
  }
}

export class ValidationError extends DomainError {
  constructor(message: string) {
    super(message, 'VALIDATION_ERROR', 400);
  }
}

全局错误处理器

// middleware/errorHandler.ts
import { ErrorRequestHandler } from 'express';
import { DomainError } from '../../core/errors';
import { ZodError } from 'zod';

export const errorHandler: ErrorRequestHandler = (err, req, res, next) => {
  if (err instanceof DomainError) {
    return res.status(err.statusCode).json({
      error: { code: err.code, message: err.message },
    });
  }

  if (err instanceof ZodError) {
    return res.status(400).json({
      error: { code: 'VALIDATION_ERROR', details: err.errors },
    });
  }

  console.error('Unexpected error:', err);
  return res.status(500).json({
    error: { code: 'INTERNAL_ERROR', message: 'Something went wrong' },
  });
};

数据库模式

仓库模式

// db/repositories/user.ts
import { Kysely } from 'kysely';
import { Database, User } from '../types';

export class UserRepository {
  constructor(private db: Kysely<Database>) {}

  async findById(id: string): Promise<User | null> {
    return this.db
      .selectFrom('users')
      .where('id', '=', id)
      .selectAll()
      .executeTakeFirst() ?? null;
  }

  async create(data: Omit<User, 'id' | 'createdAt'>): Promise<User> {
    return this.db
      .insertInto('users')
      .values(data)
      .returningAll()
      .executeTakeFirstOrThrow();
  }
}

事务

async function transferFunds(
  fromId: string,
  toId: string,
  amount: number,
  db: Kysely<Database>
): Promise<void> {
  await db.transaction().execute(async (trx) => {
    await trx
      .updateTable('accounts')
      .set((eb) => ({ balance: eb('balance', '-', amount) }))
      .where('id', '=', fromId)
      .execute();

    await trx
      .updateTable('accounts')
      .set((eb) => ({ balance: eb('balance', '+', amount) }))
      .where('id', '=', toId)
      .execute();
  });
}

配置

验证配置

// config/index.ts
import { z } from 'zod';

const ConfigSchema = z.object({
  NODE_ENV: z.enum(['development', 'production', 'test']),
  PORT: z.coerce.number().default(3000),
  DATABASE_URL: z.string().url(),
  API_KEY: z.string().min(1),
});

export type Config = z.infer<typeof ConfigSchema>;

export function loadConfig(): Config {
  return ConfigSchema.parse(process.env);
}

测试

单元测试(核心)

// tests/unit/services/user.test.ts
import { createUser } from '../../../src/core/services/user';

describe('createUser', () => {
  it('creates user with valid data', async () => {
    const mockRepo = {
      create: jest.fn().mockResolvedValue({ id: '1', email: 'test@example.com' }),
      findByEmail: jest.fn().mockResolvedValue(null),
    };

    const result = await createUser({ email: 'test@example.com', name: 'Test' }, mockRepo);

    expect(result.email).toBe('test@example.com');
    expect(mockRepo.create).toHaveBeenCalledTimes(1);
  });
});

集成测试(API)

// tests/integration/users.test.ts
import request from 'supertest';
import { createTestApp, createTestDb } from '../helpers';

describe('POST /users', () => {
  let app: Express;
  let db: TestDb;

  beforeAll(async () => {
    db = await createTestDb();
    app = createTestApp(db);
  });

  afterAll(async () => {
    await db.destroy();
  });

  it('creates user and returns 201', async () => {
    const response = await request(app)
      .post('/users')
      .send({ email: 'new@example.com', name: 'New User' });

    expect(response.status).toBe(201);
    expect(response.body.email).toBe('new@example.com');
  });
});

Node.js 反模式

  • ❌ 回调地狱 - 使用 async/await
  • ❌ 未处理的Promise拒绝 - 总是捕获或让错误处理器捕获
  • ❌ 阻塞事件循环 - 卸载重型计算
  • ❌ 代码中的秘密 - 使用环境变量
  • ❌ SQL字符串连接 - 使用参数化查询
  • ❌ 无输入验证 - 在API边界处验证
  • ❌ 生产中的Console.log - 使用适当的日志记录器
  • ❌ 无优雅关闭 - 处理SIGTERM
  • ❌ 单片路由文件 - 按资源拆分