nodejs-backend-patterns
Node.jsバックエンドサービスをExpress/Fastifyで構築し、ミドルウェアパターン・エラーハンドリング・認証・データベース連携・API設計のベストプラクティスを実装します。Node.jsサーバー、REST API、GraphQLバックエンド、マイクロサービスアーキテクチャを作成する際に使用してください。
description の原文を見る
Build production-ready Node.js backend services with Express/Fastify, implementing middleware patterns, error handling, authentication, database integration, and API design best practices. Use when creating Node.js servers, REST APIs, GraphQL backends, or microservices architectures.
SKILL.md 本文
Node.js Backend Patterns
スケーラブルで保守性が高く、プロダクション対応のNode.jsバックエンドアプリケーションを構築するための包括的なガイダンスです。最新のフレームワーク、アーキテクチャパターン、ベストプラクティスを活用します。
このスキルを使うべき場合
- REST APIまたはGraphQLサーバーの構築
- Node.jsを使用したマイクロサービスの作成
- 認証と認可の実装
- スケーラブルなバックエンドアーキテクチャの設計
- ミドルウェアとエラーハンドリングの設定
- データベースの統合(SQLおよびNoSQL)
- WebSocketを使用したリアルタイムアプリケーションの構築
- バックグラウンドジョブ処理の実装
コアフレームワーク
Express.js - ミニマリストフレームワーク
基本セットアップ:
import express, { Request, Response, NextFunction } from "express";
import helmet from "helmet";
import cors from "cors";
import compression from "compression";
const app = express();
// Security middleware
app.use(helmet());
app.use(cors({ origin: process.env.ALLOWED_ORIGINS?.split(",") }));
app.use(compression());
// Body parsing
app.use(express.json({ limit: "10mb" }));
app.use(express.urlencoded({ extended: true, limit: "10mb" }));
// Request logging
app.use((req: Request, res: Response, next: NextFunction) => {
console.log(`${req.method} ${req.path}`);
next();
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Fastify - 高パフォーマンスフレームワーク
基本セットアップ:
import Fastify from "fastify";
import helmet from "@fastify/helmet";
import cors from "@fastify/cors";
import compress from "@fastify/compress";
const fastify = Fastify({
logger: {
level: process.env.LOG_LEVEL || "info",
transport: {
target: "pino-pretty",
options: { colorize: true },
},
},
});
// Plugins
await fastify.register(helmet);
await fastify.register(cors, { origin: true });
await fastify.register(compress);
// Type-safe routes with schema validation
fastify.post<{
Body: { name: string; email: string };
Reply: { id: string; name: string };
}>(
"/users",
{
schema: {
body: {
type: "object",
required: ["name", "email"],
properties: {
name: { type: "string", minLength: 1 },
email: { type: "string", format: "email" },
},
},
},
},
async (request, reply) => {
const { name, email } = request.body;
return { id: "123", name };
},
);
await fastify.listen({ port: 3000, host: "0.0.0.0" });
アーキテクチャパターン
パターン1: レイヤード・アーキテクチャ
構造:
src/
├── controllers/ # HTTPリクエスト/レスポンスの処理
├── services/ # ビジネスロジック
├── repositories/ # データアクセス層
├── models/ # データモデル
├── middleware/ # Express/Fastifyミドルウェア
├── routes/ # ルート定義
├── utils/ # ヘルパー関数
├── config/ # 設定
└── types/ # TypeScript型
コントローラー層:
// controllers/user.controller.ts
import { Request, Response, NextFunction } from "express";
import { UserService } from "../services/user.service";
import { CreateUserDTO, UpdateUserDTO } from "../types/user.types";
export class UserController {
constructor(private userService: UserService) {}
async createUser(req: Request, res: Response, next: NextFunction) {
try {
const userData: CreateUserDTO = req.body;
const user = await this.userService.createUser(userData);
res.status(201).json(user);
} catch (error) {
next(error);
}
}
async getUser(req: Request, res: Response, next: NextFunction) {
try {
const { id } = req.params;
const user = await this.userService.getUserById(id);
res.json(user);
} catch (error) {
next(error);
}
}
async updateUser(req: Request, res: Response, next: NextFunction) {
try {
const { id } = req.params;
const updates: UpdateUserDTO = req.body;
const user = await this.userService.updateUser(id, updates);
res.json(user);
} catch (error) {
next(error);
}
}
async deleteUser(req: Request, res: Response, next: NextFunction) {
try {
const { id } = req.params;
await this.userService.deleteUser(id);
res.status(204).send();
} catch (error) {
next(error);
}
}
}
サービス層:
// services/user.service.ts
import { UserRepository } from "../repositories/user.repository";
import { CreateUserDTO, UpdateUserDTO, User } from "../types/user.types";
import { NotFoundError, ValidationError } from "../utils/errors";
import bcrypt from "bcrypt";
export class UserService {
constructor(private userRepository: UserRepository) {}
async createUser(userData: CreateUserDTO): Promise<User> {
// Validation
const existingUser = await this.userRepository.findByEmail(userData.email);
if (existingUser) {
throw new ValidationError("Email already exists");
}
// Hash password
const hashedPassword = await bcrypt.hash(userData.password, 10);
// Create user
const user = await this.userRepository.create({
...userData,
password: hashedPassword,
});
// Remove password from response
const { password, ...userWithoutPassword } = user;
return userWithoutPassword as User;
}
async getUserById(id: string): Promise<User> {
const user = await this.userRepository.findById(id);
if (!user) {
throw new NotFoundError("User not found");
}
const { password, ...userWithoutPassword } = user;
return userWithoutPassword as User;
}
async updateUser(id: string, updates: UpdateUserDTO): Promise<User> {
const user = await this.userRepository.update(id, updates);
if (!user) {
throw new NotFoundError("User not found");
}
const { password, ...userWithoutPassword } = user;
return userWithoutPassword as User;
}
async deleteUser(id: string): Promise<void> {
const deleted = await this.userRepository.delete(id);
if (!deleted) {
throw new NotFoundError("User not found");
}
}
}
リポジトリ層:
// repositories/user.repository.ts
import { Pool } from "pg";
import { CreateUserDTO, UpdateUserDTO, UserEntity } from "../types/user.types";
export class UserRepository {
constructor(private db: Pool) {}
async create(
userData: CreateUserDTO & { password: string },
): Promise<UserEntity> {
const query = `
INSERT INTO users (name, email, password)
VALUES ($1, $2, $3)
RETURNING id, name, email, password, created_at, updated_at
`;
const { rows } = await this.db.query(query, [
userData.name,
userData.email,
userData.password,
]);
return rows[0];
}
async findById(id: string): Promise<UserEntity | null> {
const query = "SELECT * FROM users WHERE id = $1";
const { rows } = await this.db.query(query, [id]);
return rows[0] || null;
}
async findByEmail(email: string): Promise<UserEntity | null> {
const query = "SELECT * FROM users WHERE email = $1";
const { rows } = await this.db.query(query, [email]);
return rows[0] || null;
}
async update(id: string, updates: UpdateUserDTO): Promise<UserEntity | null> {
const fields = Object.keys(updates);
const values = Object.values(updates);
const setClause = fields
.map((field, idx) => `${field} = $${idx + 2}`)
.join(", ");
const query = `
UPDATE users
SET ${setClause}, updated_at = CURRENT_TIMESTAMP
WHERE id = $1
RETURNING *
`;
const { rows } = await this.db.query(query, [id, ...values]);
return rows[0] || null;
}
async delete(id: string): Promise<boolean> {
const query = "DELETE FROM users WHERE id = $1";
const { rowCount } = await this.db.query(query, [id]);
return rowCount > 0;
}
}
パターン2: 依存性注入
DIコンテナを使用してリポジトリ、サービス、コントローラーを接続します。完全なコンテナ実装については、references/advanced-patterns.mdを参照してください。
ミドルウェアパターン
認証ミドルウェア
// middleware/auth.middleware.ts
import { Request, Response, NextFunction } from "express";
import jwt from "jsonwebtoken";
import { UnauthorizedError } from "../utils/errors";
interface JWTPayload {
userId: string;
email: string;
}
declare global {
namespace Express {
interface Request {
user?: JWTPayload;
}
}
}
export const authenticate = async (
req: Request,
res: Response,
next: NextFunction,
) => {
try {
const token = req.headers.authorization?.replace("Bearer ", "");
if (!token) {
throw new UnauthorizedError("No token provided");
}
const payload = jwt.verify(token, process.env.JWT_SECRET!) as JWTPayload;
req.user = payload;
next();
} catch (error) {
next(new UnauthorizedError("Invalid token"));
}
};
export const authorize = (...roles: string[]) => {
return async (req: Request, res: Response, next: NextFunction) => {
if (!req.user) {
return next(new UnauthorizedError("Not authenticated"));
}
// Check if user has required role
const hasRole = roles.some((role) => req.user?.roles?.includes(role));
if (!hasRole) {
return next(new UnauthorizedError("Insufficient permissions"));
}
next();
};
};
バリデーションミドルウェア
// middleware/validation.middleware.ts
import { Request, Response, NextFunction } from "express";
import { AnyZodObject, ZodError } from "zod";
import { ValidationError } from "../utils/errors";
export const validate = (schema: AnyZodObject) => {
return async (req: Request, res: Response, next: NextFunction) => {
try {
await schema.parseAsync({
body: req.body,
query: req.query,
params: req.params,
});
next();
} catch (error) {
if (error instanceof ZodError) {
const errors = error.errors.map((err) => ({
field: err.path.join("."),
message: err.message,
}));
next(new ValidationError("Validation failed", errors));
} else {
next(error);
}
}
};
};
// Usage with Zod
import { z } from "zod";
const createUserSchema = z.object({
body: z.object({
name: z.string().min(1),
email: z.string().email(),
password: z.string().min(8),
}),
});
router.post("/users", validate(createUserSchema), userController.createUser);
レート制限ミドルウェア
// middleware/rate-limit.middleware.ts
import rateLimit from "express-rate-limit";
import RedisStore from "rate-limit-redis";
import Redis from "ioredis";
const redis = new Redis({
host: process.env.REDIS_HOST,
port: parseInt(process.env.REDIS_PORT || "6379"),
});
export const apiLimiter = rateLimit({
store: new RedisStore({
client: redis,
prefix: "rl:",
}),
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per windowMs
message: "Too many requests from this IP, please try again later",
standardHeaders: true,
legacyHeaders: false,
});
export const authLimiter = rateLimit({
store: new RedisStore({
client: redis,
prefix: "rl:auth:",
}),
windowMs: 15 * 60 * 1000,
max: 5, // Stricter limit for auth endpoints
skipSuccessfulRequests: true,
});
リクエストログミドルウェア
// middleware/logger.middleware.ts
import { Request, Response, NextFunction } from "express";
import pino from "pino";
const logger = pino({
level: process.env.LOG_LEVEL || "info",
transport: {
target: "pino-pretty",
options: { colorize: true },
},
});
export const requestLogger = (
req: Request,
res: Response,
next: NextFunction,
) => {
const start = Date.now();
// Log response when finished
res.on("finish", () => {
const duration = Date.now() - start;
logger.info({
method: req.method,
url: req.url,
status: res.statusCode,
duration: `${duration}ms`,
userAgent: req.headers["user-agent"],
ip: req.ip,
});
});
next();
};
export { logger };
エラーハンドリング
カスタムエラークラス
// utils/errors.ts
export class AppError extends Error {
constructor(
public message: string,
public statusCode: number = 500,
public isOperational: boolean = true,
) {
super(message);
Object.setPrototypeOf(this, AppError.prototype);
Error.captureStackTrace(this, this.constructor);
}
}
export class ValidationError extends AppError {
constructor(
message: string,
public errors?: any[],
) {
super(message, 400);
}
}
export class NotFoundError extends AppError {
constructor(message: string = "Resource not found") {
super(message, 404);
}
}
export class UnauthorizedError extends AppError {
constructor(message: string = "Unauthorized") {
super(message, 401);
}
}
export class ForbiddenError extends AppError {
constructor(message: string = "Forbidden") {
super(message, 403);
}
}
export class ConflictError extends AppError {
constructor(message: string) {
super(message, 409);
}
}
グローバルエラーハンドラー
// middleware/error-handler.ts
import { Request, Response, NextFunction } from "express";
import { AppError } from "../utils/errors";
import { logger } from "./logger.middleware";
export const errorHandler = (
err: Error,
req: Request,
res: Response,
next: NextFunction,
) => {
if (err instanceof AppError) {
return res.status(err.statusCode).json({
status: "error",
message: err.message,
...(err instanceof ValidationError && { errors: err.errors }),
});
}
// Log unexpected errors
logger.error({
error: err.message,
stack: err.stack,
url: req.url,
method: req.method,
});
// Don't leak error details in production
const message =
process.env.NODE_ENV === "production"
? "Internal server error"
: err.message;
res.status(500).json({
status: "error",
message,
});
};
// Async error wrapper
export const asyncHandler = (
fn: (req: Request, res: Response, next: NextFunction) => Promise<any>,
) => {
return (req: Request, res: Response, next: NextFunction) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
};
データベースパターン
Node.jsはSQLデータベースとNoSQLデータベースの両方をサポートしています。すべてのプロダクションデータベースに対してコネクションプーリングを使用してください。
references/advanced-patterns.mdで説明されている主なパターン:
- PostgreSQLとコネクションプール —
pgPoolの設定とグレースフルシャットダウン - MongoDBとMongoose — コネクション管理とスキーマ定義
- トランザクションパターン —
pgクライアントを使用したBEGIN/COMMIT/ROLLBACK
認証と認可
JWTベースの認証(短期アクセストークン(15分)と長期リフレッシュトークン(7日))。bcryptパスワード比較を使用した完全なAuthService実装はreferences/advanced-patterns.mdに掲載されています。
キャッシング戦略
Redis バックアップのCacheService(get/set/delete/invalidatePattern機能)と、メソッドレベルのキャッシング用@Cacheableデコレータ。詳細はreferences/advanced-patterns.mdを参照してください。
APIレスポンスフォーマット
success、error、paginatedスタティックメソッドを備えた標準化されたApiResponseヘルパー。詳細はreferences/advanced-patterns.mdを参照してください。
ベストプラクティス
- TypeScriptを使用: 型安全性がランタイムエラーを防止
- 適切なエラーハンドリングを実装: カスタムエラークラスを使用
- 入力を検証: ZodやJoiなどのライブラリを使用
- 環境変数を使用: シークレットをハードコーディングしない
- ロギングを実装: 構造化ログ(Pino、Winston)を使用
- レート制限を追加: 悪用を防止
- HTTPSを使用: プロダクションでは常に使用
- CORSを適切に実装: プロダクションでは
*を使用しない - 依存性注入を使用: テストと保守が容易
- テストを記述: ユニット、統合、E2Eテスト
- グレースフルシャットダウンを処理: リソースをクリーンアップ
- コネクションプーリングを使用: データベース用
- ヘルスチェックを実装: 監視用
- 圧縮を使用: レスポンスサイズを削減
- パフォーマンスを監視: APMツールを使用
テストパターン
包括的なテストガイダンスについてはjavascript-testing-patternsスキルを参照してください。
ライセンス: MIT(寛容ライセンスのため全文を引用しています) · 原本リポジトリ
詳細情報
- 作者
- wshobson
- リポジトリ
- wshobson/agents
- ライセンス
- MIT
- 最終更新
- 不明
Source: https://github.com/wshobson/agents / ライセンス: MIT
関連スキル
doubt-driven-development
重要な判断はすべて、本番環境への展開前に新しい視点から対抗的レビューを実施します。速度より正確性が重要な場合、不慣れなコードを扱う場合、本番環境・セキュリティに関わるロジック・取り消し不可の操作など影響度が高い場合、または後でバグを修正するよりも今検証する方が効率的な場合に活用してください。
apprun-skills
TypeScriptを使用したAppRunアプリケーションのMVU設計に関する総合的なガイダンスが得られます。コンポーネントパターン、イベントハンドリング、状態管理(非同期ジェネレータを含む)、パラメータと保護機能を備えたルーティング・ナビゲーション、vistestを使用したテストに対応しています。AppRunコンポーネントの設計・レビュー、ルートの配線、状態フローの管理、AppRunテストの作成時に活用してください。
desloppify
コードベースのヘルスチェックと技術負債の追跡ツールです。コード品質、技術負債、デッドコード、大規模ファイル、ゴッドクラス、重複関数、コードスメル、命名規則の問題、インポートサイクル、結合度の問題についてユーザーが質問した場合に使用してください。また、ヘルススコアの確認、次の改善項目の提案、クリーンアップ計画の作成をリクエストされた際にも対応します。29言語に対応しています。
debugging-and-error-recovery
テストが失敗したり、ビルドが壊れたり、動作が期待と異なったり、予期しないエラーが発生したりした場合に、体系的な根本原因デバッグをガイドします。推測ではなく、根本原因を見つけて修正するための体系的なアプローチが必要な場合に使用してください。
test-driven-development
テスト駆動開発により実装を進めます。ロジックの実装、バグの修正、動作の変更など、あらゆる場面で活用できます。コードが正常に動作することを証明する必要がある場合、バグ報告を受けた場合、既存機能を修正する予定がある場合に使用してください。
incremental-implementation
変更を段階的に実施します。複数のファイルに影響する機能や変更を実装する場合に使用してください。大量のコードを一度に書こうとしている場合や、タスクが一度では完結できないほど大きい場合に活用します。