fullstack-dev について
fullstack-dev は、フルスタックアプリやバックエンドサービスの開発向けコーディングスキルです。要件確認、設計判断、実装チェックリスト、検証を順に進め、引き継ぎまで支援します。
主な機能
- 01技術スタックとサービス要件の確認
- 02機能別プロジェクト構成のガイド
- 03バックエンドと連携のチェックリスト
- 04エラー、データベース、認証の設計例
- 05ビルドと連携の検証を要求
活用シーン
- 01フルスタックアプリの新規構築
- 02バックエンドのモジュール境界設計
- 03フロントエンドと API の連携確認
- 04サービスの本番向け強化
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ minimax-ai/ skills --skill fullstack-devインストール後、Claude Code で/fullstack-devを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/api-design.mdauth-flow.mddb-schema.mddjango-best-practices.mdenvironment-management.mdrelease-checklist.mdtechnology-selection.mdtesting-strategy.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:アーキテクチャ上の決定
要件に基づき、コーディングの前に以下の決定を行い、明示してください。
| 決定事項 | 選択肢 | 参照 |
|---|---|---|
| プロジェクト構成 | 機能優先(推奨)またはレイヤー優先 | セクション 1 |
| API クライアントの方式 | 型付き fetch / React Query / tRPC / OpenAPI コード生成 | セクション 5 |
| 認証戦略 | JWT + リフレッシュ / セッション / サードパーティ | セクション 6 |
| リアルタイム方式 | ポーリング / SSE / WebSocket | セクション 11 |
| エラー処理 | 型付きエラー階層 + グローバルハンドラー | セクション 3 |
各選択について簡潔に説明してください(決定事項ごとに 1 文)。
ステップ 2:チェックリストを使ったスキャフォールディング
以下の適切なチェックリストを使ってください。チェック項目をすべて実装し、省略しないでください。
ステップ 3:パターンに従って実装
このドキュメントのパターンに従ってコードを書いてください。各パートの実装時に、該当するセクションを参照してください。
ステップ 4:テストと検証
実装後、完了を報告する前に、以下のチェックを実行してください。
- ビルドチェック:バックエンドとフロントエンドの両方がエラーなくコンパイルできることを確認します。
# Backend cd server && npm run build # Frontend cd client && npm run build - 起動とスモークテスト:サーバーを起動し、主要なエンドポイントが想定どおりのレスポンスを返すことを確認します。
# Start server, then test 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 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 + fetch | React アプリ、サーバー状態 | 手動で型を定義 | 中 |
| tRPC | 同じチーム、両側で TypeScript を使用 | 自動 | 低 |
| OpenAPI 生成 | 公開 API、複数の利用者向け | 自動 | 中 |
| GraphQL codegen | GraphQL 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 | ルート/コントローラーにビジネスロジックを記述する | サービスレイヤーに移す |
| 2 | process.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)を使う |
| 12 | JWT を localStorage に保存する | メモリと httpOnly の更新用 Cookie を使う |
| 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 で構築する(モデル、ビュー、シリアライザー、管理画面) | 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 |