概要
以前、生成AIを用いたアプリ開発ではNext.jsよりもViteの方が使いやすいと述べました。
参考:Claude 3.5 Sonnet Artifactsで爆速アプリ開発してみた
ですが、サーバサイド処理はNext.jsの方がお手軽にできるイメージでした。
そこで、今回は自分用のメモとして、Viteを用いたサーバサイド処理の実装方法をまとめます。
例によって、Claude 3.5に実装方法のベースを書かせています。
環境
- マシン:Macbook Air (M1チップ、2020)
- OS:Sonoma 14.6
- Docker:v4.33.0
- Node.js v18.16.0
- MySQL Ver 8.0.36 for macos14.2 on arm64 (Homebrew)
アーキテクチャ
- Viteプロジェクトの中でExpressサーバを立てて、データフェッチはサーバサイド、レンダリングはクライアントサイドで行います。
- Server-side処理とClient-side処理が分かりやすいよう、docker composeを用います。
- Expressサーバはコンテナ外からのアクセスは出来ないようにします。
アプリケーションを公開するならGoogle Cloud Platformのコンテナ系サービスを使いたい気持ちで書いています。
実装方法
- 新しいディレクトリを作成し、Viteプロジェクト用ディレクトリに移動します
mkdir my-app cd my-app mkdir vite-project database cd vite-project
- Viteプロジェクトを作成します
npm create vite@latest npm install
- 必要なパッケージをインストールします
npm install express mysql2 npm install -D concurrently ts-node @types/express @types/node
- src/server.tsファイルを作成し、以下の内容を追加します。
src/server.ts
import express from 'express'; import mysql from 'mysql2/promise'; import cors from 'cors'; const app = express(); const port = 3000; app.use(cors()); app.get('/api/hello', async (req, res) => { try { const connection = await mysql.createConnection({ host: process.env.DB_HOST || 'database', user: process.env.DB_USER || 'root', password: process.env.DB_PASSWORD || 'password', database: process.env.DB_NAME || 'testdb', port: 3306 }); const [rows] = await connection.execute('SELECT * FROM hello_world'); await connection.end(); if (Array.isArray(rows) && rows.length > 0) { res.json(rows[0]); } else { res.status(404).json({ error: 'No data found' }); } } catch (error) { console.error('Database error:', error); res.status(500).json({ error: 'Internal Server Error', details: error instanceof Error ? error.message : String(error) }); } }); app.listen(port, '127.0.0.1', () => { console.log(`Server running at http://localhost:${port}`); });
- Expressサーバがコンテナ内でのみアクセス可能になるように、app.listenに
127.0.0.1
(=localhost)を指定します。
- Expressサーバがコンテナ内でのみアクセス可能になるように、app.listenに
- src/App.tsxファイルを次のように更新します。
src/App.tsx
import { useState, useEffect } from 'react'; function App() { const [message, setMessage] = useState(''); const [error, setError] = useState(''); useEffect(() => { fetch('/api/hello') .then(response => { if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return response.json(); }) .then(data => { if (data.message) { setMessage(data.message); } else { setError('Unexpected data structure'); } }) .catch(error => { console.error('Error:', error); setError(`Failed to fetch data: ${error.message}`); }); }, []); if (error) { return <div>Error: {error}</div>; } return ( <div> <h1>Message from MySQL:</h1> <h1>{message}</h1> </div> ); } export default App;
- APIリクエストをプロキシする設定を行います。
- package.jsonファイルのscriptフィールドを以下のように更新します。
package.json
"scripts": { "dev": "concurrently \"vite\" \"NODE_OPTIONS='--experimental-specifier-resolution=node' node --loader ts-node/esm src/server.ts\"", "build": "tsc && vite build", "preview": "vite preview" }
- concurrentlyを使うことで、フロントエンドの開発サーバーとバックエンドのサーバーを同時に立ち上げる
-
--experimental-specifier-resolution=node
: Node.jsがESモジュールの解決方法を変更 -
--loader ts-node/esm
: TypeScriptのコードを直接実行するためのローダー。ts-nodeはTypeScriptのコードを直接実行できるツールで、/esm
はESモジュールをサポートするためのオプション。
- tsconfig.app.jsonを削除し、tsconfig.jsonを以下のように更新します。
tsconfig.json
{ "compilerOptions": { "target": "ES2020", "module": "ESNext", "moduleResolution": "node", "esModuleInterop": true, "strict": true, "outDir": "./dist", "rootDir": "./src", "typeRoots": ["./node_modules/@types"], "types": ["node"], "allowJs": true, "resolveJsonModule": true, "allowSyntheticDefaultImports": true, "jsx": "react-jsx" }, "ts-node": { "esm": true, "experimentalSpecifierResolution": "node" }, "include": ["src/**/*"], "exclude": ["node_modules"] }
- vite.config.tsを以下のように更新します。
vite.config.ts
import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], server: { host: '0.0.0.0', port: 5173, strictPort: true, proxy: { '/api': 'http://127.0.0.1:3000' } } })
- ViteプロジェクトのルートディレクトリにDockerfileを作成し、以下の内容を追加します。
Dockerfile
FROM node:18 WORKDIR /app COPY package*.json ./ RUN npm install COPY . . EXPOSE 5173 CMD ["npm", "run", "dev"]
- databaseディレクトリ配下にinit.sqlファイルを作成し、以下の内容を追加します。
init.sql
USE testdb; CREATE TABLE IF NOT EXISTS hello_world ( id INT AUTO_INCREMENT PRIMARY KEY, message VARCHAR(255) NOT NULL ); INSERT INTO hello_world (message) VALUES ('Hello World');
- my-appディレクトリ配下にdocker-compose.yamlを作成し、以下の内容を追加します。
docker-compose.yaml
version: '3' services: app: build: ./vite-project ports: - "5173:5173" environment: - DB_HOST=database - DB_USER=root - DB_PASSWORD=mysql - DB_NAME=testdb depends_on: - database database: image: mysql:8 environment: MYSQL_ROOT_PASSWORD: mysql MYSQL_DATABASE: testdb volumes: - ./mysql-data:/var/lib/mysql - ./database/init.sql:/docker-entrypoint-initdb.d/init.sql
- vite-projectディレクトリからmy-appディレクトリに移動し、コンテナを立ち上げます。
cd .. docker compose up -d
- ブラウザを立ち上げ、
http://localhost:5173
に接続して、以下の画面が表示されていることを確認します。
- ブラウザから、
http://localhost:3000/api/hello
に接続して、アクセスできないことを確認します。