mcpField
B
backend

backend

github.com/miles990開発2659 回インストール40 週間前に更新
リポジトリ

backend について

backendは、APIやサービスを構築する開発者向けのバックエンド開発ガイドです。Express.js、NestJS、データベース、バックグラウンドジョブ、WebSocketに加え、よくある問題とコード検証ルールを扱います。

主な機能

  1. 01Express.jsのアプリ構成とミドルウェアパターン
  2. 02NestJSのモジュール、DTO、依存性注入
  3. 03Prismaのデータベース連携とトランザクション
  4. 04Bullのバックグラウンドジョブ例
  5. 05WebSocketゲートウェイのパターン
  6. 06バックエンドの注意点とコード検証ルール

活用シーン

  1. 01Express.jsサーバーアプリの構築
  2. 02NestJSのモジュールと入力検証の整理
  3. 03データベーストランザクションとジョブの実装
  4. 04未処理の非同期エラーやN+1クエリの調査

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/miles990/claude-software-skills --skill backend

インストール後、Claude Code で/backendを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

templates/
README.md
templates/express/
app.ts
server.ts
templates/fastapi/
main.py
requirements.txt

詳細

バックエンド開発

概要

スケーラブルな API とサービスを構築するための、サーバーサイド開発のパターン、フレームワーク、ベストプラクティスです。


Express.js

アプリケーション構造
// 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;
ミドルウェアのパターン
// 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();
  };
}
エラー処理
// 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

モジュール構造
// 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);
  }
}
バリデーション付き DTO
// 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: '[email protected]' })
  @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)
) {}
依存性注入を使用したサービス
// 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 } });
  }
}

データベース連携

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();
  }
}
トランザクション
// 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 };
  });
}

バックグラウンドジョブ

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;
  }
}

WebSocket

// 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);
  }
}

関連スキル

  • [[api-design]] - API 設計パターン
  • [[database]] - データベースのパターン
  • [[security-practices]] - バックエンドのセキュリティ

注意すべき落とし穴(よくある落とし穴)

これらは、バックエンド開発で最もよくある、影響の大きなミスです

SE-1: Promise の未処理の Reject
  • 重大度: critical
  • 状況: Async 関数内のエラーが catch されず、アプリケーションのクラッシュやサイレントな失敗につながります
  • 原因: await の付け忘れ、エラー処理の不足、または callback 内で async を使用している
  • 症状:
    • UnhandledPromiseRejectionWarning
    • API リクエストがハングして応答しない
    • データベース操作が一部だけ完了する
  • 検出: \.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)、許可リストによる検証を使用します

検証

V-1: 空の catch block を禁止
  • 種類: regex
  • 重大度: critical
  • パターン: catch\s*\([^)]*\)\s*\{\s*\}
  • メッセージ: 空の catch block はエラーを黙って握りつぶします
  • 修正の提案: エラーログを追加します:console.error(err) または logger.error(err)
  • 対象: *.ts, *.js
V-2: forEach + async を禁止
  • 種類: regex
  • 重大度: high
  • パターン: \.forEach\s*\(\s*async
  • メッセージ: forEach は async callback の完了を待ちません。for...of または Promise.all を使用してください
  • 修正の提案: for (const item of items) または 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 操作が await または catch されていない可能性があります
  • 修正の提案: await キーワードまたは .catch() ハンドラーを追加します
  • 対象: *.ts, *.js
V-4: キーのハードコードを禁止
  • 種類: regex
  • 重大度: critical
  • パターン: (password|secret|api_key|apikey|token)\s*[=:]\s*["'][^"']+["']
  • メッセージ: ハードコードされたシークレットが検出されました。環境変数を使用してください
  • 修正の提案: 環境変数に移動します: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 * は不要なデータを取得します。フィールドを指定してください
  • 修正の提案: 必要なフィールドを明示的に列挙します:SELECT id, name FROM...
  • 対象: *.ts, *.js, *.sql

コメント