Skip to content

安全に壊れるための設計原則

「正常に動く」よりも「異常時に安全に壊れる」ことを優先する設計原則を詳しく解説します。

外部(API・DOM・ユーザ入力)からのデータは常に汚染されていると仮定し、型・形式・範囲を検査してからロジックに渡す。

// ❌ 悪い例: 無防備な入力受付
export async function getServerSideProps({ query }: { query: any }) {
// 問題: 型チェックなし、バリデーションなし
const page = query.page;
const posts = await fetch(`https://api.example.com/posts?page=${page}`);
return {
props: {
posts: await posts.json(),
},
};
}
// ✅ 良い例: 境界防御の実装
interface QueryParams {
page?: string;
}
function validateQueryParams(query: unknown): QueryParams {
if (typeof query !== 'object' || query === null) {
throw new Error('Invalid query parameters');
}
const obj = query as Record<string, unknown>;
if (obj.page !== undefined && typeof obj.page !== 'string') {
throw new Error('Invalid page parameter');
}
return {
page: typeof obj.page === 'string' ? obj.page : undefined,
};
}
export async function getServerSideProps({ query }: { query: unknown }) {
const validated = validateQueryParams(query);
const page = parseInt(validated.page || '1', 10);
if (page < 1 || page > 1000) {
throw new Error('Page number out of range');
}
const res = await fetch(`https://api.example.com/posts?page=${page}`);
const posts = await res.json();
return {
props: {
posts,
},
};
}

なぜ重要か:

  • 型安全性: TypeScriptの型システムで型チェック
  • バリデーション: 形式・範囲を検査
  • セキュリティ: XSS、インジェクションなどの攻撃を防止

DOM更新・API呼び出し・ストレージ操作などの副作用をロジックの末尾に集約し、それ以前を状態を持たない純粋処理として保つ。

// ❌ 悪い例: 副作用が散在
'use client';
export default function Page() {
const [data, setData] = useState<any[]>([]);
useEffect(() => {
// 副作用1: API呼び出し
fetch('/api/data')
.then(res => res.json())
.then(json => setData(json));
// ビジネスロジック(副作用が混在)
if (data.length > 100) {
// 副作用2: ストレージ操作
localStorage.setItem('largeData', JSON.stringify(data));
}
// 副作用3: DOM更新
document.title = `Data: ${data.length}`;
}, [data]);
return <div>{data.length}</div>;
}
// ✅ 良い例: 副作用の局所化
'use client';
function processData(rawData: any[]): ProcessedData[] {
// 純粋関数: 副作用なし
return rawData.map(item => ({
id: item.id,
name: item.name.toUpperCase(),
timestamp: new Date(item.timestamp).getTime(),
}));
}
export default function Page() {
const [data, setData] = useState<ProcessedData[]>([]);
useEffect(() => {
fetch('/api/data')
.then(res => res.json())
.then(rawData => {
// 1. 純粋処理: ビジネスロジック(副作用なし)
const processed = processData(rawData);
// 2. 副作用の集約: すべての副作用を末尾に
setData(processed);
saveToStorageIfNeeded(processed);
updateDocumentTitle(processed.length);
});
}, []);
return <div>{data.length}</div>;
}
function saveToStorageIfNeeded(data: ProcessedData[]) {
if (data.length > 100) {
localStorage.setItem('largeData', JSON.stringify(data));
}
}
function updateDocumentTitle(count: number) {
document.title = `Data: ${count}`;
}

なぜ重要か:

  • テスト容易性: 純粋関数は単体テストが容易
  • 可読性: 副作用が明確に分離される
  • デバッグ容易性: 副作用の発生箇所が明確

ビジネスロジックが特定ライブラリやNext.jsの仕様に依存しないよう、インターフェース層で抽象化する。

// ❌ 悪い例: Next.jsのAPIに直接依存
export async function getServerSideProps() {
// Next.jsのAPIに直接依存
const res = await fetch('https://api.example.com/data');
return {
props: {
data: await res.json(),
},
};
}
// ✅ 良い例: インターフェースで抽象化
interface DataRepository {
fetchData(): Promise<Data[]>;
}
class ApiDataRepository implements DataRepository {
async fetchData(): Promise<Data[]> {
const res = await fetch('https://api.example.com/data');
return res.json();
}
}
class MockDataRepository implements DataRepository {
async fetchData(): Promise<Data[]> {
return [{ id: 1, name: 'Mock Data' }];
}
}
// サービス層: インターフェースに依存
class DataService {
constructor(private repository: DataRepository) {}
async getData(): Promise<Data[]> {
return this.repository.fetchData();
}
}
// Next.jsのAPI Routesで使用
export async function getServerSideProps() {
const repository = new ApiDataRepository();
const service = new DataService(repository);
const data = await service.getData();
return {
props: {
data,
},
};
}

なぜ重要か:

  • 交換容易性: APIを変更してもビジネスロジックは変更不要
  • テスト容易性: モックで簡単にテスト可能
  • 保守性: フレームワークの変更に強い

安全に壊れるための設計原則のポイント:

  • 境界防御: 外部データは常に汚染されていると仮定し、型・形式・範囲を検査
  • 副作用の局所化: 副作用をロジックの末尾に集約し、純粋処理と分離
  • 依存の隔離: ビジネスロジックが特定ライブラリに依存しないよう抽象化

これらの原則により、「異常時に安全に壊れる」堅牢なシステムを構築できます。