backend について
backendは、APIやサービスを構築する開発者向けのバックエンド開発ガイドです。Express.js、NestJS、データベース、バックグラウンドジョブ、WebSocketに加え、よくある問題とコード検証ルールを扱います。
主な機能
- 01Express.jsのアプリ構成とミドルウェアパターン
- 02NestJSのモジュール、DTO、依存性注入
- 03Prismaのデータベース連携とトランザクション
- 04Bullのバックグラウンドジョブ例
- 05WebSocketゲートウェイのパターン
- 06バックエンドの注意点とコード検証ルール
活用シーン
- 01Express.jsサーバーアプリの構築
- 02NestJSのモジュールと入力検証の整理
- 03データベーストランザクションとジョブの実装
- 04未処理の非同期エラーやN+1クエリの調査
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ miles990/ claude-software-skills --skill backendインストール後、Claude Code で/backendを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
templates/README.mdtemplates/express/app.tsserver.tstemplates/fastapi/main.pyrequirements.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
コメント
Markdown 対応