Agent Skills by ALSEL
Anthropic Claudeソフトウェア開発⭐ リポ 0品質スコア 50/100

fullstack-dev

フルスタックアプリのバックエンド設計とフロントエンド・バックエンド統合をガイドするスキルです。REST APIの構築、CRUDアプリやリアルタイムチャットアプリの開発、Express + React / Next.js API / Node.js・Python・Goバックエンドの実装、認証フロー・ファイルアップロード・SSE/WebSocket対応、本番環境向けの堅牢化など、フルスタック開発全般でトリガーされます。純粋なフロントエンドUIやCSSのスタイリング、DBスキーマのみの作業には使用しません。

description の原文を見る

| Full-stack backend architecture and frontend-backend integration guide. TRIGGER when: building a full-stack app, creating REST API with frontend, scaffolding backend service, building todo app, building CRUD app, building real-time app, building chat app, Express + React, Next.js API, Node.js backend, Python backend, Go backend, designing service layers, implementing error handling, managing config/auth, setting up API clients, implementing auth flows, handling file uploads, adding real-time features (SSE/WebSocket), hardening for production. DO NOT TRIGGER when: pure frontend UI work, pure CSS/styling, database schema only.

SKILL.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: アーキテクチャの決定

要件に基づいて、コーディングの前に次の決定を行って述べる:

決定項目オプションリファレンス
プロジェクト構造フィーチャーファースト (推奨) vs レイヤーファーストセクション 1
API クライアント アプローチ型付き fetch / React Query / tRPC / OpenAPI コード生成セクション 5
認証戦略JWT + リフレッシュ / セッション / サードパーティセクション 6
リアルタイム方式ポーリング / SSE / WebSocketセクション 11
エラー ハンドリング型付きエラー階層 + グローバル ハンドラーセクション 3

各選択について簡潔に説明する (1 決定につき 1 文)。

ステップ 2: チェックリスト付きでスキャフォルド

下のチェックリストを使用する。チェック済みアイテムすべてが実装されていることを確認する — スキップしない。

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

このドキュメントのパターンに従ってコードを書く。各部分を実装するときに特定のセクションを参照する。

ステップ 4: テストして検証する

実装後、完了を主張する前にこれらのチェックを実行する:

  1. ビルド チェック: バックエンドとフロントエンドの両方がエラーなしでコンパイルされることを確認する
    # バックエンド
    cd server && npm run build
    # フロントエンド
    cd client && npm run build
    
  2. 起動とスモーク テスト: サーバーを起動し、主要なエンドポイントが予想される応答を返すことを確認する
    # サーバーを起動してからテスト
    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 バリデータ)
  • 認証ミドルウェア が設置されている
  • ヘルスチェック エンドポイント (/health/ready)
  • グレースフル シャットダウン ハンドリング (SIGTERM)
  • CORS が設定されている (明示的なオリジン、* ではない)
  • セキュリティ ヘッダー (helmet または同等)
  • .env.example がコミットされている (実際のシークレットなし)

クイックスタート — フロントエンド-バックエンド統合 チェックリスト

  • API クライアント が設定されている (型付き fetch ラッパー、React Query、tRPC、または OpenAPI 生成)
  • ベース URL が環境変数から取得される (ハードコードされていない)
  • 認証トークン がリクエストに自動的に付与される (インターセプター / ミドルウェア)
  • エラー ハンドリング — API エラーがユーザー向けメッセージにマップされている
  • ローディング状態 がハンドルされている (スケルトン/スピナー、空白画面ではない)
  • 型安全性 が境界を越えている (共有型、OpenAPI、または tRPC)
  • CORS が明示的なオリジンで設定されている (本番環境では * ではない)
  • リフレッシュ トークン フロー実装 (httpOnly クッキー + 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. ✅ テクニカルレイヤーではなく、フィーチャーで整理する
2. ✅ コントローラーにはビジネス ロジックを含めない
3. ✅ サービスは HTTP リクエスト/レスポンス型をインポートしない
4. ✅ すべての設定は環境変数から、起動時に検証、早期失敗
5. ✅ すべてのエラーは型付き、ログされ、一貫した形式を返す
6. ✅ すべての入力はバウンダリで検証 — クライアントからのものを信頼しない
7. ✅ 構造化 JSON ロギング + リクエスト ID — console.log ではない

1. プロジェクト構造 & レイヤリング (CRITICAL)

フィーチャーファースト 構成

✅ フィーチャーファースト                    ❌ レイヤーファースト
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 層アーキテクチャ

コントローラー (HTTP) → サービス (ビジネス ロジック) → リポジトリ (データ アクセス)
レイヤー責任❌ しない
コントローラーリクエストを解析、検証、サービス呼び出し、レスポンス形式化ビジネス ロジック、DB クエリ
サービスビジネス ルール、オーケストレーション、トランザクション管理HTTP 型 (req/res)、直接 DB
リポジトリデータベース クエリ、外部 API 呼び出しビジネス ロジック、HTTP 型

依存関係の注入 (すべての言語)

TypeScript:

class OrderService {
  constructor(
    private readonly orderRepo: OrderRepository,    // ✅ 注入されたインターフェース
    private readonly emailService: EmailService,
  ) {}
}

Python:

class OrderService:
    def __init__(self, order_repo: OrderRepository, email_service: EmailService):
        self.order_repo = order_repo                 # ✅ 注入される
        self.email_service = email_service

Go:

type OrderService struct {
    orderRepo    OrderRepository                      // ✅ インターフェース
    emailService EmailService
}

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

2. 設定 & 環境 (CRITICAL)

集中化、型付き、早期失敗

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}`);  // 早期失敗
  return value;
}

Python:

from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    database_url: str                        # 必須 — これなしではアプリが起動しない
    jwt_secret: str                          # 必須
    port: int = 3000                         # デフォルト値付き (オプション)
    db_pool_size: int = 10
    class Config:
        env_file = ".env"

settings = Settings()                        # DATABASE_URL がない場合は失敗

ルール

✅ すべての設定は環境変数経由 (Twelve-Factor)
✅ 必須変数を起動時に検証 — 早期失敗
✅ 設定レイヤーで型キャスト、使用箇所ではしない
✅ .env.example をコミット (実シークレットなし)

❌ シークレット、URL、認証情報をハードコードしない
❌ .env ファイルをコミットしない
❌ process.env / os.environ をコード全体に分散させない

3. エラー ハンドリング & 回復力 (HIGH)

型付きエラー階層

// ベース (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);
  }
}
# ベース (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 });
});

ルール

✅ 型付き、ドメイン固有のエラー クラス
✅ グローバル エラー ハンドラーがすべてをキャッチ
✅ 運用エラー → 構造化レスポンス
✅ プログラミング エラー → ログ + ジェネリック 500
✅ 一時的な失敗を指数バックオフで再試行

❌ エラーをサイレントに無視してキャッチしない
❌ スタック トレースをクライアントに返さない
❌ ジェネリック Error('something') をスロー しない

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

マイグレーション は必ず

# 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
✅ スキーマ変更はマイグレーション経由、手動 SQL ではない
✅ マイグレーションは可逆的であること
✅ 本番環境の前にマイグレーション SQL をレビュー
❌ 本番環境スキーマを手動で変更しない

N+1 問題の防止

// ❌ N+1: 1 クエリ + N クエリ
const orders = await db.order.findMany();
for (const o of orders) { o.items = await db.item.findMany({ where: { orderId: o.id } }); }

// ✅ 単一 JOIN クエリ
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 コア × 2) + スピンドル数 (10-20 から始める)。接続タイムアウトを常に設定。サーバーレスには PgBouncer を使用。


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

フロントエンドとバックエンドを結ぶ「接着層」。チームと スタックに合ったアプローチを選択。

オプション 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();  // クッキー / メモリ / コンテキストから

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

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

// コンポーネントでの使用:
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: 自動型安全性、コード生成なし
const { data } = trpc.orders.list.useQuery();
const createOrder = trpc.orders.create.useMutation();

オプション D: OpenAPI 生成クライアント (パブリック / マルチ消費者 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 コード生成GraphQL API自動

6. 認証 & ミドルウェア (HIGH)

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

標準ミドルウェア順序

リクエスト → 1.RequestID → 2.Logging → 3.CORS → 4.RateLimit → 5.BodyParse
         → 6.Auth → 7.Authz → 8.Validation → 9.Handler → 10.ErrorHandler → レスポンス

JWT ルール

✅ 短い有効期限のアクセス トークン (15分) + リフレッシュ トークン (サーバー保存)
✅ 最小限のクレーム: userId、roles (ユーザー オブジェクト全体ではない)
✅ 署名キーを定期的にローテーション

❌ トークンを localStorage に保存しない (XSS リスク)
❌ トークンを URL クエリ パラメータで渡さない

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 — 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',  // httpOnly クッキーを送信
      });
      setAuthToken(refreshed.accessToken);
      return api<T>(path, options);  // 再試行
    }
    throw err;
  }
}

7. ロギング & オブザーバビリティ (MEDIUM-HIGH)

構造化 JSON ロギング

// ✅ 構造化 — パース可能、フィルター可能、アラート可能
logger.info('Order created', {
  orderId: order.id, userId: user.id, total: order.total,
  items: order.items.length, duration_ms: Date.now() - startTime,
});
// 出力: {"level":"info","msg":"Order created","orderId":"ord_123",...}

// ❌ 非構造化 — 大規模では無用
console.log(`Order created for user ${user.id} with total ${order.total}`);

ログ レベル

レベルいつ本番環境?
error即座の対応が必要✅ 常に
warn予期しないが処理済み✅ 常に
info通常の操作、監査証跡✅ 常に
debug開発トラブルシューティング❌ 開発のみ

ルール

✅ すべてのログ エントリにリクエスト ID (ミドルウェア経由で伝播)
✅ レイヤー境界でログ (リクエスト in、レスポンス out、外部呼び出し)
❌ パスワード、トークン、PII、またはシークレットをロギングしない
❌ 本番コードで console.log を使用しない

8. バックグラウンド ジョブ & 非同期 (MEDIUM)

ルール

✅ すべてのジョブは IDEMPOTENT (同じジョブが 2 回実行 = 同じ結果)
✅ 失敗したジョブ → 再試行 (最大 3) → デッド レター キュー → アラート
✅ ワーカーが SEPARATE プロセスとして実行 (API サーバーのスレッドではない)

❌ リクエスト ハンドラーに長時間実行タスクを置かない
❌ ジョブが正確に 1 回実行されると仮定しない

Idempotent ジョブ パターン

async function processPayment(data: { orderId: string }) {
  const order = await orderRepo.findById(data.orderId);
  if (order.paymentStatus === 'completed') return;  // すでに処理済み
  await paymentGateway.charge(order);
  await orderRepo.updatePaymentStatus(order.id, 'completed');
}

9. キャッシング パターン (MEDIUM)

キャッシュ アサイド (遅延ロード)

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);  // 15分 TTL
  return user;
}

ルール

✅ 常に TTL を設定 — TTL なしでキャッシュしない
✅ 書き込み時に無効化 (更新後にキャッシュ キーを削除)
✅ キャッシュを読み取り用に使用、権限のある状態には使用しない

❌ TTL なしでキャッシュしない (古いデータは遅いデータより悪い)
データ型推奨 TTL
ユーザー プロフィール5-15 分
製品 カタログ1-5 分
設定 / フィーチャー フラグ30-60 秒
セッションセッション期間に一致

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

オプション A: プリサイン URL (大きなファイル推奨)

クライアント → GET /api/uploads/presign?filename=photo.jpg&type=image/jpeg
サーバー → { uploadUrl: "https://s3.../presigned", fileKey: "uploads/abc123.jpg" }
クライアント → PUT uploadUrl (S3 に直接、サーバーをバイパス)
クライアント → POST /api/photos { fileKey: "uploads/abc123.jpg" }  (参照を保存)

バックエンド:

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

// フロントエンド
const formData = new FormData();
formData.append('file', file);
formData.append('description', 'Profile photo');
const res = await fetch('/api/upload', { method: 'POST', body: formData });
// 注: Content-Type ヘッダーを設定しない — ブラウザが境界を設定

決定

メソッドファイル サイズサーバー負荷複雑さ
プリサイン URLすべて (> 5MB 推奨)なし (ストレージに直接)
マルチパート< 10MB高 (サーバー経由ストリーム)
チャンク / 再開可能> 100MB

11. リアルタイム パターン (MEDIUM)

オプション 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(() => /* 再接続 */, 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(() => /* 再接続 */, 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. クロス境界 エラー ハンドリング (MEDIUM)

API エラー → ユーザー向けメッセージ

// lib/error-handler.ts
export function getErrorMessage(error: unknown): string {
  if (error instanceof ApiError) {
    switch (error.status) {
      case 401: return 'ログインして続行してください。';
      case 403: return 'これを実行する権限がありません。';
      case 404: return 'お探しのアイテムは存在しません。';
      case 409: return 'これは既存のアイテムと競合しています。';
      case 422:
        const fields = error.body?.errors;
        if (fields?.length) return fields.map((f: any) => f.message).join('. ');
        return '入力を確認してください。';
      case 429: return 'リクエストが多すぎます。少しお待ちください。';
      default: return 'エラーが発生しました。もう一度試してください。';
    }
  }
  if (error instanceof TypeError && error.message === 'Failed to fetch') {
    return 'サーバーに接続できません。インターネット接続を確認してください。';
  }
  return '予期しないエラーが発生しました。';
}

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

ルール

✅ すべての API エラー コードをユーザー向けメッセージにマップ
✅ フィールド レベルの検証エラーはフォーム入力の横に表示
✅ 5xx で自動再試行 (最大 3、バックオフ付き)、4xx では再試行しない
✅ 401 でログイン にリダイレクト (リフレッシュ試行後も失敗)
✅ fetch が TypeError で失敗するときは「オフライン」バナーを表示

❌ 生の API エラー メッセージをユーザーに表示しない ("NullPointerException")
❌ エラーをサイレントに無視する (トースト またはログを表示)
❌ 4xx エラーを再試行する (クライアントが間違っている、再試行は役に立たない)

統合 決定木

同じチームがフロントエンド + バックエンドを所有?
│
├─ はい、両側 TypeScript
│   └─ tRPC (エンドツーエンド型安全性、ゼロコード生成)
│
├─ はい、異なる言語
│   └─ OpenAPI スペック → 生成クライアント (コード生成経由の型安全性)
│
├─ いいえ、パブリック API
│   └─ REST + OpenAPI → 消費者用生成 SDK
│
└─ 複雑なデータ、複数フロントエンド
    └─ GraphQL + コード生成 (クライアント ごとの柔軟なクエリ)

リアルタイムが必要?
│
├─ サーバー → クライアントのみ (通知、フィード、AI ストリーミング)
│   └─ SSE (最もシンプル、自動再接続、プロキシ経由で動作)
│
├─ 双方向 (チャット、協調)
│   └─ WebSocket (ハートビート + 再接続ロジックが必要)
│
└─ シンプルなステータス ポーリング (< 10 クライアント)
    └─ React Query refetchInterval (インフラ不要)

13. 本番用セキュア化 (MEDIUM)

ヘルスチェック

app.get('/health', (req, res) => res.json({ status: 'ok' }));           // 生存確認
app.get('/ready', async (req, res) => {                                   // 準備確認
  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 受信');
  server.close();              // 新しい接続を停止
  await drainConnections();    // 処理中を完了
  await closeDatabase();
  process.exit(0);
});

セキュリティ チェックリスト

✅ CORS: 明示的なオリジン (本番環境では '*' ではない)
✅ セキュリティ ヘッダー (helmet または同等)
✅ パブリック エンドポイントのレート リミット
✅ すべてのエンドポイントで入力検証 (クライアントからのものを信頼しない)
✅ HTTPS 強制
❌ クライアントに内部エラーを公開しない

アンチパターン

#❌ しない✅ 代わりにこうする
1ルート/コントローラーのビジネス ロジックサービス レイヤーに移動
2コード全体に散在する process.env集中化された型付き設定
3ロギングに console.log構造化 JSON ロガー
4ジェネリック Error('oops')型付きエラー階層
5コントローラーからの直接 DB 呼び出しリポジトリ パターン
6入力検証なしバウンダリで検証 (Zod/Pydantic)
7エラーを無視してキャッチログして再スロー またはエラー返す
8ヘルスチェック エンドポイントなし/health + /ready
9ハードコードされた設定/シークレット環境変数
10グレースフル シャットダウンなしSIGTERM を正しく処理
11フロントエンドで API URL をハードコード環境変数 (NEXT_PUBLIC_API_URL)
12JWT を localStorage に保存メモリ + httpOnly リフレッシュ クッキー
13生の API エラーをユーザーに表示ユーザー向けメッセージにマップ
144xx エラーを再試行5xx のみ再試行 (サーバー障害)
15ローディング状態をスキップフェッチ中にスケルトン/スピナー
16大きいファイルを API サーバー経由でアップロードプリサイン URL → S3 に直接
17リアルタイム データをポーリングSSE または WebSocket
18フロントエンド + バックエンド で型を複製共有型、tRPC、または OpenAPI コード生成

よくある問題

問題 1: 「このビジネス ルールはどこに行く?」

ルール: HTTP に関係する (リクエスト解析、ステータス コード、ヘッダー) → コントローラー。ビジネス決定に関係 (価格設定、権限、ルール) → サービス。データベースに接触 → リポジトリ。

問題 2: 「サービスが大きすぎる」

症状: 1 つのサービス ファイル > 500 行、20+ メソッド。

修正: サブドメインで分割。OrderServiceOrderCreationService + OrderFulfillmentService + OrderQueryService。各々が 1 つのワークフローに焦点。

問題 3: 「テストが遅い、データベースにアクセスしている」

修正: ユニット テストはリポジトリ レイヤーをモック (高速)。統合テストはテスト コンテナ またはトランザクション ロールバックを使用 (実 DB、まだ高速)。統合テストではサービス レイヤーをモックしない。


リファレンス ドキュメント

このスキルには特殊なトピックの詳細リファレンスが含まれます。詳細なガイダンスが必要な場合は関連リファレンスを読む。

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

ライセンス: MIT(寛容ライセンスのため全文を引用しています) · 原本リポジトリ

詳細情報

作者
minimax-ai
リポジトリ
minimax-ai/skills
ライセンス
MIT
最終更新
不明

Source: https://github.com/minimax-ai/skills / ライセンス: MIT

関連スキル

汎用ソフトウェア開発⭐ リポ 39,967

doubt-driven-development

重要な判断はすべて、本番環境への展開前に新しい視点から対抗的レビューを実施します。速度より正確性が重要な場合、不慣れなコードを扱う場合、本番環境・セキュリティに関わるロジック・取り消し不可の操作など影響度が高い場合、または後でバグを修正するよりも今検証する方が効率的な場合に活用してください。

by addyosmani
汎用ソフトウェア開発⭐ リポ 1,175

apprun-skills

TypeScriptを使用したAppRunアプリケーションのMVU設計に関する総合的なガイダンスが得られます。コンポーネントパターン、イベントハンドリング、状態管理(非同期ジェネレータを含む)、パラメータと保護機能を備えたルーティング・ナビゲーション、vistestを使用したテストに対応しています。AppRunコンポーネントの設計・レビュー、ルートの配線、状態フローの管理、AppRunテストの作成時に活用してください。

by yysun
OpenAIソフトウェア開発⭐ リポ 797

desloppify

コードベースのヘルスチェックと技術負債の追跡ツールです。コード品質、技術負債、デッドコード、大規模ファイル、ゴッドクラス、重複関数、コードスメル、命名規則の問題、インポートサイクル、結合度の問題についてユーザーが質問した場合に使用してください。また、ヘルススコアの確認、次の改善項目の提案、クリーンアップ計画の作成をリクエストされた際にも対応します。29言語に対応しています。

by Git-on-my-level
汎用ソフトウェア開発⭐ リポ 39,967

debugging-and-error-recovery

テストが失敗したり、ビルドが壊れたり、動作が期待と異なったり、予期しないエラーが発生したりした場合に、体系的な根本原因デバッグをガイドします。推測ではなく、根本原因を見つけて修正するための体系的なアプローチが必要な場合に使用してください。

by addyosmani
汎用ソフトウェア開発⭐ リポ 39,967

test-driven-development

テスト駆動開発により実装を進めます。ロジックの実装、バグの修正、動作の変更など、あらゆる場面で活用できます。コードが正常に動作することを証明する必要がある場合、バグ報告を受けた場合、既存機能を修正する予定がある場合に使用してください。

by addyosmani
汎用ソフトウェア開発⭐ リポ 39,967

incremental-implementation

変更を段階的に実施します。複数のファイルに影響する機能や変更を実装する場合に使用してください。大量のコードを一度に書こうとしている場合や、タスクが一度では完結できないほど大きい場合に活用します。

by addyosmani
本サイトは GitHub 上で公開されているオープンソースの SKILL.md ファイルをクロール・インデックス化したものです。 各スキルの著作権は原作者に帰属します。掲載に問題がある場合は info@alsel.co.jp または /takedown フォームよりご連絡ください。
原作者: minimax-ai · minimax-ai/skills · ライセンス: MIT