React + TypeScript + Node.js で作る動的英語熟語学習アプリ
はじめに
このアプリ・記事はCursorで作成しました。
英語の熟語を楽しく学べるWebアプリケーションを作成しました。このアプリは、React + TypeScriptでフロントエンドを構築し、Node.jsでAPIサーバーを実装しています。特徴的なのは、外部APIとローカルデータを組み合わせた動的な熟語取得機能です。
アプリの特徴
🎯 主要機能
- 動的熟語取得: ボタン一つで新しい10個の熟語を取得
- 難易度フィルター: 初級・中級・上級で問題を分類
- インタラクティブ学習: 答えを隠して考えてから確認
- レスポンシブデザイン: モバイルでも使いやすいUI
🔄 データ取得の仕組み
-
外部API:
api.dictionaryapi.dev
から実際の熟語フレーズを取得 - ローカル生成: 著作権フリーの熟語データから動的生成
- フォールバック: API失敗時はローカルデータで補完
技術スタック
フロントエンド
- React 18 - UIライブラリ
- TypeScript - 型安全性
- Vite - ビルドツール
- Tailwind CSS - スタイリング
バックエンド
- Node.js - サーバーサイド
- HTTP/HTTPS - 外部API通信
- CORS - クロスオリジン対応
開発・デプロイ
- Concurrently - 並行実行
- Render - ホスティング
- Git - バージョン管理
プロジェクト構造
study-english-idiom/
├── src/
│ ├── components/
│ │ ├── IdiomCard.tsx # 熟語表示カード
│ │ ├── DifficultyFilter.tsx # 難易度フィルター
│ │ └── DynamicIdiomLoader.tsx # 動的データ読み込み
│ ├── services/
│ │ └── api.ts # API通信
│ ├── types/
│ │ └── idiom.ts # 型定義
│ └── data/
│ ├── idioms.ts # サンプルデータ
│ └── scraped-idioms.ts # スクレイピングデータ
├── scripts/
│ ├── api-server.js # APIサーバー
│ └── dynamic-scraper.js # 動的データ生成
└── package.json
実装のポイント
1. 動的データ取得の実装
// src/services/api.ts
const API_BASE_URL = process.env.NODE_ENV === 'production'
? 'https://study-english-idiom-api.onrender.com/api'
: 'http://localhost:3001/api';
export const fetchRandomIdioms = async (count: number = 10, difficulty?: string) => {
const params = new URLSearchParams();
params.append('count', count.toString());
if (difficulty) params.append('difficulty', difficulty);
const timestamp = new Date().getTime();
params.append('t', timestamp.toString());
const response = await fetch(`${API_BASE_URL}/idioms?${params}`, {
cache: 'no-cache'
});
if (!response.ok) {
throw new Error(`API Error: ${response.statusText}`);
}
return response.json();
};
2. 外部APIとの連携
// scripts/dynamic-scraper.js
async function fetchIdiomsFromAPI() {
const idiomPhrases = [
'break the ice', 'hit the nail on the head', 'cost an arm and a leg',
'pull yourself together', 'get over it', 'take it easy'
// ... 60以上の熟語フレーズ
];
const shuffledPhrases = [...idiomPhrases].sort(() => Math.random() - 0.5);
const selectedPhrases = shuffledPhrases.slice(0, 10);
const allIdioms = [];
for (const phrase of selectedPhrases) {
const firstWord = phrase.split(' ')[0];
const apiUrl = `https://api.dictionaryapi.dev/api/v2/entries/en/${firstWord}`;
try {
const response = await makeRequest(apiUrl);
const data = JSON.parse(response);
if (data && data[0]) {
allIdioms.push({
id: `api-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
english: phrase,
japanese: `${phrase}の意味`,
explanation: data[0].meanings?.[0]?.definitions?.[0]?.definition || '説明なし',
example: data[0].meanings?.[0]?.definitions?.[0]?.example || '例文なし',
difficulty: 'medium'
});
}
} catch (error) {
console.log(`API取得エラー: ${phrase}`);
}
// レート制限を避けるため待機
await new Promise(resolve => setTimeout(resolve, 300));
}
return allIdioms;
}
3. キャッシュ制御とランダム化
// 常に10個の熟語を返すように制御
const shuffledIdioms = dynamicIdioms
.map((idiom, index) => ({
id: `scraped-${Date.now()}-${Math.random().toString(36).substr(2, 9)}-${index}`,
...idiom
}))
.sort(() => Math.random() - 0.5)
.slice(0, 10); // 常に10個に制限
console.log(`取得数: ${shuffledIdioms.length}個`);
return shuffledIdioms;
4. CORS設定
// scripts/api-server.js
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Cache-Control, Pragma, Expires',
'Content-Type': 'application/json',
'Cache-Control': 'no-cache, no-store, must-revalidate',
'Pragma': 'no-cache',
'Expires': '0'
};
開発環境のセットアップ
1. 依存関係のインストール
npm install
2. 開発サーバーの起動
# フロントエンド + APIサーバーを並行実行
npm run dev:full
# または個別に起動
npm run dev # フロントエンド (http://localhost:3000)
npm run api # APIサーバー (http://localhost:3001)
3. ビルド
npm run build
デプロイ
Renderでのデプロイ
render.yaml
を使用してフロントエンドとAPIサーバーの両方をデプロイ:
services:
- type: web
name: study-english-idiom
env: static
buildCommand: npm run build
staticPublishPath: ./dist
- type: web
name: study-english-idiom-api
env: node
buildCommand: npm install
startCommand: npm run start
envVars:
- key: NODE_ENV
value: production
学んだこと・課題
✅ 成功した点
- 動的データ取得: 外部APIとローカルデータの組み合わせで豊富なコンテンツを提供
- キャッシュ制御: ブラウザキャッシュを適切に制御して常に新しいデータを取得
- エラーハンドリング: API失敗時のフォールバック機能で安定性を確保
- 型安全性: TypeScriptでランタイムエラーを最小限に抑制
🔧 技術的課題と解決策
- CORSエラー: APIサーバー側のヘッダー設定で解決
- 取得数の不整合: フォールバック機能と最終的な制限で統一
- レート制限: API呼び出し間の待機時間を設定
- 著作権問題: 外部APIとローカルデータの組み合わせで回避
🚀 今後の改善点
- ユーザー認証: 学習進捗の保存機能
- スコア機能: 正答率の記録とランキング
- 音声機能: 発音の確認機能
- オフライン対応: Service Workerでのキャッシュ機能
まとめ
このプロジェクトを通じて、以下の技術を実践的に学ぶことができました:
- React + TypeScriptでの型安全な開発
- Node.jsでのAPIサーバー構築
- 外部API連携とエラーハンドリング
- キャッシュ制御とパフォーマンス最適化
- CORSとセキュリティ考慮
- Renderでのフルスタックデプロイ
特に、外部APIとローカルデータを組み合わせた動的コンテンツ生成の仕組みは、実際のWebアプリケーションでよく使われるパターンで、実践的なスキルを身につけることができました。
参考リンク
技術スタック: React, TypeScript, Node.js, Vite, Tailwind CSS, Render
開発期間: 約4時間
コード行数: 約1,500行