mcpField
F
fullstack-dev

fullstack-dev

github.com/minimax-ai開発13.7k3.4k 回インストール28 週間前に更新
リポジトリ

fullstack-dev について

fullstack-dev は、フルスタックアプリやバックエンドサービスの開発向けコーディングスキルです。要件確認、設計判断、実装チェックリスト、検証を順に進め、引き継ぎまで支援します。

主な機能

  1. 01技術スタックとサービス要件の確認
  2. 02機能別プロジェクト構成のガイド
  3. 03バックエンドと連携のチェックリスト
  4. 04エラー、データベース、認証の設計例
  5. 05ビルドと連携の検証を要求

活用シーン

  1. 01フルスタックアプリの新規構築
  2. 02バックエンドのモジュール境界設計
  3. 03フロントエンドと API の連携確認
  4. 04サービスの本番向け強化

インストール

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

$npx skills add https://github.com/minimax-ai/skills --skill fullstack-dev

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

同梱ファイル

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

references/
api-design.md
auth-flow.md
db-schema.md
django-best-practices.md
environment-management.md
release-checklist.md
technology-selection.md
testing-strategy.md

詳細

フルスタック開発のプラクティス

必須ワークフロー — 以下の手順を順番に実行してください

このスキルが起動したら、コードを書く前に必ずこのワークフローに従ってください。

ステップ 0:要件の確認

何かをスキャフォールディングする前に、ユーザーに以下を確認してください(またはコンテキストから推測してください)。

  1. スタック:バックエンドとフロントエンドの言語/フレームワーク(例:Express + React、Django + Vue、Go + HTMX)
  2. サービスの種類:API のみ、フルスタックのモノリス、またはマイクロサービスですか?
  3. データベース:SQL(PostgreSQL、SQLite、MySQL)または NoSQL(MongoDB、Redis)ですか?
  4. 連携方式:REST、GraphQL、tRPC、または gRPC ですか?
  5. リアルタイム性:必要ですか?必要な場合は、SSE、WebSocket、ポーリングのどれですか?
  6. 認証:必要ですか?必要な場合は、JWT、セッション、OAuth、またはサードパーティ(Clerk、Auth.js)のどれですか?

ユーザーがすでに依頼の中でこれらを指定している場合は、質問を省略して進めてください。

ステップ 1:アーキテクチャ上の決定

要件に基づき、コーディングの前に以下の決定を行い、明示してください。

決定事項選択肢参照
プロジェクト構成機能優先(推奨)またはレイヤー優先セクション 1
API クライアントの方式型付き fetch / React Query / tRPC / OpenAPI コード生成セクション 5
認証戦略JWT + リフレッシュ / セッション / サードパーティセクション 6
リアルタイム方式ポーリング / SSE / WebSocketセクション 11
エラー処理型付きエラー階層 + グローバルハンドラーセクション 3

各選択について簡潔に説明してください(決定事項ごとに 1 文)。

ステップ 2:チェックリストを使ったスキャフォールディング

以下の適切なチェックリストを使ってください。チェック項目をすべて実装し、省略しないでください。

ステップ 3:パターンに従って実装

このドキュメントのパターンに従ってコードを書いてください。各パートの実装時に、該当するセクションを参照してください。

ステップ 4:テストと検証

実装後、完了を報告する前に、以下のチェックを実行してください。

  1. ビルドチェック:バックエンドとフロントエンドの両方がエラーなくコンパイルできることを確認します。
    # Backend
    cd server && npm run build
    # Frontend
    cd client && npm run build
    
  2. 起動とスモークテスト:サーバーを起動し、主要なエンドポイントが想定どおりのレスポンスを返すことを確認します。
    # Start server, then test
    curl http://localhost:3000/health
    curl http://localhost:3000/api/<resource>
    
  3. 連携チェック:フロントエンドがバックエンドに接続できることを確認します(CORS、API ベース URL、認証フロー)。
  4. リアルタイムチェック(該当する場合):ブラウザーのタブを 2 つ開き、変更が同期されることを確認します。

チェックに失敗した場合は、先に問題を修正してください。

ステップ 5:引き継ぎの概要

ユーザーに簡潔な概要を提示してください。

  • 構築したもの:実装した機能とエンドポイントの一覧
  • 実行方法:バックエンドとフロントエンドを起動する正確なコマンド
  • 未対応項目/次のステップ:先送りした項目、既知の制限、推奨される改善
  • 主要ファイル:ユーザーが把握しておくべき最も重要なファイルの一覧

対象範囲

このスキルを使用するケース:

  • フルスタックアプリケーション(バックエンド + フロントエンド)を構築する
  • 新しいバックエンドサービスまたは API をスキャフォールディングする
  • サービスレイヤーとモジュール境界を設計する
  • データベースアクセス、キャッシュ、またはバックグラウンドジョブを実装する
  • エラー処理、ロギング、または構成管理を作成する
  • アーキテクチャ上の問題についてバックエンドコードをレビューする
  • 本番環境向けに堅牢化する
  • API クライアント、認証フロー、ファイルアップロード、またはリアルタイム機能を設定する

対象外:

  • フロントエンド/UI のみの課題(使用するフロントエンドフレームワークのドキュメントを参照してください)
  • バックエンドのコンテキストを伴わない、データベーススキーマ設計のみの課題

クイックスタート — 新しいバックエンドサービスのチェックリスト

  • 機能優先の構成でプロジェクトをひな形から作成する
  • 設定を一元化し、環境変数を起動時に検証する(フェイルファスト)
  • 型付きのエラー階層を定義する(汎用の Error は使用しない)
  • グローバルエラーハンドラーミドルウェア
  • リクエスト ID の伝播に対応した構造化 JSON ロギング
  • データベース:マイグレーションをセットアップし、コネクションプーリングを設定する
  • すべてのエンドポイントで入力を検証する(Zod / Pydantic / Go validator)
  • 認証ミドルウェアを導入する
  • ヘルスチェックエンドポイント(/health、/ready)
  • グレースフルシャットダウンを処理する(SIGTERM)
  • CORSを設定する(明示的なオリジンを指定し、* は使わない)
  • セキュリティヘッダー(helmet または同等のもの)
  • .env.example をコミットする(実際のシークレットは含めない)

クイックスタート — フロントエンドとバックエンドの連携チェックリスト

  • API クライアントを設定する(型付き fetch ラッパー、React Query、tRPC、または OpenAPI で生成したもの)
  • ベース URL を環境変数から取得する(ハードコードしない)
  • リクエストに認証トークンを自動的に付与する(インターセプター / ミドルウェア)
  • エラー処理 — API エラーをユーザー向けのメッセージに変換する
  • 読み込み状態を処理する(スケルトン / スピナーを表示し、空白画面にしない)
  • 境界を越えて型安全性を確保する(共有型、OpenAPI、または tRPC)
  • 明示的なオリジンを指定してCORSを設定する(本番環境では * を使わない)
  • リフレッシュトークンのフローを実装する(httpOnly Cookie + 401 時の透過的な再試行)

クイックナビゲーション

必要な作業移動先
プロジェクトのフォルダーを整理する1. プロジェクト構造
設定とシークレットを管理する2. 設定
エラーを適切に処理する3. エラー処理
データベースコードを書く4. データベースアクセスパターン
フロントエンドから API クライアントを設定する5. API クライアントパターン
認証ミドルウェアを追加する6. 認証とミドルウェア
ロギングを設定する7. ロギングと可観測性
バックグラウンドジョブを追加する8. バックグラウンドジョブ
キャッシュを実装する9. キャッシュ
ファイルをアップロードする(署名付き URL、マルチパート)10. ファイルアップロードパターン
リアルタイム機能を追加する(SSE、WebSocket)11. リアルタイムパターン
フロントエンド UI で API エラーを処理する12. 境界を越えたエラー処理
本番環境向けに堅牢化する13. 本番環境向けの堅牢化
API エンドポイントを設計するAPI 設計
データベーススキーマを設計するデータベーススキーマ
認証フロー(JWT、更新、Next.js SSR、RBAC)references/auth-flow.md
CORS、環境変数、環境管理references/environment-management.md

コア原則(7 つの鉄則)

1. ✅ Organize by FEATURE, not by technical layer
2. ✅ Controllers never contain business logic
3. ✅ Services never import HTTP request/response types
4. ✅ All config from env vars, validated at startup, fail fast
5. ✅ Every error is typed, logged, and returns consistent format
6. ✅ All input validated at the boundary — trust nothing from client
7. ✅ Structured JSON logging with request ID — not console.log

1. プロジェクト構造とレイヤー分割(最重要)

機能優先の構成
✅ Feature-first                    ❌ Layer-first
src/                                src/
  orders/                             controllers/
    order.controller.ts                 order.controller.ts
    order.service.ts                    user.controller.ts
    order.repository.ts               services/
    order.dto.ts                        order.service.ts
    order.test.ts                       user.service.ts
  users/                              repositories/
    user.controller.ts                  ...
    user.service.ts
  shared/
    database/
    middleware/
3 層アーキテクチャ
Controller (HTTP) → Service (Business Logic) → Repository (Data Access)
レイヤー責務❌ 禁止事項
コントローラーリクエストを解析し、検証して、サービスを呼び出し、レスポンスを整形するビジネスロジック、DB クエリ
サービスビジネスルール、オーケストレーション、トランザクション管理HTTP 型(req/res)、DB への直接アクセス
リポジトリーデータベースクエリ、外部 API 呼び出しビジネスロジック、HTTP 型
依存性注入(全言語)

TypeScript:

class OrderService {
  constructor(
    private readonly orderRepo: OrderRepository,    // ✅ injected interface
    private readonly emailService: EmailService,
  ) {}
}

Python:

class OrderService:
    def __init__(self, order_repo: OrderRepository, email_service: EmailService):
        self.order_repo = order_repo                 # ✅ injected
        self.email_service = email_service

Go:

type OrderService struct {
    orderRepo    OrderRepository                      // ✅ interface
    emailService EmailService
}

func NewOrderService(repo OrderRepository, email EmailService) *OrderService {
    return &OrderService{orderRepo: repo, emailService: email}
}

2. 設定と環境(最重要)

一元化、型付け、フェイルファスト

TypeScript:

const config = {
  port: parseInt(process.env.PORT || '3000', 10),
  database: { url: requiredEnv('DATABASE_URL'), poolSize: intEnv('DB_POOL_SIZE', 10) },
  auth: { jwtSecret: requiredEnv('JWT_SECRET'), expiresIn: process.env.JWT_EXPIRES_IN || '1h' },
} as const;

function requiredEnv(name: string): string {
  const value = process.env[name];
  if (!value) throw new Error(`Missing required env var: ${name}`);  // fail fast
  return value;
}

Python:

from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    database_url: str                        # required — app won't start without it
    jwt_secret: str                          # required
    port: int = 3000                         # optional with default
    db_pool_size: int = 10
    class Config:
        env_file = ".env"

settings = Settings()                        # fails fast if DATABASE_URL missing
ルール
✅ All config via environment variables (Twelve-Factor)
✅ Validate required vars at startup — fail fast
✅ Type-cast at config layer, not at usage sites
✅ Commit .env.example with dummy values

❌ Never hardcode secrets, URLs, or credentials
❌ Never commit .env files
❌ Never scatter process.env / os.environ throughout code

3. エラー処理とレジリエンス(高)

型付きのエラー階層
// Base (TypeScript)
class AppError extends Error {
  constructor(
    message: string,
    public readonly code: string,
    public readonly statusCode: number,
    public readonly isOperational: boolean = true,
  ) { super(message); }
}
class NotFoundError extends AppError {
  constructor(resource: string, id: string) {
    super(`${resource} not found: ${id}`, 'NOT_FOUND', 404);
  }
}
class ValidationError extends AppError {
  constructor(public readonly errors: FieldError[]) {
    super('Validation failed', 'VALIDATION_ERROR', 422);
  }
}
# Base (Python)
class AppError(Exception):
    def __init__(self, message: str, code: str, status_code: int):
        self.message, self.code, self.status_code = message, code, status_code

class NotFoundError(AppError):
    def __init__(self, resource: str, id: str):
        super().__init__(f"{resource} not found: {id}", "NOT_FOUND", 404)
グローバルエラーハンドラー
// TypeScript (Express)
app.use((err, req, res, next) => {
  if (err instanceof AppError && err.isOperational) {
    return res.status(err.statusCode).json({
      title: err.code, status: err.statusCode,
      detail: err.message, request_id: req.id,
    });
  }
  logger.error('Unexpected error', { error: err.message, stack: err.stack, request_id: req.id });
  res.status(500).json({ title: 'Internal Error', status: 500, request_id: req.id });
});
ルール
✅ Typed, domain-specific error classes
✅ Global error handler catches everything
✅ Operational errors → structured response
✅ Programming errors → log + generic 500
✅ Retry transient failures with exponential backoff

❌ Never catch and ignore errors silently
❌ Never return stack traces to client
❌ Never throw generic Error('something')

4. データベースアクセスのパターン(高)

マイグレーションを必ず行う
# TypeScript (Prisma)           # Python (Alembic)              # Go (golang-migrate)
npx prisma migrate dev          alembic revision --autogenerate  migrate -source file://migrations
npx prisma migrate deploy       alembic upgrade head             migrate -database $DB up
✅ Schema changes via migrations, never manual SQL
✅ Migrations must be reversible
✅ Review migration SQL before production
❌ Never modify production schema manually
N+1 の防止
// ❌ N+1: 1 query + N queries
const orders = await db.order.findMany();
for (const o of orders) { o.items = await db.item.findMany({ where: { orderId: o.id } }); }

// ✅ Single JOIN query
const orders = await db.order.findMany({ include: { items: true } });
複数ステップの書き込みにはトランザクションを使用する
await db.$transaction(async (tx) => {
  const order = await tx.order.create({ data: orderData });
  await tx.inventory.decrement({ productId, quantity });
  await tx.payment.create({ orderId: order.id, amount });
});
コネクションプーリング

プールサイズ = (CPU cores × 2) + spindle_count(10~20から始めてください)。コネクションタイムアウトを必ず設定してください。サーバーレス環境では PgBouncer を使用してください。


5. API クライアントのパターン(中)

フロントエンドとバックエンドをつなぐ「接着層」です。チームやスタックに適したアプローチを選んでください。

オプション A:型付き Fetch ラッパー(シンプル、依存関係なし)
// lib/api-client.ts
const BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001';

class ApiError extends Error {
  constructor(public status: number, public body: any) {
    super(body?.detail || body?.message || `API error ${status}`);
  }
}

async function api<T>(path: string, options: RequestInit = {}): Promise<T> {
  const token = getAuthToken();  // from cookie / memory / context

  const res = await fetch(`${BASE_URL}${path}`, {
    ...options,
    headers: {
      'Content-Type': 'application/json',
      ...(token ? { Authorization: `Bearer ${token}` } : {}),
      ...options.headers,
    },
  });

  if (!res.ok) {
    const body = await res.json().catch(() => null);
    throw new ApiError(res.status, body);
  }

  if (res.status === 204) return undefined as T;
  return res.json();
}

export const apiClient = {
  get: <T>(path: string) => api<T>(path),
  post: <T>(path: string, data: unknown) => api<T>(path, { method: 'POST', body: JSON.stringify(data) }),
  put: <T>(path: string, data: unknown) => api<T>(path, { method: 'PUT', body: JSON.stringify(data) }),
  patch: <T>(path: string, data: unknown) => api<T>(path, { method: 'PATCH', body: JSON.stringify(data) }),
  delete: <T>(path: string) => api<T>(path, { method: 'DELETE' }),
};
オプション B:React Query + 型付きクライアント(React におすすめ)
// hooks/use-orders.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { apiClient } from '@/lib/api-client';

interface Order { id: string; total: number; status: string; }
interface CreateOrderInput { items: { productId: string; quantity: number }[] }

export function useOrders() {
  return useQuery({
    queryKey: ['orders'],
    queryFn: () => apiClient.get<{ data: Order[] }>('/api/orders'),
    staleTime: 1000 * 60,  // 1 min
  });
}

export function useCreateOrder() {
  const queryClient = useQueryClient();
  return useMutation({
    mutationFn: (data: CreateOrderInput) =>
      apiClient.post<{ data: Order }>('/api/orders', data),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['orders'] });
    },
  });
}

// Usage in component:
function OrdersPage() {
  const { data, isLoading, error } = useOrders();
  const createOrder = useCreateOrder();
  if (isLoading) return <Skeleton />;
  if (error) return <ErrorBanner error={error} />;
  // ...
}
オプション C:tRPC(同じチームが両側を担当する場合)
// server: trpc/router.ts
export const appRouter = router({
  orders: router({
    list: publicProcedure.query(async () => {
      return db.order.findMany({ include: { items: true } });
    }),
    create: protectedProcedure
      .input(z.object({ items: z.array(orderItemSchema) }))
      .mutation(async ({ input, ctx }) => {
        return orderService.create(ctx.user.id, input);
      }),
  }),
});
export type AppRouter = typeof appRouter;

// client: automatic type safety, no code generation
const { data } = trpc.orders.list.useQuery();
const createOrder = trpc.orders.create.useMutation();
オプション D:OpenAPI 生成クライアント(公開 API/複数の利用者向け API)
npx openapi-typescript-codegen \
  --input http://localhost:3001/api/openapi.json \
  --output src/generated/api \
  --client axios
判断:どの API クライアントを選ぶか
アプローチ適したケース型安全性労力
型付き Fetch ラッパーシンプルなアプリ、小規模チーム手動で型を定義低
React Query + fetchReact アプリ、サーバー状態手動で型を定義中
tRPC同じチーム、両側で TypeScript を使用自動低
OpenAPI 生成公開 API、複数の利用者向け自動中
GraphQL codegenGraphQL API自動中

6. 認証とミドルウェア(高)

完全なリファレンス: references/auth-flow.md — JWT ベアラーフロー、自動トークン更新、Next.js のサーバーサイド認証、RBAC パターン、バックエンドのミドルウェア順序。

標準的なミドルウェアの順序
Request → 1.RequestID → 2.Logging → 3.CORS → 4.RateLimit → 5.BodyParse
       → 6.Auth → 7.Authz → 8.Validation → 9.Handler → 10.ErrorHandler → Response
JWT のルール
✅ Short expiry access token (15min) + refresh token (server-stored)
✅ Minimal claims: userId, roles (not entire user object)
✅ Rotate signing keys periodically

❌ Never store tokens in localStorage (XSS risk)
❌ Never pass tokens in URL query params
RBAC パターン
function authorize(...roles: Role[]) {
  return (req, res, next) => {
    if (!req.user) throw new UnauthorizedError();
    if (!roles.some(r => req.user.roles.includes(r))) throw new ForbiddenError();
    next();
  };
}
router.delete('/users/:id', authenticate, authorize('admin'), deleteUser);
認証トークンの自動更新
// lib/api-client.ts — transparent refresh on 401
async function apiWithRefresh<T>(path: string, options: RequestInit = {}): Promise<T> {
  try {
    return await api<T>(path, options);
  } catch (err) {
    if (err instanceof ApiError && err.status === 401) {
      const refreshed = await api<{ accessToken: string }>('/api/auth/refresh', {
        method: 'POST',
        credentials: 'include',  // send httpOnly cookie
      });
      setAuthToken(refreshed.accessToken);
      return api<T>(path, options);  // retry
    }
    throw err;
  }
}

7. ロギングと可観測性(中~高)

構造化 JSON ロギング
// ✅ Structured — parseable, filterable, alertable
logger.info('Order created', {
  orderId: order.id, userId: user.id, total: order.total,
  items: order.items.length, duration_ms: Date.now() - startTime,
});
// Output: {"level":"info","msg":"Order created","orderId":"ord_123",...}

// ❌ Unstructured — useless at scale
console.log(`Order created for user ${user.id} with total ${order.total}`);
ログレベル
レベル適用場面本番環境?
error直ちに対応が必要✅ 常時
warn予期しないが処理済み✅ 常時
info通常の操作、監査証跡✅ 常時
debug開発時のトラブルシューティング❌ 開発時のみ
ルール
✅ Request ID in every log entry (propagated via middleware)
✅ Log at layer boundaries (request in, response out, external call)
❌ Never log passwords, tokens, PII, or secrets
❌ Never use console.log in production code

8. バックグラウンドジョブと非同期処理(中)

ルール
✅ All jobs must be IDEMPOTENT (same job running twice = same result)
✅ Failed jobs → retry (max 3) → dead letter queue → alert
✅ Workers run as SEPARATE processes (not threads in API server)

❌ Never put long-running tasks in request handlers
❌ Never assume job runs exactly once
冪等なジョブのパターン
async function processPayment(data: { orderId: string }) {
  const order = await orderRepo.findById(data.orderId);
  if (order.paymentStatus === 'completed') return;  // already processed
  await paymentGateway.charge(order);
  await orderRepo.updatePaymentStatus(order.id, 'completed');
}

9. キャッシュのパターン(中)

キャッシュアサイド(遅延読み込み)
async function getUser(id: string): Promise<User> {
  const cached = await redis.get(`user:${id}`);
  if (cached) return JSON.parse(cached);

  const user = await userRepo.findById(id);
  if (!user) throw new NotFoundError('User', id);

  await redis.set(`user:${id}`, JSON.stringify(user), 'EX', 900);  // 15min TTL
  return user;
}
ルール
✅ ALWAYS set TTL — never cache without expiry
✅ Invalidate on write (delete cache key after update)
✅ Use cache for reads, never for authoritative state

❌ Never cache without TTL (stale data is worse than slow data)
データ種別推奨 TTL
ユーザープロフィール5~15 分
製品カタログ1~5 分
設定/機能フラグ30~60 秒
セッションセッション期間に合わせる

10. ファイルアップロードのパターン(中)

オプション A:署名付き URL(大きなファイルにおすすめ)
Client → GET /api/uploads/presign?filename=photo.jpg&type=image/jpeg
Server → { uploadUrl: "https://s3.../presigned", fileKey: "uploads/abc123.jpg" }
Client → PUT uploadUrl (direct to S3, bypasses your server)
Client → POST /api/photos { fileKey: "uploads/abc123.jpg" }  (save reference)

バックエンド:

app.get('/api/uploads/presign', authenticate, async (req, res) => {
  const { filename, type } = req.query;
  const key = `uploads/${crypto.randomUUID()}-${filename}`;
  const url = await s3.getSignedUrl('putObject', {
    Bucket: process.env.S3_BUCKET, Key: key,
    ContentType: type, Expires: 300,  // 5 min
  });
  res.json({ uploadUrl: url, fileKey: key });
});

フロントエンド:

async function uploadFile(file: File) {
  const { uploadUrl, fileKey } = await apiClient.get<PresignResponse>(
    `/api/uploads/presign?filename=${file.name}&type=${file.type}`
  );
  await fetch(uploadUrl, { method: 'PUT', body: file, headers: { 'Content-Type': file.type } });
  return apiClient.post('/api/photos', { fileKey });
}
オプション B:マルチパート(小さなファイル、10MB 未満)
// Frontend
const formData = new FormData();
formData.append('file', file);
formData.append('description', 'Profile photo');
const res = await fetch('/api/upload', { method: 'POST', body: formData });
// Note: do NOT set Content-Type header — browser sets boundary automatically
判断
方法ファイルサイズサーバー負荷複雑さ
署名付き URL任意(5MB 超を推奨)なし(ストレージに直接転送)中
マルチパート10MB 未満高(サーバー経由でストリーミング)低
チャンク/再開可能100MB 超中高

11. リアルタイム通信のパターン(中)

オプション A:Server-Sent Events(SSE)— サーバーからクライアントへの一方向

適した用途:通知、ライブフィード、AI の応答のストリーミング。

バックエンド(Express):

app.get('/api/events', authenticate, (req, res) => {
  res.writeHead(200, {
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache',
    Connection: 'keep-alive',
  });
  const send = (event: string, data: unknown) => {
    res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
  };
  const unsubscribe = eventBus.subscribe(req.user.id, (event) => {
    send(event.type, event.payload);
  });
  req.on('close', () => unsubscribe());
});

フロントエンド:

function useServerEvents(userId: string) {
  useEffect(() => {
    const source = new EventSource(`/api/events?userId=${userId}`);
    source.addEventListener('notification', (e) => {
      showToast(JSON.parse(e.data).message);
    });
    source.onerror = () => { source.close(); setTimeout(() => /* reconnect */, 3000); };
    return () => source.close();
  }, [userId]);
}
オプション B:WebSocket — 双方向

適した用途:チャット、共同編集、ゲーム。

バックエンド(ws ライブラリ):

import { WebSocketServer } from 'ws';
const wss = new WebSocketServer({ server: httpServer, path: '/ws' });
wss.on('connection', (ws, req) => {
  const userId = authenticateWs(req);
  if (!userId) { ws.close(4001, 'Unauthorized'); return; }
  ws.on('message', (raw) => handleMessage(userId, JSON.parse(raw.toString())));
  ws.on('close', () => cleanupUser(userId));
  const interval = setInterval(() => ws.ping(), 30000);
  ws.on('pong', () => { /* alive */ });
  ws.on('close', () => clearInterval(interval));
});

フロントエンド:

function useWebSocket(url: string) {
  const [ws, setWs] = useState<WebSocket | null>(null);
  useEffect(() => {
    const socket = new WebSocket(url);
    socket.onopen = () => setWs(socket);
    socket.onclose = () => setTimeout(() => /* reconnect */, 3000);
    return () => socket.close();
  }, [url]);
  const send = useCallback((data: unknown) => ws?.send(JSON.stringify(data)), [ws]);
  return { ws, send };
}
オプション C:ポーリング(最もシンプル、インフラ不要)
function useOrderStatus(orderId: string) {
  return useQuery({
    queryKey: ['order-status', orderId],
    queryFn: () => apiClient.get<Order>(`/api/orders/${orderId}`),
    refetchInterval: (query) => {
      if (query.state.data?.status === 'completed') return false;
      return 5000;
    },
  });
}
判断
方法方向複雑さ適した用途
ポーリングクライアント → サーバー低シンプルなステータス確認、クライアント数 10 未満
SSEサーバー → クライアント中通知、フィード、AI のストリーミング
WebSocket双方向高チャット、共同作業、ゲーム

12. 境界をまたぐエラー処理(中)

API エラーからユーザー向けメッセージへの変換
// lib/error-handler.ts
export function getErrorMessage(error: unknown): string {
  if (error instanceof ApiError) {
    switch (error.status) {
      case 401: return 'Please log in to continue.';
      case 403: return 'You don\'t have permission to do this.';
      case 404: return 'The item you\'re looking for doesn\'t exist.';
      case 409: return 'This conflicts with an existing item.';
      case 422:
        const fields = error.body?.errors;
        if (fields?.length) return fields.map((f: any) => f.message).join('. ');
        return 'Please check your input.';
      case 429: return 'Too many requests. Please wait a moment.';
      default: return 'Something went wrong. Please try again.';
    }
  }
  if (error instanceof TypeError && error.message === 'Failed to fetch') {
    return 'Cannot connect to server. Check your internet connection.';
  }
  return 'An unexpected error occurred.';
}
React Query グローバルエラーハンドラー
const queryClient = new QueryClient({
  defaultOptions: {
    mutations: { onError: (error) => toast.error(getErrorMessage(error)) },
    queries: {
      retry: (failureCount, error) => {
        if (error instanceof ApiError && error.status < 500) return false;
        return failureCount < 3;
      },
    },
  },
});
ルール
✅ Map every API error code to a human-readable message
✅ Show field-level validation errors next to form inputs
✅ Auto-retry on 5xx (max 3, with backoff), never on 4xx
✅ Redirect to login on 401 (after refresh attempt fails)
✅ Show "offline" banner when fetch fails with TypeError

❌ Never show raw API error messages to users ("NullPointerException")
❌ Never silently swallow errors (show toast or log)
❌ Never retry 4xx errors (client is wrong, retrying won't help)
統合の判断フローチャート
Same team owns frontend + backend?
│
├─ YES, both TypeScript
│   └─ tRPC (end-to-end type safety, zero codegen)
│
├─ YES, different languages
│   └─ OpenAPI spec → generated client (type safety via codegen)
│
├─ NO, public API
│   └─ REST + OpenAPI → generated SDKs for consumers
│
└─ Complex data needs, multiple frontends
    └─ GraphQL + codegen (flexible queries per client)

Real-time needed?
│
├─ Server → Client only (notifications, feeds, AI streaming)
│   └─ SSE (simplest, auto-reconnect, works through proxies)
│
├─ Bidirectional (chat, collaboration)
│   └─ WebSocket (need heartbeat + reconnection logic)
│
└─ Simple status polling (< 10 clients)
    └─ React Query refetchInterval (no infrastructure needed)

13. 本番環境の堅牢化(中)

ヘルスチェック
app.get('/health', (req, res) => res.json({ status: 'ok' }));           // liveness
app.get('/ready', async (req, res) => {                                   // readiness
  const checks = {
    database: await checkDb(), redis: await checkRedis(), 
  };
  const ok = Object.values(checks).every(c => c.status === 'ok');
  res.status(ok ? 200 : 503).json({ status: ok ? 'ok' : 'degraded', checks });
});
グレースフルシャットダウン
process.on('SIGTERM', async () => {
  logger.info('SIGTERM received');
  server.close();              // stop new connections
  await drainConnections();    // finish in-flight
  await closeDatabase();
  process.exit(0);
});
セキュリティチェックリスト
✅ CORS: explicit origins (never '*' in production)
✅ Security headers (helmet / equivalent)
✅ Rate limiting on public endpoints
✅ Input validation on ALL endpoints (trust nothing)
✅ HTTPS enforced
❌ Never expose internal errors to clients

アンチパターン

#❌ やってはいけないこと✅ 代わりに行うこと
1ルート/コントローラーにビジネスロジックを記述するサービスレイヤーに移す
2process.env をあちこちに散在させる型付き設定を一元管理する
3ロギングに console.log を使う構造化 JSON ロガーを使う
4汎用的な Error('oops') を使う型付きのエラー階層を使う
5コントローラーからデータベースを直接呼び出すリポジトリパターンを使う
6入力を検証しない境界で検証する(Zod/Pydantic)
7エラーを黙って捕捉するログに記録して再スローするか、エラーを返す
8ヘルスチェックエンドポイントを用意しない/health と /ready を用意する
9設定やシークレットをハードコードする環境変数を使う
10グレースフルシャットダウンを実装しないSIGTERM を適切に処理する
11フロントエンドに API URL をハードコードする環境変数(NEXT_PUBLIC_API_URL)を使う
12JWT を localStorage に保存するメモリと httpOnly の更新用 Cookie を使う
13生の API エラーをユーザーに表示する人が理解しやすいメッセージに変換する
144xx エラーを再試行する5xx(サーバー障害)のみ再試行する
15読み込み中の状態を省略するデータ取得中はスケルトン/スピナーを表示する
16大きなファイルを API サーバー経由でアップロードする署名付き URL を使い、S3 に直接アップロードする
17リアルタイムデータをポーリングするSSE または WebSocket を使う
18フロントエンドとバックエンドで型を重複させる共通の型、tRPC、または OpenAPI コード生成を使う

よくある問題

問題 1:「このビジネスルールはどこに配置すればよいですか?」

ルール: HTTP(リクエストの解析、ステータスコード、ヘッダー)に関わるものはコントローラーに配置します。ビジネス上の判断(価格設定、権限、ルール)に関わるものはサービスに配置します。データベースにアクセスするものはリポジトリに配置します。

問題 2:「サービスが大きくなりすぎています」

症状: 1 つのサービスファイルが 500 行を超え、メソッドが 20 個以上あります。

解決策: サブドメインごとに分割します。OrderService を OrderCreationService、OrderFulfillmentService、OrderQueryService に分割します。それぞれ 1 つのワークフローに特化させます。

問題 3:「データベースにアクセスするため、テストが遅いです」

解決策: ユニットテストではリポジトリレイヤーをモックします(高速)。統合テストではテストコンテナーまたはトランザクションのロールバックを使います(実際の DB を使いながらも高速です)。統合テストではサービスレイヤーを決してモックしないでください。


参考ドキュメント

このスキルには、専門的なトピックを詳しく解説するリファレンスが含まれています。詳細なガイダンスが必要な場合は、該当するリファレンスをお読みください。

必要な作業リファレンス
バックエンドのテストを書く(ユニット、統合、E2E、コントラクト、パフォーマンス)references/testing-strategy.md
デプロイ前にリリースを検証する(6 ゲートのチェックリスト)references/release-checklist.md
技術スタックを選ぶ(言語、フレームワーク、データベース、インフラ)references/technology-selection.md
Django / DRF で構築する(モデル、ビュー、シリアライザー、管理画面)references/django-best-practices.md
REST/GraphQL/gRPC エンドポイントを設計する(URL、ステータスコード、ページネーション)references/api-design.md
データベーススキーマ、インデックス、マイグレーション、マルチテナンシーを設計するreferences/db-schema.md
認証フロー(JWT ベアラー、トークンの更新、Next.js SSR、RBAC、ミドルウェアの順序)references/auth-flow.md
CORS 設定、環境ごとの環境変数、よくある CORS の問題references/environment-management.md

コメント