关于 nodejs-backend
面向构建可维护服务端应用的 Node.js 后端开发指南,涵盖 API 设计、数据访问、配置、错误处理与测试。
主要功能
- 01分层项目结构
- 02Express 路由处理模式
- 03组合根依赖注入
- 04领域与全局错误处理
- 05仓库与事务模式
- 06配置校验及测试
适用场景
- 01规划 Node.js 服务结构
- 02添加经过校验的 API 路由
- 03整理数据库访问与事务
- 04设计单元及集成测试
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ alinaqi/ maggy --skill nodejs-backend安装后在 Claude Code 中输入/nodejs-backend确认已加载。
详细介绍
Node.js 后端技能
项目结构
project/
├── src/
│ ├── core/ # Pure business logic
│ │ ├── types.ts # Domain types
│ │ ├── errors.ts # Domain errors
│ │ └── services/ # Pure functions
│ │ ├── user.ts
│ │ └── order.ts
│ ├── infra/ # Side effects
│ │ ├── http/ # HTTP layer
│ │ │ ├── server.ts # Server setup
│ │ │ ├── routes/ # Route handlers
│ │ │ └── middleware/ # Express middleware
│ │ ├── db/ # Database
│ │ │ ├── client.ts # DB connection
│ │ │ ├── repositories/ # Data access
│ │ │ └── migrations/ # Schema migrations
│ │ └── external/ # Third-party APIs
│ ├── config/ # Configuration
│ │ └── index.ts # Env vars, validated
│ └── index.ts # Entry point
├── 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: '[email protected]' }),
findByEmail: jest.fn().mockResolvedValue(null),
};
const result = await createUser({ email: '[email protected]', name: 'Test' }, mockRepo);
expect(result.email).toBe('[email protected]');
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: '[email protected]', name: 'New User' });
expect(response.status).toBe(201);
expect(response.body.email).toBe('[email protected]');
});
});
Node.js 反模式
- ❌ 回调地狱 - 使用 async/await
- ❌ 未处理的 Promise 拒绝 - 始终捕获,或交由错误处理器捕获
- ❌ 阻塞事件循环 - 将繁重计算移交处理
- ❌ 在代码中存放机密信息 - 使用环境变量
- ❌ 拼接 SQL 字符串 - 使用参数化查询
- ❌ 不验证输入 - 在 API 边界进行验证
- ❌ 在生产环境中使用 Console.log - 使用适当的日志记录器
- ❌ 没有优雅关闭 - 处理 SIGTERM
- ❌ 单体路由文件 - 按资源拆分
评论
支持 Markdown