算法艺术创作
algorithmic-art
通过代码和可调参数创作生成艺术与交互式视觉效果。
nodejs-express-skill【设计与创意】Use this skill when building Express.js apps — app setup, middleware chain, routing, error handling, validation. This skill enforces: proper middleware ordering, global error handler, Zod/Joi validation, module-based routing, environment config. Do NOT use for: database schema design, frontend, DevOps, non-Express Node backends (Fastify/Hono).
独立 GitHub 快照 · 自动分类 · 以原作者文档为准
Structure Express.js applications with clean middleware pipeline, error handling, route separation, and validation.
User request includes: express, express.js, middleware, router, next.js error handling, express app, express setup, express routing, express validation.
Project structure, middleware order, route layout, error handler, validation setup.
Produce 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
| Criterion | Express | Fastify | Hono |
|---|---|---|---|
| Performance | ~30k req/s | ~50k req/s | ~60k req/s |
| Plugin ecosystem | Largest | Growing | Small |
| TypeScript | Manual annotations | Schema-first (TypeBox/Zod) | Full (TypeBox) |
| Serialization | JSON.stringify | Fast JSON serialization | Built-in |
| Middleware model | Callback chain | Plugin registration | Express-like |
| Validation | Manual middleware | Schema-compiled serializer | Middleware-based |
Decision: Largest ecosystem / wide support → Express. Performance + schema-first → Fastify. Edge/Cloudflare Workers → Hono.
| Layer | Middleware | Position |
|---|---|---|
| Security | Helmet, CORS, CSP | 1st (before any body) |
| Parsing | JSON, URL-encoded | 2nd |
| Observability | Logger, request ID | 3rd |
| Protection | Rate limiter | 4th |
| Auth | JWT/Session check | 5th |
| Routes | Domain routers | 6th |
| Fallback | 404 handler | 7th |
| Error | Global error handler | Last |
express-app/
src/
app.ts # Express app factory
server.ts # HTTP server entry
config/
env.ts # Typed env vars
database.ts
redis.ts
modules/
users/
user.controller.ts
user.service.ts
user.repository.ts
user.validation.ts
user.routes.ts
user.test.ts
orders/
...
common/
middleware/
auth.ts
error-handler.ts
request-logger.ts
rate-limiter.ts
validate.ts
errors/
app-error.ts
not-found.ts
types/
express.d.ts
index.ts
shared/
logger.ts
pagination.ts
package.json
tsconfig.json
.env.example
// app.ts
import express from 'express';
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 { routes } from './modules/routes';
export function createApp() {
const app = express();
// Security
app.use(cors({ origin: env.CORS_ORIGIN }));
app.use(helmet());
app.use(compression());
// Parsing
app.use(express.json({ limit: '1mb' }));
app.use(express.urlencoded({ extended: true }));
// Observability
app.use(requestLogger);
// Rate limiting
app.use(rateLimiter);
// Routes
app.use('/api/v1', routes);
// Error handling
app.use(notFoundHandler);
app.use(errorHandler);
return app;
}
// server.ts
import { createApp } from './app';
import { logger } from './shared/logger';
async function main() {
const app = createApp();
const server = app.listen(env.PORT, () => {
logger.info(`Server listening on port ${env.PORT}`);
});
const shutdown = (signal: string) => {
logger.info(`${signal} received. Shutting down...`);
server.close(() => {
logger.info('Server closed');
process.exit(0);
});
};
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));
}
main().catch((err) => {
console.error('Fatal startup error:', err);
process.exit(1);
});
// config/env.ts
import { z } from 'zod';
import dotenv from 'dotenv';
dotenv.config({ path: `.env.${process.env.NODE_ENV || 'development'}` });
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(),
CORS_ORIGIN: z.string().default('*'),
JWT_SECRET: z.string().min(32),
LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
});
export const env = envSchema.parse(process.env);
// modules/users/user.controller.ts
import { Request, Response, NextFunction } from 'express';
import { userService } from './user.service';
import { asyncHandler } from '../../common/middleware/async-handler';
export const userController = {
list: asyncHandler(async (req: Request, res: Response) => {
const users = await userService.findAll(req.query);
res.json({ success: true, data: users });
}),
getById: asyncHandler(async (req: Request, res: Response) => {
const user = await userService.findById(req.params.id);
res.json({ success: true, data: user });
}),
create: asyncHandler(async (req: Request, res: Response) => {
const user = await userService.create(req.body);
res.status(201).json({ success: true, data: user });
}),
update: asyncHandler(async (req: Request, res: Response) => {
const user = await userService.update(req.params.id, req.body);
res.json({ success: true, data: user });
}),
remove: asyncHandler(async (req: Request, res: Response) => {
await userService.remove(req.params.id);
res.status(204).send();
}),
};
// modules/users/user.routes.ts
import { Router } from 'express';
import { userController } from './user.controller';
import { validate } from '../../common/middleware/validate';
import { createUserSchema, updateUserSchema } from './user.validation';
import { authenticate } from '../../common/middleware/auth';
const router = Router();
router.use(authenticate);
router.get('/', userController.list);
router.get('/:id', userController.getById);
router.post('/', validate(createUserSchema), userController.create);
router.put('/:id', validate(updateUserSchema), userController.update);
router.delete('/:id', userController.remove);
export default router;
// common/middleware/validate.ts
import { Request, Response, NextFunction } from 'express';
import { ZodSchema, ZodError } from 'zod';
import { AppError } from '../errors/app-error';
export function validate(schema: ZodSchema, source: 'body' | 'query' | 'params' = 'body') {
return (req: Request, _res: Response, next: NextFunction) => {
try {
req[source] = schema.parse(req[source]);
next();
} catch (err) {
if (err instanceof ZodError) {
next(new AppError(400, 'VALIDATION_ERROR', 'Invalid request data', err.errors));
} else {
next(err);
}
}
};
}
// common/middleware/error-handler.ts
import { Request, Response, NextFunction } from 'express';
import { AppError } from '../errors/app-error';
import { logger } from '../../shared/logger';
export function errorHandler(err: Error, _req: Request, res: Response, _next: NextFunction) {
if (err instanceof AppError) {
return res.status(err.statusCode).json({
success: false,
error: {
code: err.code,
message: err.message,
details: err.details,
},
});
}
logger.error('Unhandled error', { error: err.message, stack: err.stack });
return res.status(500).json({
success: false,
error: {
code: 'INTERNAL_ERROR',
message: 'An unexpected error occurred',
},
});
}
// common/middleware/async-handler.ts
import { Request, Response, NextFunction } from 'express';
export function asyncHandler(fn: (req: Request, res: Response, next: NextFunction) => Promise<void>) {
return (req: Request, res: Response, next: NextFunction) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
}
// common/errors/app-error.ts
export class AppError extends Error {
constructor(
public readonly statusCode: number,
public readonly code: string,
message: string,
public readonly details?: unknown
) {
super(message);
this.name = 'AppError';
}
}
// common/middleware/auth.ts
import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
import { env } from '../../config/env';
import { AppError } from '../errors/app-error';
export interface AuthPayload {
userId: string;
role: string;
}
declare global {
namespace Express {
interface Request {
user?: AuthPayload;
}
}
}
export function authenticate(req: Request, _res: Response, next: NextFunction) {
const header = req.headers.authorization;
if (!header?.startsWith('Bearer ')) {
return next(new AppError(401, 'UNAUTHORIZED', 'Missing or invalid token'));
}
try {
const token = header.slice(7);
req.user = jwt.verify(token, env.JWT_SECRET) as AuthPayload;
next();
} catch {
next(new AppError(401, 'UNAUTHORIZED', 'Token expired or invalid'));
}
}
export function authorize(...roles: string[]) {
return (req: Request, _res: Response, next: NextFunction) => {
if (!req.user || !roles.includes(req.user.role)) {
return next(new AppError(403, 'FORBIDDEN', 'Insufficient permissions'));
}
next();
};
}
// shared/container.ts
import { createContainer, asClass, asValue, Lifetime } from 'awilix';
import { PrismaClient } from '@prisma/client';
const container = createContainer();
container.register({
prisma: asValue(new PrismaClient()),
userService: asClass(UserService).scoped(Lifetime.SCOPED),
userController: asClass(UserController).scoped(Lifetime.SCOPED),
});
export { container };
// app.ts — DI middleware
app.use((req, res, next) => {
req.container = container.createScope();
next();
});
// modules/health/health.controller.ts
export const healthController = {
check: asyncHandler(async (req: Request, res: Response) => {
const checks = {
database: await checkDatabase(),
redis: await checkRedis(),
uptime: process.uptime(),
};
const healthy = Object.values(checks).every(c => c.status === 'ok');
res.status(healthy ? 200 : 503).json({
status: healthy ? 'healthy' : 'degraded',
checks,
});
}),
};
trust proxy behind nginx/ELB: app.set('trust proxy', 1)compression() middlewareexpress.json({ limit: '1mb' })pm2 start app.js -i maxnode --heapsnapshot-signal=SIGUSR2app.use(helmet({
contentSecurityPolicy: { directives: { defaultSrc: ["'self'"] } },
hsts: { maxAge: 31536000, includeSubDomains: true },
referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
}));
| Anti-Pattern | Why | Fix |
|---|---|---|
app.listen in app module | Can't test app without starting server | App factory pattern |
Naked try/catch in controllers | Duplicated error handling | asyncHandler wrapper |
Direct req.body without validation | Security vulnerability | Zod schema in middleware |
Multiple res.json calls in one handler | ERR_HTTP_HEADERS_SENT | Single code path |
Sync crypto.randomBytes in request | Blocks event loop | Use crypto.randomBytes callback or async |
* with credentialscsurf middleware for cookie/session auth.strip() (default) — remove unknown propertiesimport request from 'supertest';
import { createApp } from '../src/app';
const app = createApp();
describe('POST /api/v1/users', () => {
it('should create user with valid data', async () => {
const res = await request(app)
.post('/api/v1/users')
.send({ name: 'John', email: 'john@test.com' })
.expect(201);
expect(res.body.success).toBe(true);
expect(res.body.data).toHaveProperty('id');
});
it('should return 400 for invalid email', async () => {
const res = await request(app)
.post('/api/v1/users')
.send({ name: 'John', email: 'invalid' })
.expect(400);
expect(res.body.error.code).toBe('VALIDATION_ERROR');
});
});
Use testcontainers for DB integration. Mock HTTP with nock. Use jest with --detectOpenHandles for unclosed connections.
Hand off to backend/nodejs/prisma/SKILL.md for database integration or backend/nodejs/patterns/SKILL.md for advanced Express patterns.