Claude Code Plugins

Community-maintained marketplace

Feedback

Backend development with Node.js, Express, NestJS, and server patterns

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

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 [object Object]
collaboration [object Object]

Backend Development

Overview

Server-side development patterns, frameworks, and best practices for building scalable APIs and services.


Express.js

Application Structure

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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.*(?<!await\s)db\.|async.*(?<!await\s)fetch
  • 解法: 使用 try-catch 包裝、加上 global error handler、使用 ESLint no-floating-promises 規則

SE-2: N+1 查詢問題

  • 嚴重度: high
  • 情境: 在迴圈中執行資料庫查詢,導致效能急劇下降
  • 原因: ORM 的 lazy loading、沒有使用 batch fetch
  • 症狀:
    • 查詢數量與資料量成正比
    • API 響應時間隨資料量線性增長
    • 資料庫連接池耗盡
  • 檢測: for.*await.*findOne|forEach.*await.*find|\.map\(.*await.*query
  • 解法: 使用 eager loading (include/populate)、DataLoader、批次查詢

SE-3: 敏感資訊洩露

  • 嚴重度: critical
  • 情境: 錯誤訊息、日誌、或 API 回應中包含敏感資訊
  • 原因: 開發環境的 debug 設定被帶到生產、錯誤處理過於詳細
  • 症狀:
    • 錯誤回應包含 stack trace
    • 日誌中有密碼或 token
    • API 回應包含內部資料庫結構
  • 檢測: console\.log.*password|console\.log.*token|res\.json\(err\)|stack.*trace
  • 解法: 生產環境只回傳通用錯誤訊息、使用專門的錯誤序列化、過濾敏感欄位

SE-4: 競態條件 (Race Condition)

  • 嚴重度: high
  • 情境: 多個請求同時修改同一資源,導致資料不一致
  • 原因: 缺乏適當的鎖定機制、read-modify-write 沒有原子性
  • 症狀:
    • 庫存數量變成負數
    • 重複扣款
    • 資料覆蓋(後來的寫入覆蓋先前的)
  • 檢測: findOne.*update|get.*set|read.*write
  • 解法: 使用資料庫事務、樂觀鎖 (version field)、分散式鎖

SE-5: 未驗證的用戶輸入

  • 嚴重度: critical
  • 情境: 直接使用用戶輸入進行資料庫查詢或系統命令
  • 原因: 信任前端驗證、沒有後端驗證、拼接 SQL/命令字串
  • 症狀:
    • SQL Injection 攻擊
    • NoSQL Injection
    • Command Injection
  • 檢測: \$\{.*req\.body|query\(.*\+.*req\.|exec\(.*req\.|eval\(
  • 解法: 使用參數化查詢、輸入驗證(Zod/Joi)、白名單驗證

Validations

V-1: 禁止空的 catch block

  • 類型: regex
  • 嚴重度: critical
  • 模式: catch\s*\([^)]*\)\s*\{\s*\}
  • 訊息: Empty catch block silently swallows errors
  • 修復建議: Add error logging: console.error(err) or logger.error(err)
  • 適用: *.ts, *.js

V-2: 禁止 forEach + async

  • 類型: regex
  • 嚴重度: high
  • 模式: \.forEach\s*\(\s*async
  • 訊息: forEach does not await async callbacks - use for...of or Promise.all
  • 修復建議: Replace with for (const item of items) or await Promise.all(items.map(...))
  • 適用: *.ts, *.js

V-3: 檢測未處理的 Promise

  • 類型: regex
  • 嚴重度: high
  • 模式: (?<!await\s)(?<!return\s)(?<!\.\s*catch\()fetch\(|db\.(find|query|insert|update|delete)
  • 訊息: Async operation may not be awaited or caught
  • 修復建議: Add await keyword or .catch() handler
  • 適用: *.ts, *.js

V-4: 禁止硬編碼密鑰

  • 類型: regex
  • 嚴重度: critical
  • 模式: (password|secret|api_key|apikey|token)\s*[=:]\s*["'][^"']+["']
  • 訊息: Hardcoded secret detected - use environment variables
  • 修復建議: Move to environment variables: process.env.SECRET_KEY
  • 適用: *.ts, *.js, *.json

V-5: 禁止 SELECT *

  • 類型: regex
  • 嚴重度: medium
  • 模式: SELECT\s+\*\s+FROM|\.findMany\s*\(\s*\)|\.find\s*\(\s*\{\s*\}\s*\)
  • 訊息: SELECT * fetches unnecessary data - specify fields
  • 修復建議: Explicitly list needed fields: SELECT id, name FROM...
  • 適用: *.ts, *.js, *.sql