MinWeb2025のサンプルプロジェクトです。フロントエンドはReact、バックエンドはNode.js (Express)、データベースはMySQLを使用しています。
バックエンドは以下のレイヤーで構成されています:
domain/entities
: ビジネスオブジェクトとビジネスルールを定義しますdomain/repositories
: リポジトリのインターフェースを定義します
application/usecases
: アプリケーションのユースケースを実装します- 各ユースケースは単一の責任を持ちます
interfaces/controllers
: HTTPリクエストを処理し、適切なユースケースを呼び出します
infrastructure/repositories
: リポジトリの具体的な実装(MySQLなど)を提供します
フロントエンドも類似のアーキテクチャを採用しています:
domain/entities
: ビジネスオブジェクトを定義します
infrastructure/api
: APIとの通信を処理します
presentation/components
: UI部品を提供しますpresentation/pages
: ページコンポーネントを提供します
- フロントエンド: React, TypeScript
- バックエンド: Node.js, Express, TypeScript
- データベース: MySQL
- 開発/デプロイ: Docker, docker-compose
backend
: バックエンドサービス (Node.js/Express)frontend
: フロントエンドビルド環境 (React)nginx
: Webサーバー (静的ファイル配信・リバースプロキシ)mysql
: データベースサービス
# 起動
docker-compose up -d
# 停止
docker-compose down
# ボリュームも含めて完全に環境を削除
docker-compose down -v
cd frontend
npm install
npm start
cd frontend
npm run build
-
src/index.tsx
: エントリーポイントimport React from 'react'; import ReactDOM from 'react-dom/client'; import App from './App'; const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement); root.render( <React.StrictMode> <App /> </React.StrictMode> );
-
src/App.tsx
: メインアプリケーションコンポーネントimport React from 'react'; import { BrowserRouter, Routes, Route } from 'react-router-dom'; import { HomePage, TaskListPage, TaskDetailPage } from './presentation/pages'; import { Header, Footer } from './presentation/components/common'; const App: React.FC = () => { return ( <BrowserRouter> <Header /> <main> <Routes> <Route path="/" element={<HomePage />} /> <Route path="/tasks" element={<TaskListPage />} /> <Route path="/tasks/:id" element={<TaskDetailPage />} /> </Routes> </main> <Footer /> </BrowserRouter> ); }; export default App;
-
src/domain/entities/
: ドメインオブジェクト// src/domain/entities/Task.ts export interface Task { id: number; title: string; description: string; completed: boolean; createdAt: Date; }
-
src/infrastructure/api/
: API通信処理// src/infrastructure/api/taskApi.ts import { Task } from '../../domain/entities/Task'; const API_BASE_URL = '/api'; export const fetchTasks = async (): Promise<Task[]> => { const response = await fetch(`${API_BASE_URL}/tasks`); if (!response.ok) throw new Error('Failed to fetch tasks'); return response.json(); }; export const fetchTaskById = async (id: number): Promise<Task> => { const response = await fetch(`${API_BASE_URL}/tasks/${id}`); if (!response.ok) throw new Error('Failed to fetch task'); return response.json(); };
-
src/presentation/
: UI関連のコンポーネント// src/presentation/components/task/TaskItem.tsx import React from 'react'; import { Task } from '../../../domain/entities/Task'; interface TaskItemProps { task: Task; onComplete: (id: number) => void; } export const TaskItem: React.FC<TaskItemProps> = ({ task, onComplete }) => { return ( <div className="task-item"> <h3>{task.title}</h3> <p>{task.description}</p> <span>{task.completed ? '完了' : '未完了'}</span> <button onClick={() => onComplete(task.id)}>完了にする</button> </div> ); };
cd backend
npm install
npm run dev
cd backend
npm run build
-
src/index.ts
: エントリーポイントimport express from 'express'; import cors from 'cors'; import taskRoutes from './routes/taskRoutes'; const app = express(); const PORT = process.env.PORT || 3001; app.use(cors()); app.use(express.json()); // ルートの設定 app.use('/api/tasks', taskRoutes); app.listen(PORT, () => { console.log(`Server is running on port ${PORT}`); });
-
src/routes/
: APIルート定義// src/routes/taskRoutes.ts import { Router } from 'express'; import { TaskController } from '../interfaces/controllers/TaskController'; const router = Router(); const taskController = new TaskController(); router.get('/', taskController.getAllTasks); router.get('/:id', taskController.getTaskById); router.post('/', taskController.createTask); router.put('/:id', taskController.updateTask); router.delete('/:id', taskController.deleteTask); export default router;
-
src/domain/
: ドメイン層(エンティティ、リポジトリインターフェース)// src/domain/entities/Task.ts export interface Task { id?: number; title: string; description: string; completed: boolean; createdAt?: Date; } // src/domain/repositories/TaskRepository.ts import { Task } from '../entities/Task'; export interface TaskRepository { findAll(): Promise<Task[]>; findById(id: number): Promise<Task | null>; create(task: Task): Promise<Task>; update(id: number, task: Partial<Task>): Promise<Task | null>; delete(id: number): Promise<boolean>; }
-
src/application/
: アプリケーション層(ユースケース)// src/application/usecases/task/GetAllTasksUseCase.ts import { Task } from '../../../domain/entities/Task'; import { TaskRepository } from '../../../domain/repositories/TaskRepository'; export class GetAllTasksUseCase { constructor(private taskRepository: TaskRepository) {} async execute(): Promise<Task[]> { return this.taskRepository.findAll(); } }
-
src/interfaces/
: インターフェース層(コントローラー)// src/interfaces/controllers/TaskController.ts import { Request, Response } from 'express'; import { GetAllTasksUseCase } from '../../application/usecases/task/GetAllTasksUseCase'; import { GetTaskByIdUseCase } from '../../application/usecases/task/GetTaskByIdUseCase'; import { MySQLTaskRepository } from '../../infrastructure/repositories/MySQLTaskRepository'; const taskRepository = new MySQLTaskRepository(); export class TaskController { async getAllTasks(req: Request, res: Response): Promise<void> { try { const useCase = new GetAllTasksUseCase(taskRepository); const tasks = await useCase.execute(); res.json(tasks); } catch (error) { res.status(500).json({ error: 'Internal Server Error' }); } } // 他のコントローラーメソッド... }
-
src/infrastructure/
: インフラストラクチャ層(リポジトリ実装など)// src/infrastructure/repositories/MySQLTaskRepository.ts import { Task } from '../../domain/entities/Task'; import { TaskRepository } from '../../domain/repositories/TaskRepository'; import { pool } from '../database/mysql'; export class MySQLTaskRepository implements TaskRepository { async findAll(): Promise<Task[]> { const [rows] = await pool.query('SELECT * FROM tasks'); return rows as Task[]; } async findById(id: number): Promise<Task | null> { const [rows] = await pool.query('SELECT * FROM tasks WHERE id = ?', [id]); const tasks = rows as Task[]; return tasks.length > 0 ? tasks[0] : null; } // 他のリポジトリメソッド... }
データベースの初期化スクリプトは mysql/init/
ディレクトリに配置されています。
- ホスト:
mysql
(コンテナ内から) - ユーザー:
user
- パスワード:
password
- データベース名:
minweb
- ポート:
3306