nodejs-backend-typescript

安装量: 386
排名: #2490

安装

npx skills add https://github.com/bobmatnyc/claude-mpm-skills --skill nodejs-backend-typescript

Node.js Backend Development with TypeScript progressive_disclosure: entry_point: summary: "TypeScript backend patterns with Express/Fastify, routing, middleware, database integration" when_to_use: - "When building REST APIs with TypeScript" - "When creating Express/Fastify servers" - "When needing server-side TypeScript" - "When building microservices" quick_start: - "npm init -y && npm install -D typescript @types/node tsx" - "npm install express @types/express zod" - "Create tsconfig.json with strict mode" - "npm run dev" token_estimate: entry: 75 full: 4700 TypeScript Setup Essential Configuration

tsconfig.json (strict mode recommended):

{ "compilerOptions": { "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", "outDir": "./dist", "rootDir": "./src", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, "types": ["node"] }, "include": ["src/*/"], "exclude": ["node_modules", "dist"] }

package.json scripts:

{ "scripts": { "dev": "tsx watch src/server.ts", "build": "tsc", "start": "node dist/server.js", "test": "vitest" } }

Development Dependencies npm install -D typescript @types/node tsx vitest npm install -D @types/express # or @types/node (Fastify has built-in types)

Express Patterns Basic Express Server

src/server.ts:

import express, { Request, Response, NextFunction } from 'express'; import { z } from 'zod';

const app = express(); const port = process.env.PORT || 3000;

// Middleware app.use(express.json()); app.use(express.urlencoded({ extended: true }));

// Type-safe request handlers interface TypedRequest extends Request { body: T; }

// Routes app.get('/health', (req: Request, res: Response) => { res.json({ status: 'ok', timestamp: new Date().toISOString() }); });

// Start server app.listen(port, () => { console.log(Server running on port ${port}); });

Router Pattern

src/routes/users.ts:

import { Router } from 'express'; import { z } from 'zod'; import { validateRequest } from '../middleware/validation';

const router = Router();

const createUserSchema = z.object({ email: z.string().email(), name: z.string().min(2), age: z.number().int().positive().optional(), });

router.post( '/users', validateRequest(createUserSchema), async (req, res, next) => { try { const userData = req.body; // Type-safe after validation // Database insert logic res.status(201).json({ id: 1, ...userData }); } catch (error) { next(error); } } );

export default router;

Middleware Patterns

src/middleware/validation.ts:

import { Request, Response, NextFunction } from 'express'; import { z, ZodSchema } from 'zod';

export const validateRequest = (schema: ZodSchema) => { return (req: Request, res: Response, next: NextFunction) => { try { req.body = schema.parse(req.body); next(); } catch (error) { if (error instanceof z.ZodError) { res.status(400).json({ error: 'Validation failed', details: error.errors, }); } else { next(error); } } }; };

src/middleware/auth.ts:

import { Request, Response, NextFunction } from 'express'; import jwt from 'jsonwebtoken';

interface JwtPayload { userId: string; email: string; }

declare global { namespace Express { interface Request { user?: JwtPayload; } } }

export const authenticate = ( req: Request, res: Response, next: NextFunction ) => { const token = req.headers.authorization?.replace('Bearer ', '');

if (!token) { return res.status(401).json({ error: 'No token provided' }); }

try { const decoded = jwt.verify(token, process.env.JWT_SECRET!) as JwtPayload; req.user = decoded; next(); } catch (error) { res.status(401).json({ error: 'Invalid token' }); } };

Error Handling

src/middleware/errorHandler.ts:

import { Request, Response, NextFunction } from 'express';

export class AppError extends Error { constructor( public statusCode: number, message: string, public isOperational = true ) { super(message); Object.setPrototypeOf(this, AppError.prototype); } }

export const errorHandler = ( err: Error, req: Request, res: Response, next: NextFunction ) => { if (err instanceof AppError) { return res.status(err.statusCode).json({ error: err.message, ...(process.env.NODE_ENV === 'development' && { stack: err.stack }), }); }

console.error('Unexpected error:', err); res.status(500).json({ error: 'Internal server error', ...(process.env.NODE_ENV === 'development' && { message: err.message, stack: err.stack, }), }); };

Fastify Patterns Basic Fastify Server

src/server.ts:

import Fastify from 'fastify'; import { TypeBoxTypeProvider } from '@fastify/type-provider-typebox'; import { Type } from '@sinclair/typebox';

const fastify = Fastify({ logger: { level: process.env.LOG_LEVEL || 'info', }, }).withTypeProvider();

// Type-safe route with schema validation fastify.route({ method: 'POST', url: '/users', schema: { body: Type.Object({ email: Type.String({ format: 'email' }), name: Type.String({ minLength: 2 }), age: Type.Optional(Type.Integer({ minimum: 0 })), }), response: { 201: Type.Object({ id: Type.Number(), email: Type.String(), name: Type.String(), }), }, }, handler: async (request, reply) => { const { email, name, age } = request.body; // Auto-typed and validated return reply.status(201).send({ id: 1, email, name }); }, });

const start = async () => { try { await fastify.listen({ port: 3000, host: '0.0.0.0' }); } catch (err) { fastify.log.error(err); process.exit(1); } };

start();

Plugin Pattern

src/plugins/database.ts:

import { FastifyPluginAsync } from 'fastify'; import fp from 'fastify-plugin'; import { drizzle } from 'drizzle-orm/node-postgres'; import { Pool } from 'pg';

declare module 'fastify' { interface FastifyInstance { db: ReturnType; } }

const databasePlugin: FastifyPluginAsync = async (fastify) => { const pool = new Pool({ connectionString: process.env.DATABASE_URL, });

const db = drizzle(pool); fastify.decorate('db', db);

fastify.addHook('onClose', async () => { await pool.end(); }); };

export default fp(databasePlugin);

Hooks Pattern

src/hooks/auth.ts:

import { FastifyRequest, FastifyReply } from 'fastify'; import jwt from 'jsonwebtoken';

declare module 'fastify' { interface FastifyRequest { user?: { userId: string; email: string; }; } }

export const authHook = async ( request: FastifyRequest, reply: FastifyReply ) => { const token = request.headers.authorization?.replace('Bearer ', '');

if (!token) { return reply.status(401).send({ error: 'No token provided' }); }

try { const decoded = jwt.verify(token, process.env.JWT_SECRET!) as { userId: string; email: string; }; request.user = decoded; } catch (error) { return reply.status(401).send({ error: 'Invalid token' }); } };

Request Validation Zod with Express import { z } from 'zod';

const userSchema = z.object({ email: z.string().email(), password: z.string().min(8), profile: z.object({ firstName: z.string(), lastName: z.string(), age: z.number().int().positive(), }), tags: z.array(z.string()).optional(), });

type CreateUserInput = z.infer;

router.post('/users', async (req, res) => { const result = userSchema.safeParse(req.body);

if (!result.success) { return res.status(400).json({ error: 'Validation failed', details: result.error.format(), }); }

const user: CreateUserInput = result.data; // Type-safe user object });

TypeBox with Fastify import { Type, Static } from '@sinclair/typebox';

const UserSchema = Type.Object({ email: Type.String({ format: 'email' }), password: Type.String({ minLength: 8 }), profile: Type.Object({ firstName: Type.String(), lastName: Type.String(), age: Type.Integer({ minimum: 0 }), }), tags: Type.Optional(Type.Array(Type.String())), });

type User = Static;

fastify.post('/users', { schema: { body: UserSchema }, handler: async (request, reply) => { const user: User = request.body; // Auto-validated return { id: 1, ...user }; }, });

Authentication JWT Authentication

src/services/auth.ts:

import jwt from 'jsonwebtoken'; import bcrypt from 'bcrypt';

interface TokenPayload { userId: string; email: string; }

export class AuthService { private static JWT_SECRET = process.env.JWT_SECRET!; private static JWT_EXPIRES_IN = '7d';

static async hashPassword(password: string): Promise { return bcrypt.hash(password, 10); }

static async comparePassword( password: string, hash: string ): Promise { return bcrypt.compare(password, hash); }

static generateToken(payload: TokenPayload): string { return jwt.sign(payload, this.JWT_SECRET, { expiresIn: this.JWT_EXPIRES_IN, }); }

static verifyToken(token: string): TokenPayload { return jwt.verify(token, this.JWT_SECRET) as TokenPayload; } }

Session-based Auth (Express) import session from 'express-session'; import RedisStore from 'connect-redis'; import { createClient } from 'redis';

const redisClient = createClient({ url: process.env.REDIS_URL, }); redisClient.connect();

app.use( session({ store: new RedisStore({ client: redisClient }), secret: process.env.SESSION_SECRET!, resave: false, saveUninitialized: false, cookie: { secure: process.env.NODE_ENV === 'production', httpOnly: true, maxAge: 1000 * 60 * 60 * 24 * 7, // 7 days }, }) );

declare module 'express-session' { interface SessionData { userId: string; } }

Database Integration Drizzle ORM

src/db/schema.ts:

import { pgTable, serial, varchar, timestamp } from 'drizzle-orm/pg-core';

export const users = pgTable('users', { id: serial('id').primaryKey(), email: varchar('email', { length: 255 }).notNull().unique(), name: varchar('name', { length: 255 }).notNull(), passwordHash: varchar('password_hash', { length: 255 }).notNull(), createdAt: timestamp('created_at').defaultNow().notNull(), });

export type User = typeof users.$inferSelect; export type NewUser = typeof users.$inferInsert;

src/db/client.ts:

import { drizzle } from 'drizzle-orm/node-postgres'; import { Pool } from 'pg'; import * as schema from './schema';

const pool = new Pool({ connectionString: process.env.DATABASE_URL, });

export const db = drizzle(pool, { schema });

src/repositories/userRepository.ts:

import { eq } from 'drizzle-orm'; import { db } from '../db/client'; import { users, NewUser } from '../db/schema';

export class UserRepository { static async create(data: NewUser) { const [user] = await db.insert(users).values(data).returning(); return user; }

static async findByEmail(email: string) { return db.query.users.findFirst({ where: eq(users.email, email), }); }

static async findById(id: number) { return db.query.users.findFirst({ where: eq(users.id, id), }); }

static async list(limit = 10, offset = 0) { return db.query.users.findMany({ limit, offset, columns: { passwordHash: false, // Exclude sensitive fields }, }); } }

Prisma

prisma/schema.prisma:

datasource db { provider = "postgresql" url = env("DATABASE_URL") }

generator client { provider = "prisma-client-js" }

model User { id Int @id @default(autoincrement()) email String @unique name String passwordHash String @map("password_hash") createdAt DateTime @default(now()) @map("created_at") posts Post[]

@@map("users") }

model Post { id Int @id @default(autoincrement()) title String content String? published Boolean @default(false) authorId Int @map("author_id") author User @relation(fields: [authorId], references: [id]) createdAt DateTime @default(now()) @map("created_at")

@@map("posts") }

src/services/userService.ts:

import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

export class UserService { static async createUser(data: { email: string; name: string; password: string }) { const passwordHash = await AuthService.hashPassword(data.password);

return prisma.user.create({
  data: {
    email: data.email,
    name: data.name,
    passwordHash,
  },
  select: {
    id: true,
    email: true,
    name: true,
    createdAt: true,
  },
});

}

static async getUserWithPosts(userId: number) { return prisma.user.findUnique({ where: { id: userId }, include: { posts: { where: { published: true }, orderBy: { createdAt: 'desc' }, }, }, }); } }

API Design REST API Patterns

Pagination:

import { z } from 'zod';

const paginationSchema = z.object({ page: z.coerce.number().int().positive().default(1), limit: z.coerce.number().int().positive().max(100).default(20), });

router.get('/users', async (req, res) => { const { page, limit } = paginationSchema.parse(req.query); const offset = (page - 1) * limit;

const [users, total] = await Promise.all([ UserRepository.list(limit, offset), UserRepository.count(), ]);

res.json({ data: users, pagination: { page, limit, total, totalPages: Math.ceil(total / limit), }, }); });

Filtering and Sorting:

const filterSchema = z.object({ status: z.enum(['active', 'inactive']).optional(), search: z.string().optional(), sortBy: z.enum(['createdAt', 'name', 'email']).default('createdAt'), sortOrder: z.enum(['asc', 'desc']).default('desc'), });

router.get('/users', async (req, res) => { const filters = filterSchema.parse(req.query);

const users = await db.query.users.findMany({ where: and( filters.status && eq(users.status, filters.status), filters.search && ilike(users.name, %${filters.search}%) ), orderBy: [ filters.sortOrder === 'asc' ? asc(users[filters.sortBy]) : desc(users[filters.sortBy]), ], });

res.json({ data: users }); });

Error Response Format interface ErrorResponse { error: string; message: string; statusCode: number; details?: unknown; timestamp: string; path: string; }

export const formatError = ( err: AppError, req: Request ): ErrorResponse => ({ error: err.name, message: err.message, statusCode: err.statusCode, ...(err.details && { details: err.details }), timestamp: new Date().toISOString(), path: req.path, });

Environment Configuration Type-safe Environment Variables

src/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(), JWT_SECRET: z.string().min(32), LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'), });

export type Env = z.infer;

export const env = envSchema.parse(process.env);

Usage:

import { env } from './config/env';

const port = env.PORT; // Type-safe, validated

Testing Vitest Setup

vitest.config.ts:

import { defineConfig } from 'vitest/config';

export default defineConfig({ test: { globals: true, environment: 'node', setupFiles: ['./src/tests/setup.ts'], coverage: { provider: 'v8', reporter: ['text', 'json', 'html'], }, }, });

Integration Tests with Supertest

src/tests/users.test.ts:

import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import request from 'supertest'; import { app } from '../server'; import { db } from '../db/client';

describe('User API', () => { beforeAll(async () => { // Setup test database await db.delete(users); });

afterAll(async () => { // Cleanup });

it('should create a new user', async () => { const response = await request(app) .post('/users') .send({ email: 'test@example.com', name: 'Test User', password: 'password123', }) .expect(201);

expect(response.body).toMatchObject({
  email: 'test@example.com',
  name: 'Test User',
});
expect(response.body).toHaveProperty('id');
expect(response.body).not.toHaveProperty('passwordHash');

});

it('should return 400 for invalid email', async () => { const response = await request(app) .post('/users') .send({ email: 'invalid-email', name: 'Test User', password: 'password123', }) .expect(400);

expect(response.body).toHaveProperty('error');

}); });

Unit Tests

src/services/auth.test.ts:

import { describe, it, expect } from 'vitest'; import { AuthService } from './auth';

describe('AuthService', () => { it('should hash password correctly', async () => { const password = 'mySecurePassword123'; const hash = await AuthService.hashPassword(password);

expect(hash).not.toBe(password);
expect(hash.length).toBeGreaterThan(50);

});

it('should verify password correctly', async () => { const password = 'mySecurePassword123'; const hash = await AuthService.hashPassword(password);

const isValid = await AuthService.comparePassword(password, hash);
expect(isValid).toBe(true);

const isInvalid = await AuthService.comparePassword('wrongPassword', hash);
expect(isInvalid).toBe(false);

});

it('should generate valid JWT token', () => { const token = AuthService.generateToken({ userId: '123', email: 'test@example.com', });

expect(token).toBeTruthy();

const decoded = AuthService.verifyToken(token);
expect(decoded).toMatchObject({
  userId: '123',
  email: 'test@example.com',
});

}); });

Production Deployment Docker Setup

Dockerfile:

FROM node:20-alpine AS builder

WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build

FROM node:20-alpine

WORKDIR /app COPY package*.json ./ RUN npm ci --omit=dev COPY --from=builder /app/dist ./dist

ENV NODE_ENV=production EXPOSE 3000

CMD ["node", "dist/server.js"]

docker-compose.yml:

version: '3.8'

services: app: build: . ports: - "3000:3000" environment: - DATABASE_URL=postgresql://user:pass@db:5432/mydb - REDIS_URL=redis://redis:6379 - JWT_SECRET=${JWT_SECRET} depends_on: - db - redis

db: image: postgres:16-alpine environment: - POSTGRES_USER=user - POSTGRES_PASSWORD=pass - POSTGRES_DB=mydb volumes: - postgres_data:/var/lib/postgresql/data

redis: image: redis:7-alpine volumes: - redis_data:/data

volumes: postgres_data: redis_data:

PM2 Clustering

ecosystem.config.js:

module.exports = { apps: [{ name: 'api', script: './dist/server.js', instances: 'max', exec_mode: 'cluster', env: { NODE_ENV: 'production', }, error_file: './logs/err.log', out_file: './logs/out.log', log_date_format: 'YYYY-MM-DD HH:mm:ss Z', }], };

Best Practices Project Structure src/ ├── server.ts # Entry point ├── config/ │ └── env.ts # Environment config ├── routes/ │ ├── index.ts # Route aggregator │ ├── users.ts │ └── posts.ts ├── middleware/ │ ├── auth.ts │ ├── validation.ts │ └── errorHandler.ts ├── services/ │ ├── auth.ts │ └── user.ts ├── repositories/ │ └── userRepository.ts ├── db/ │ ├── client.ts │ └── schema.ts ├── types/ │ └── index.ts └── tests/ ├── setup.ts ├── users.test.ts └── auth.test.ts

Key Principles Separation of Concerns: Routes → Controllers → Services → Repositories Type Safety: Use TypeScript strict mode, Zod for runtime validation Error Handling: Centralized error handler, custom error classes Security: Helmet, rate limiting, input validation, CORS Logging: Structured logging (pino, winston), request IDs Testing: Unit tests for services, integration tests for APIs Documentation: OpenAPI/Swagger for API documentation Express vs Fastify

Use Express when:

Large ecosystem of middleware needed Team familiarity is priority Prototype/MVP development Legacy codebase compatibility

Use Fastify when:

Performance is critical (2-3x faster) Type safety is important (built-in TypeScript support) Schema validation required (JSON Schema built-in) Modern async/await patterns preferred Plugin architecture needed Performance Tips Use connection pooling for databases Implement caching (Redis, in-memory) Enable compression (gzip, brotli) Use clustering for CPU-intensive tasks Implement rate limiting Optimize database queries (indexes, query analysis) Use CDN for static assets Enable HTTP/2 in production

返回排行榜