--- name: backend description: Backend development with Node.js, Express, NestJS, and server patterns domain: development-stacks version: 1.0.0 tags: [nodejs, express, nestjs, fastify, api, middleware, authentication] triggers: keywords: primary: [backend, server, nodejs, express, nestjs, api, microservice] secondary: [fastify, middleware, authentication, rest, graphql, queue] context_boost: [service, endpoint, database, http] context_penalty: [frontend, css, ui, mobile] priority: high collaboration: prerequisites: - skill: typescript reason: Type-safe backend development delegation_triggers: - trigger: API endpoint design decisions delegate_to: api-design context: RESTful conventions, versioning, error formats - trigger: Database schema design or query optimization delegate_to: database context: Data modeling, indexing strategy - trigger: Writing tests for services or controllers delegate_to: testing-strategies context: Integration test patterns, mocking strategies receives_context_from: - skill: api-design receives: - Endpoint naming conventions - Request/response formats - Error response structure - skill: database receives: - Connection pool settings - Query optimization hints - Transaction patterns provides_context_to: - skill: frontend provides: - API endpoints list - Authentication flow - WebSocket events - skill: testing-strategies provides: - Service dependencies to mock - Integration test scenarios --- # Backend Development ## Overview Server-side development patterns, frameworks, and best practices for building scalable APIs and services. --- ## Express.js ### Application Structure ```typescript // app.ts import express from 'express'; import helmet from 'helmet'; import cors from 'cors'; import compression from 'compression'; import { errorHandler } from './middleware/errorHandler'; import { requestLogger } from './middleware/requestLogger'; import routes from './routes'; const app = express(); // Security middleware app.use(helmet()); app.use(cors({ origin: process.env.ALLOWED_ORIGINS?.split(',') || '*', credentials: true, })); // Request processing app.use(compression()); app.use(express.json({ limit: '10kb' })); app.use(express.urlencoded({ extended: true })); // Logging app.use(requestLogger); // Routes app.use('/api/v1', routes); // Health check app.get('/health', (req, res) => { res.json({ status: 'healthy', timestamp: new Date().toISOString() }); }); // Error handling (must be last) app.use(errorHandler); export default app; ``` ### Middleware Patterns ```typescript // Authentication middleware import { Request, Response, NextFunction } from 'express'; import jwt from 'jsonwebtoken'; interface AuthRequest extends Request { user?: { id: string; role: string }; } export function authenticate(req: AuthRequest, res: Response, next: NextFunction) { const token = req.headers.authorization?.replace('Bearer ', ''); if (!token) { return res.status(401).json({ error: 'Authentication required' }); } try { const decoded = jwt.verify(token, process.env.JWT_SECRET!) as { id: string; role: string }; req.user = decoded; next(); } catch { return res.status(401).json({ error: 'Invalid token' }); } } // Authorization middleware export function authorize(...roles: string[]) { return (req: AuthRequest, res: Response, next: NextFunction) => { if (!req.user || !roles.includes(req.user.role)) { return res.status(403).json({ error: 'Forbidden' }); } next(); }; } // Rate limiting import rateLimit from 'express-rate-limit'; export const apiLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100, message: { error: 'Too many requests' }, standardHeaders: true, }); // Validation middleware import { z } from 'zod'; export function validate(schema: z.ZodSchema) { return (req: Request, res: Response, next: NextFunction) => { const result = schema.safeParse({ body: req.body, query: req.query, params: req.params, }); if (!result.success) { return res.status(400).json({ error: 'Validation failed', details: result.error.flatten(), }); } req.body = result.data.body; req.query = result.data.query; req.params = result.data.params; next(); }; } ``` ### Error Handling ```typescript // Custom error classes export class AppError extends Error { constructor( public statusCode: number, public message: string, public isOperational = true ) { super(message); Error.captureStackTrace(this, this.constructor); } } export class NotFoundError extends AppError { constructor(resource: string) { super(404, `${resource} not found`); } } export class ValidationError extends AppError { constructor(message: string) { super(400, message); } } // Global error handler export function errorHandler( err: Error, req: Request, res: Response, next: NextFunction ) { console.error('Error:', err); if (err instanceof AppError) { return res.status(err.statusCode).json({ error: err.message, ...(process.env.NODE_ENV === 'development' && { stack: err.stack }), }); } // Mongoose/Prisma errors if (err.name === 'CastError') { return res.status(400).json({ error: 'Invalid ID format' }); } if (err.name === 'ValidationError') { return res.status(400).json({ error: err.message }); } // Default error res.status(500).json({ error: process.env.NODE_ENV === 'production' ? 'Internal server error' : err.message, }); } ``` --- ## NestJS ### Module Structure ```typescript // users/users.module.ts import { Module } from '@nestjs/common'; import { UsersController } from './users.controller'; import { UsersService } from './users.service'; import { PrismaModule } from '../prisma/prisma.module'; @Module({ imports: [PrismaModule], controllers: [UsersController], providers: [UsersService], exports: [UsersService], }) export class UsersModule {} // users/users.controller.ts import { Controller, Get, Post, Put, Delete, Body, Param, Query, UseGuards, HttpCode, HttpStatus, } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger'; import { UsersService } from './users.service'; import { CreateUserDto, UpdateUserDto, QueryUsersDto } from './dto'; import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; import { RolesGuard } from '../auth/guards/roles.guard'; import { Roles } from '../auth/decorators/roles.decorator'; @ApiTags('users') @Controller('users') @UseGuards(JwtAuthGuard, RolesGuard) @ApiBearerAuth() export class UsersController { constructor(private readonly usersService: UsersService) {} @Get() @ApiOperation({ summary: 'List users' }) async findAll(@Query() query: QueryUsersDto) { return this.usersService.findAll(query); } @Get(':id') @ApiOperation({ summary: 'Get user by ID' }) async findOne(@Param('id') id: string) { return this.usersService.findOne(id); } @Post() @Roles('admin') @ApiOperation({ summary: 'Create user' }) async create(@Body() createUserDto: CreateUserDto) { return this.usersService.create(createUserDto); } @Put(':id') @ApiOperation({ summary: 'Update user' }) async update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) { return this.usersService.update(id, updateUserDto); } @Delete(':id') @Roles('admin') @HttpCode(HttpStatus.NO_CONTENT) @ApiOperation({ summary: 'Delete user' }) async remove(@Param('id') id: string) { await this.usersService.remove(id); } } ``` ### DTOs with Validation ```typescript // users/dto/create-user.dto.ts import { IsEmail, IsString, MinLength, IsOptional, IsEnum } from 'class-validator'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; export enum UserRole { USER = 'user', ADMIN = 'admin', } export class CreateUserDto { @ApiProperty({ example: 'user@example.com' }) @IsEmail() email: string; @ApiProperty({ minLength: 8 }) @IsString() @MinLength(8) password: string; @ApiPropertyOptional() @IsOptional() @IsString() name?: string; @ApiPropertyOptional({ enum: UserRole, default: UserRole.USER }) @IsOptional() @IsEnum(UserRole) role?: UserRole = UserRole.USER; } // Partial DTO for updates import { PartialType, OmitType } from '@nestjs/swagger'; export class UpdateUserDto extends PartialType( OmitType(CreateUserDto, ['password'] as const) ) {} ``` ### Services with Dependency Injection ```typescript // users/users.service.ts import { Injectable, NotFoundException } from '@nestjs/common'; import { PrismaService } from '../prisma/prisma.service'; import { CreateUserDto, UpdateUserDto, QueryUsersDto } from './dto'; import * as argon2 from 'argon2'; @Injectable() export class UsersService { constructor(private prisma: PrismaService) {} async findAll(query: QueryUsersDto) { const { page = 1, limit = 10, search } = query; const where = search ? { OR: [ { name: { contains: search, mode: 'insensitive' } }, { email: { contains: search, mode: 'insensitive' } }, ], } : {}; const [users, total] = await Promise.all([ this.prisma.user.findMany({ where, skip: (page - 1) * limit, take: limit, select: { id: true, email: true, name: true, role: true, createdAt: true, }, }), this.prisma.user.count({ where }), ]); return { data: users, meta: { page, limit, total, totalPages: Math.ceil(total / limit), }, }; } async findOne(id: string) { const user = await this.prisma.user.findUnique({ where: { id }, select: { id: true, email: true, name: true, role: true, createdAt: true, }, }); if (!user) { throw new NotFoundException(`User with ID ${id} not found`); } return user; } async create(dto: CreateUserDto) { const hashedPassword = await argon2.hash(dto.password); return this.prisma.user.create({ data: { ...dto, password: hashedPassword, }, select: { id: true, email: true, name: true, role: true, }, }); } async update(id: string, dto: UpdateUserDto) { await this.findOne(id); // Throws if not found return this.prisma.user.update({ where: { id }, data: dto, select: { id: true, email: true, name: true, role: true, }, }); } async remove(id: string) { await this.findOne(id); // Throws if not found await this.prisma.user.delete({ where: { id } }); } } ``` --- ## Database Integration ### Prisma ORM ```typescript // prisma/schema.prisma generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } model User { id String @id @default(cuid()) email String @unique password String name String? role Role @default(USER) posts Post[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } model Post { id String @id @default(cuid()) title String content String? published Boolean @default(false) author User @relation(fields: [authorId], references: [id]) authorId String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } enum Role { USER ADMIN } // Prisma service import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common'; import { PrismaClient } from '@prisma/client'; @Injectable() export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy { async onModuleInit() { await this.$connect(); } async onModuleDestroy() { await this.$disconnect(); } } ``` ### Transactions ```typescript // Complex transaction async transferFunds(fromId: string, toId: string, amount: number) { return this.prisma.$transaction(async (tx) => { const from = await tx.account.update({ where: { id: fromId }, data: { balance: { decrement: amount } }, }); if (from.balance < 0) { throw new Error('Insufficient funds'); } const to = await tx.account.update({ where: { id: toId }, data: { balance: { increment: amount } }, }); await tx.transfer.create({ data: { fromId, toId, amount, }, }); return { from, to }; }); } ``` --- ## Background Jobs ### Bull Queue ```typescript // jobs/email.processor.ts import { Process, Processor } from '@nestjs/bull'; import { Job } from 'bull'; import { MailService } from '../mail/mail.service'; @Processor('email') export class EmailProcessor { constructor(private mailService: MailService) {} @Process('welcome') async handleWelcomeEmail(job: Job<{ email: string; name: string }>) { const { email, name } = job.data; await this.mailService.sendWelcome(email, name); } @Process('password-reset') async handlePasswordReset(job: Job<{ email: string; token: string }>) { const { email, token } = job.data; await this.mailService.sendPasswordReset(email, token); } } // Using the queue import { InjectQueue } from '@nestjs/bull'; import { Queue } from 'bull'; @Injectable() export class UsersService { constructor(@InjectQueue('email') private emailQueue: Queue) {} async createUser(dto: CreateUserDto) { const user = await this.prisma.user.create({ data: dto }); await this.emailQueue.add('welcome', { email: user.email, name: user.name, }); return user; } } ``` --- ## WebSockets ```typescript // events/events.gateway.ts import { WebSocketGateway, WebSocketServer, SubscribeMessage, OnGatewayConnection, OnGatewayDisconnect, } from '@nestjs/websockets'; import { Server, Socket } from 'socket.io'; @WebSocketGateway({ cors: { origin: '*' }, namespace: '/events', }) export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { @WebSocketServer() server: Server; handleConnection(client: Socket) { console.log(`Client connected: ${client.id}`); } handleDisconnect(client: Socket) { console.log(`Client disconnected: ${client.id}`); } @SubscribeMessage('join-room') handleJoinRoom(client: Socket, room: string) { client.join(room); this.server.to(room).emit('user-joined', { userId: client.id }); } @SubscribeMessage('message') handleMessage(client: Socket, payload: { room: string; message: string }) { this.server.to(payload.room).emit('message', { userId: client.id, message: payload.message, timestamp: new Date(), }); } // Broadcast from service broadcastToRoom(room: string, event: string, data: any) { this.server.to(room).emit(event, data); } } ``` --- ## Related Skills - [[api-design]] - API design patterns - [[database]] - Database patterns - [[security-practices]] - Backend security --- ## Sharp Edges(常見陷阱) > 這些是後端開發中最常見且代價最高的錯誤 ### SE-1: 未處理的 Promise Rejection - **嚴重度**: critical - **情境**: Async 函數中的錯誤沒有被 catch,導致應用程式崩潰或無聲失敗 - **原因**: 忘記 await、沒有錯誤處理、或在 callback 中使用 async - **症狀**: - UnhandledPromiseRejectionWarning - API 請求 hang 住不回應 - 資料庫操作部分完成 - **檢測**: `\.then\([^)]*\)(?!\s*\.catch)|async.*(?