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: 要件を集める
スキャフォルディングの前に、ユーザーに以下を明確にするよう求める (またはコンテキストから推測する):
- スタック: バックエンドとフロントエンドの言語/フレームワーク (例: Express + React、Django + Vue、Go + HTMX)
- サービス タイプ: API のみ、フルスタック モノリス、またはマイクロサービス?
- データベース: SQL (PostgreSQL、SQLite、MySQL) または NoSQL (MongoDB、Redis)?
- 統合方式: REST、GraphQL、tRPC、または gRPC?
- リアルタイム: 必要? はい の場合 — SSE、WebSocket、またはポーリング?
- 認証: 必要? はい の場合 — 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: テストして検証する
実装後、完了を主張する前にこれらのチェックを実行する:
- ビルド チェック: バックエンドとフロントエンドの両方がエラーなしでコンパイルされることを確認する
# バックエンド cd server && npm run build # フロントエンド cd client && npm run build - 起動とスモーク テスト: サーバーを起動し、主要なエンドポイントが予想される応答を返すことを確認する
# サーバーを起動してからテスト curl http://localhost:3000/health curl http://localhost:3000/api/<resource> - 統合チェック: フロントエンドがバックエンドに接続できることを確認する (CORS、API ベース URL、認証フロー)
- リアルタイム チェック (該当する場合): 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 + fetch | React アプリ、サーバー状態 | 手動 | 中 |
| 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) |
| 12 | JWT を localStorage に保存 | メモリ + httpOnly リフレッシュ クッキー |
| 13 | 生の API エラーをユーザーに表示 | ユーザー向けメッセージにマップ |
| 14 | 4xx エラーを再試行 | 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 で構築 (モデル、ビュー、シリアライザー、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
関連スキル
doubt-driven-development
重要な判断はすべて、本番環境への展開前に新しい視点から対抗的レビューを実施します。速度より正確性が重要な場合、不慣れなコードを扱う場合、本番環境・セキュリティに関わるロジック・取り消し不可の操作など影響度が高い場合、または後でバグを修正するよりも今検証する方が効率的な場合に活用してください。
apprun-skills
TypeScriptを使用したAppRunアプリケーションのMVU設計に関する総合的なガイダンスが得られます。コンポーネントパターン、イベントハンドリング、状態管理(非同期ジェネレータを含む)、パラメータと保護機能を備えたルーティング・ナビゲーション、vistestを使用したテストに対応しています。AppRunコンポーネントの設計・レビュー、ルートの配線、状態フローの管理、AppRunテストの作成時に活用してください。
desloppify
コードベースのヘルスチェックと技術負債の追跡ツールです。コード品質、技術負債、デッドコード、大規模ファイル、ゴッドクラス、重複関数、コードスメル、命名規則の問題、インポートサイクル、結合度の問題についてユーザーが質問した場合に使用してください。また、ヘルススコアの確認、次の改善項目の提案、クリーンアップ計画の作成をリクエストされた際にも対応します。29言語に対応しています。
debugging-and-error-recovery
テストが失敗したり、ビルドが壊れたり、動作が期待と異なったり、予期しないエラーが発生したりした場合に、体系的な根本原因デバッグをガイドします。推測ではなく、根本原因を見つけて修正するための体系的なアプローチが必要な場合に使用してください。
test-driven-development
テスト駆動開発により実装を進めます。ロジックの実装、バグの修正、動作の変更など、あらゆる場面で活用できます。コードが正常に動作することを証明する必要がある場合、バグ報告を受けた場合、既存機能を修正する予定がある場合に使用してください。
incremental-implementation
変更を段階的に実施します。複数のファイルに影響する機能や変更を実装する場合に使用してください。大量のコードを一度に書こうとしている場合や、タスクが一度では完結できないほど大きい場合に活用します。