算法艺术创作
algorithmic-art
通过代码和可调参数创作生成艺术与交互式视觉效果。
nodejs-architecture【设计与创意】Use this skill when designing Node.js backend architecture — Express, Fastify, Hono. Project structure, middleware pipeline, routing, error handling, validation. This skill enforces: separation of concerns, correct middleware ordering, global error handling, Zod/Joi validation integration. Do NOT use for: frontend architecture, database schema design…
独立 GitHub 快照 · 自动分类 · 以原作者文档为准
Define and enforce Node.js backend architecture, framework selection, and middleware pipeline conventions.
User request includes: node.js, nodejs, express, fastify, hono, node backend, node project structure, middleware, node.js routing.
A markdown document containing:
Produce the artifact directly. No preamble, no postamble, no explanations. No filler, no hedging, no transitions. Strip articles a/an/the where unambiguous. Compress output — why use many token when few do trick.
4096 tokens
| Framework | Performance | Ecosystem | TypeScript | When |
|---|---|---|---|---|
| Express | Moderate | Largest | Manual setup | Large ecosystem, legacy, most devs know it |
| Fastify | High | Large | Native | Performance-critical, JSON schema validation |
| Hono | Very High | Growing | Native | Edge, Bun, minimal footprint |
src/
+-- modules/
| +-- orders/
| | +-- order.controller.ts
| | +-- order.service.ts
| | +-- order.repository.ts
| | +-- order.schema.ts # Zod validation
| | +-- order.routes.ts
| | +-- order.test.ts
| | +-- order.mapper.ts
| +-- products/
| | +-- ...
| +-- users/
| +-- ...
+-- common/
| +-- middleware/
| | +-- auth.ts
| | +-- error-handler.ts
| | +-- request-logger.ts
| | +-- rate-limiter.ts
| | +-- request-id.ts
| | +-- validate.ts
| +-- errors/
| | +-- app-error.ts
| | +-- not-found.ts
| +-- types/
| | +-- index.ts
| | +-- express.d.ts
+-- config/
| +-- database.ts
| +-- env.ts
| +-- redis.ts
| +-- logger.ts
+-- app.ts # Express/Fastify app setup
+-- server.ts # Entry point
import cors from 'cors';
import helmet from 'helmet';
import compression from 'compression';
import { requestLogger } from './common/middleware/request-logger';
import { rateLimiter } from './common/middleware/rate-limiter';
import { notFoundHandler } from './common/middleware/not-found';
import { errorHandler } from './common/middleware/error-handler';
import { requestId } from './common/middleware/request-id';
import { routes } from './routes';
const app = express();
app.use(cors({ origin: process.env.CORS_ORIGIN, credentials: true }));
app.use(helmet());
app.use(compression());
app.use(express.json({ limit: '1mb' }));
app.use(express.urlencoded({ extended: true }));
app.use(requestId);
app.use(requestLogger);
app.use(rateLimiter);
app.use('/api/v1', routes);
app.use(notFoundHandler);
app.use(errorHandler);
For Fastify:
import Fastify from 'fastify';
import cors from '@fastify/cors';
import helmet from '@fastify/helmet';
import compress from '@fastify/compress';
import rateLimit from '@fastify/rate-limit';
const app = Fastify({ logger: true });
await app.register(cors, { origin: process.env.CORS_ORIGIN });
await app.register(helmet);
await app.register(compress);
await app.register(rateLimit, { max: 100, timeWindow: '1 minute' });
await app.register(routes, { prefix: '/api/v1' });
app.setNotFoundHandler((req, reply) => {
reply.status(404).send({ success: false, error: { code: 'NOT_FOUND', message: 'Route not found' } });
});
app.setErrorHandler((err, req, reply) => {
const status = err.statusCode || 500;
reply.status(status).send({ success: false, error: { code: err.code || 'INTERNAL', message: err.message } });
});
// common/errors/app-error.ts
export interface ErrorDetails {
code: string;
message: string;
details?: unknown;
stack?: string;
}
export class AppError extends Error {
public readonly statusCode: number;
public readonly code: string;
public readonly details: unknown;
public readonly isOperational: boolean;
constructor(statusCode: number, code: string, message: string, details?: unknown) {
super(message);
this.statusCode = statusCode;
this.code = code;
this.details = details;
this.isOperational = true;
Object.setPrototypeOf(this, new.target.prototype);
Error.captureStackTrace(this, this.constructor);
}
public toJSON(): ErrorDetails {
return {
code: this.code,
message: this.message,
details: this.details,
stack: process.env.NODE_ENV === 'development' ? this.stack : undefined,
};
}
}
export class NotFoundError extends AppError {
constructor(entity: string, id: string) {
super(404, 'NOT_FOUND', `${entity} with id ${id} not found`);
}
}
export class ValidationError extends AppError {
constructor(errors: unknown) {
super(400, 'VALIDATION', 'Validation failed', errors);
}
}
export class UnauthorizedError extends AppError {
constructor(message = 'Unauthorized') {
super(401, 'UNAUTHORIZED', message);
}
}
export class ForbiddenError extends AppError {
constructor(message = 'Forbidden') {
super(403, 'FORBIDDEN', message);
}
}
// common/middleware/error-handler.ts
import type { Request, Response, NextFunction } from 'express';
import { AppError } from '../errors/app-error';
import { logger } from '../../config/logger';
export function errorHandler(err: Error, req: Request, res: Response, _next: NextFunction): void {
if (err instanceof AppError) {
logger.warn({ err, requestId: req.id, path: req.path }, 'Operational error');
res.status(err.statusCode).json({
success: false,
error: err.toJSON(),
});
return;
}
logger.error({ err, requestId: req.id, path: req.path }, 'Unhandled error');
res.status(500).json({
success: false,
error: {
code: 'INTERNAL_ERROR',
message: process.env.NODE_ENV === 'production' ? 'An unexpected error occurred' : err.message,
},
});
}
// modules/orders/order.schema.ts
import { z } from 'zod';
export const createOrderSchema = z.object({
customerId: z.string().uuid(),
items: z.array(z.object({
productId: z.string().uuid(),
quantity: z.number().int().positive(),
unitPrice: z.number().positive(),
})).min(1, 'At least one item required'),
shippingAddress: z.object({
street: z.string().min(1),
city: z.string().min(1),
zipCode: z.string().regex(/^\d{5}(-\d{4})?$/),
country: z.string().length(2),
}),
couponCode: z.string().optional(),
});
export type CreateOrderInput = z.infer<typeof createOrderSchema>;
// common/middleware/validate.ts
import type { Request, Response, NextFunction } from 'express';
import type { ZodSchema } from 'zod';
import { ValidationError } from '../errors/app-error';
export function validate(schema: ZodSchema, source: 'body' | 'query' | 'params' = 'body') {
return (req: Request, _res: Response, next: NextFunction): void => {
const result = schema.safeParse(req[source]);
if (result.success) {
req[source] = result.data;
next();
} else {
next(new ValidationError(result.error.issues));
}
};
}
// Usage in route
router.post('/', validate(createOrderSchema), orderController.create);
// modules/orders/order.routes.ts
import { Router } from 'express';
import { OrderController } from './order.controller';
import { validate } from '../../common/middleware/validate';
import { createOrderSchema, updateOrderSchema } from './order.schema';
import { authenticate } from '../../common/middleware/auth';
const router = Router();
const controller = new OrderController();
router.use(authenticate);
router.get('/', controller.list);
router.get('/:id', controller.getById);
router.post('/', validate(createOrderSchema), controller.create);
router.put('/:id', validate(updateOrderSchema), controller.update);
router.delete('/:id', controller.delete);
export { router as orderRoutes };
// modules/orders/order.controller.ts
import type { Request, Response, NextFunction } from 'express';
import { OrderService } from './order.service';
export class OrderController {
constructor(private readonly orderService = new OrderService()) {}
list = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
try {
const result = await this.orderService.findAll(req.query);
res.json({ success: true, data: result });
} catch (err) {
next(err);
}
};
getById = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
try {
const result = await this.orderService.findById(req.params.id);
res.json({ success: true, data: result });
} catch (err) {
next(err);
}
};
create = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
try {
const result = await this.orderService.create(req.body);
res.status(201).json({ success: true, data: result });
} catch (err) {
next(err);
}
};
update = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
try {
const result = await this.orderService.update(req.params.id, req.body);
res.json({ success: true, data: result });
} catch (err) {
next(err);
}
};
delete = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
try {
await this.orderService.delete(req.params.id);
res.status(204).send();
} catch (err) {
next(err);
}
};
}
// config/env.ts
import { z } from 'zod';
const envSchema = z.object({
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
PORT: z.coerce.number().default(3000),
DATABASE_URL: z.string().url(),
REDIS_URL: z.string().url().optional(),
JWT_SECRET: z.string().min(32),
JWT_EXPIRES_IN: z.string().default('15m'),
CORS_ORIGIN: z.string().default('http://localhost:3000'),
LOG_LEVEL: z.enum(['fatal', 'error', 'warn', 'info', 'debug', 'trace']).default('info'),
});
export type Env = z.infer<typeof envSchema>;
let env: Env;
export function loadEnv(): Env {
const result = envSchema.safeParse(process.env);
if (!result.success) {
console.error('Invalid environment variables:', result.error.issues);
process.exit(1);
}
env = result.data;
return env;
}
export function getEnv(): Env {
if (!env) return loadEnv();
return env;
}
// config/container.ts
import { OrderService } from '../modules/orders/order.service';
import { OrderRepository } from '../modules/orders/order.repository';
import { PaymentService } from '../modules/payments/payment.service';
import { NotificationService } from '../modules/notifications/notification.service';
import { Database } from './database';
export class Container {
private static instance: Container;
private readonly db: Database;
private readonly services = new Map<string, unknown>();
private constructor() {
this.db = new Database();
}
static getInstance(): Container {
if (!Container.instance) {
Container.instance = new Container();
}
return Container.instance;
}
getOrderRepository(): OrderRepository {
if (!this.services.has('orderRepository')) {
this.services.set('orderRepository', new OrderRepository(this.db));
}
return this.services.get('orderRepository') as OrderRepository;
}
getOrderService(): OrderService {
if (!this.services.has('orderService')) {
this.services.set('orderService', new OrderService(
this.getOrderRepository(),
this.getPaymentService(),
this.getNotificationService(),
));
}
return this.services.get('orderService') as OrderService;
}
getPaymentService(): PaymentService {
if (!this.services.has('paymentService')) {
this.services.set('paymentService', new PaymentService());
}
return this.services.get('paymentService') as PaymentService;
}
getNotificationService(): NotificationService {
if (!this.services.has('notificationService')) {
this.services.set('notificationService', new NotificationService());
}
return this.services.get('notificationService') as NotificationService;
}
}
// modules/orders/order.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import request from 'supertest';
import { createApp } from '../app';
import { OrderService } from './order.service';
vi.mock('./order.service');
describe('Orders API', () => {
let app: Express.Application;
beforeEach(() => {
vi.clearAllMocks();
app = createApp('test');
});
describe('POST /api/v1/orders', () => {
it('creates order successfully', async () => {
const mockOrder = { id: '123', customerId: 'cust-1', items: [] };
vi.mocked(OrderService.prototype.create).mockResolvedValue(mockOrder);
const res = await request(app)
.post('/api/v1/orders')
.send({
customerId: '550e8400-e29b-41d4-a716-446655440000',
items: [{ productId: '550e8400-e29b-41d4-a716-446655440001', quantity: 2, unitPrice: 19.99 }],
shippingAddress: { street: '123 Main', city: 'NYC', zipCode: '10001', country: 'US' },
})
.expect(201);
expect(res.body.success).toBe(true);
expect(res.body.data.id).toBe('123');
});
it('returns 400 for invalid input', async () => {
const res = await request(app)
.post('/api/v1/orders')
.send({ customerId: 'invalid' })
.expect(400);
expect(res.body.error.code).toBe('VALIDATION');
});
it('returns 401 without auth token', async () => {
const res = await request(app)
.post('/api/v1/orders')
.send({})
.expect(401);
});
});
describe('GET /api/v1/orders/:id', () => {
it('returns 404 for non-existent order', async () => {
vi.mocked(OrderService.prototype.findById).mockRejectedValue(
new NotFoundError('Order', '999')
);
await request(app)
.get('/api/v1/orders/999')
.expect(404);
});
});
});
Need high performance?
+-- Yes -> Need edge runtime?
| +-- Yes -> Hono (Bun, Deno, Cloudflare Workers)
| +-- No -> Fastify (JSON schema validation, high throughput)
+-- No -> Need largest ecosystem?
+-- Yes -> Express (most middleware, community packages)
+-- No -> Hono (modern, lightweight, good DX)
TypeScript project?
+-- Yes -> Zod (type inference, composable schemas)
+-- No -> Joi (mature, expressive API)
Monorepo with shared types?
+-- Yes -> Use Nx or Turborepo, shared packages for types/schemas
+-- No -> Flat modules/ structure with domain grouping
Single service deployment?
+-- Yes -> Centralized AppError hierarchy with status codes
+-- No -> Add correlation IDs, distributed tracing headers
Middleware ordering wrong: Rate limiter before JSON parser causes body read issues. Security middleware (helmet, cors) must come first, before any body parsing.
Swallowing async errors: Express does not catch async promise rejections automatically. Always use express-async-errors or wrap async handlers. Fastify handles this natively.
Blocking the event loop: CPU-heavy operations in request handlers (JSON.stringify on large objects, crypto operations, image processing). Offload to worker threads or queue tasks.
No request body size limits: Attackers send gigantic payloads to exhaust memory. Always set express.json({ limit: '1mb' }) or equivalent.
Exposing stack traces in production: Never include err.stack in production error responses. Use structured logging instead.
Zod schema duplication: Defining validation both in Zod schemas and TypeScript interfaces. Use z.infer<typeof schema> to derive types.
Global state in request handlers: Mutating module-level variables across requests leads to race conditions. Use DI container or request-scoped services.
Circular dependencies: Common in barrel exports. Keep imports explicit and avoid index.ts re-exports that create cycles.
Missing HTTP header security: Not setting X-Content-Type-Options, X-Frame-Options, Strict-Transport-Security. Helmet handles this.
Database connection pool exhaustion: Creating connections per request instead of using a connection pool. Always use pg.Pool or Prisma's built-in pooling.
Controller methods never call next(err) directly with non-Error values. Always wrap in AppError or equivalent typed error class.
Use repository pattern for data access abstracted behind interfaces. Enables unit testing without a database.
Keep route handlers thin. Route file defines URL structure and middleware. Controller handles request/response. Service contains business logic.
Use async event emitters or message queues for side effects. Sending emails, push notifications, webhooks should not block the response.
Structured logging with correlation IDs. Attach a unique request ID to every log line for debugging distributed traces.
Rate limit by user ID, not just IP. For authenticated endpoints, rate-limit by userId to prevent one user exhausting another's quota.
Graceful shutdown: Handle SIGTERM, stop accepting connections, drain existing requests, close database connections.
Compression before rate limiting. Compress responses first, then apply rate limits to reduce bandwidth usage for limited users.
Health check endpoints: /health (liveness) and /ready (readiness) for Kubernetes probes. DB connection check in readiness.
Use .env files with validation at startup. Fail fast if required variables missing.
| Feature | Express | Fastify | Hono |
|---|---|---|---|
| Request validation | Manual (Zod/Joi) | Built-in (JSON Schema) | Manual (Zod) |
| Performance | 15k req/s | 35k req/s | 50k req/s |
| Plugin ecosystem | 5000+ | 200+ | 50+ |
| TypeScript DX | Manual types | Auto-generated | Native |
| Edge compatibility | No | Limited | Yes |
| OpenAPI generation | Third-party | Built-in plugin | Third-party |
| WebSocket | Via library | Via plugin | Built-in |
| Testing supertest | Yes | inject() method | Built-in test client |
| Middleware model | Sequential | Encapsulated | Sequential |
| Error handling | Manual async wrap | Auto async catch | Auto async catch |
pool.min and pool.max.compression (Express), @fastify/compress (Fastify).node-cache-manager with Redis store.cluster module or PM2 cluster mode to utilize all CPU cores.spdy or a reverse proxy.Connection: close. Reduces TCP handshake overhead.--max-old-space-size flag. Monitor heap usage with process.memoryUsage().| Tool | Purpose |
|---|---|
| Zod | Runtime schema validation with TypeScript inference |
| Vitest | Unit and integration testing (fast, ESM-native) |
| Supertest | HTTP assertion testing |
| Pino | Structured JSON logging (used by Fastify) |
| Winston | Alternative logging with transports |
| tsx / ts-node | TypeScript execution in development |
| PM2 | Production process manager with clustering |
| Docker | Containerization for consistent environments |
| ESLint | Code quality with @typescript-eslint |
| Prettier | Consistent code formatting |
| Husky + lint-staged | Pre-commit hooks |
| Nodemon / tsx watch | Auto-restart on file changes |
| Dotenv | Environment variable loading |
Hand off to backend/nodejs/patterns/SKILL.md for Node.js-specific patterns.