はじめに
この章では、Next.jsアプリケーションからMySQLデータベースへ接続する方法を解説します。ローカル環境での開発だけでなく、Vercelへのデプロイも考慮した構成で設定を進めます。
学習を始める前に、Node.jsがインストールされていること、MySQLまたはPlanetScale・AWS RDSなどのMySQL対応クラウドサービスが利用できること、そして基本的なSQLの知識があることを確認しておきましょう。
MySQL の準備
ローカル MySQL をセットアップ
MySQL をインストール(まだの場合)
まずは、使用するOSに応じてMySQLをインストールします。
- macOS:
brew install mysql - Windows: MySQL 公式インストーラー
- Linux (Ubuntu):
sudo apt-get install mysql-server
データベースとユーザーを作成
データベースへ接続し、アプリケーションで利用するデータベースと専用ユーザーを作成して権限を付与します。これにより、Next.jsから安全にデータベースへ接続できる環境が整います。
mysql -u root -p
CREATE DATABASE nextjs_mysql_demo;
CREATE USER 'nextjs_user'@'localhost' IDENTIFIED BY 'securepassword';
GRANT ALL PRIVILEGES ON nextjs_mysql_demo.* TO 'nextjs_user'@'localhost';
FLUSH PRIVILEGES;
EXIT;
クラウドサービスを使用する場合
PlanetScaleやAWS RDSなどを利用する場合は、サービスのアカウントを作成し、新しいデータベースを準備します。最後に、アプリケーションから接続するために必要な接続文字列(Connection String)を控えておきます。
- アカウントを作成
- 新しいデータベースを作成
- 接続文字列をメモ(後で使用します)
ステップ 2: Next.js プロジェクトのセットアップ
新しい Next.js プロジェクトを作成(まだの場合)
create-next-appを使用すると、Next.jsプロジェクトを簡単に作成できます。作成後は、プロジェクトディレクトリへ移動して開発を開始します。
npx create-next-app@latest nextjs-mysql-demo
cd nextjs-mysql-demo
必要な依存関係をインストール
mysql2パッケージをインストールすると、Next.jsからMySQLデータベースへ接続し、SQLを実行できるようになります。npmまたはyarnのどちらを使用してもインストール可能です。
npm install mysql2
# または
yarn add mysql2
環境変数の設定
プロジェクトルートに .env.local ファイルを作成
プロジェクトのルートに.env.localファイルを作成し、MySQLの接続情報を設定します。ローカル環境ではホスト名やユーザー名などを個別に指定し、クラウドサービスを利用する場合は接続文字列(DATABASE_URL)を設定することで、アプリケーションからデータベースへ接続できます。
MYSQL_HOST="localhost"
MYSQL_PORT="3306"
MYSQL_DATABASE="nextjs_mysql_demo"
MYSQL_USER="nextjs_user"
MYSQL_PASSWORD="securepassword"
# クラウドサービスの場合は次のような形式になります:
# DATABASE_URL="mysql://username:password@host:port/database?options"
.gitignore に .env.local が含まれていることを確認
.env.localにはパスワードなどの機密情報が含まれるため、.gitignoreに登録されていることを確認しましょう。これにより、GitHubなどのリポジトリへ誤って公開されることを防げます。
データベース接続の設定
lib/db.ts ファイルを作成します。このコードは、Next.jsでMySQLへ接続するための共通モジュールを作成しています。createPool()を利用して接続プールを作成し、環境変数からデータベースの接続情報を読み込んでいます。
また、query()関数を用意することで、接続プールが初期化されていない場合は自動で初期化し、その後SQLを実行して結果を返します。これにより、アプリケーション全体でデータベース接続を効率的に管理できます。
import { createPool, Pool } from 'mysql2/promise';
import dotenv from 'dotenv';
dotenv.config();
let pool: Pool;
export async function initializePool() {
pool = createPool({
host: process.env.MYSQL_HOST,
port: parseInt(process.env.MYSQL_PORT || '3306'),
database: process.env.MYSQL_DATABASE,
user: process.env.MYSQL_USER,
password: process.env.MYSQL_PASSWORD,
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0,
});
}
export async function query(sql: string, values?: any[]) {
if (!pool) await initializePool();
const [rows] = await pool.query(sql, values);
return rows;
}
データベース操作の実装
例としてユーザーテーブルの作成と操作について以下のコードを実装します。
初期セットアップスクリプトを作成 scripts/initDB.ts
initDatabese()メソッドはデータベースにusersテーブルが存在しない場合に自動で作成します。query()を使用してSQLを実行し、初期化が成功した場合やエラーが発生した場合のメッセージを表示します。
import { query } from '../lib/db';
async function initDatabase() {
try {
await query(`
CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(100) UNIQUE NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
`);
console.log('MySQL database initialized successfully');
} catch (error) {
console.error('Error initializing MySQL database:', error);
}
}
initDatabase();
スクリプトを実行
ts-nodeを使用してTypeScriptファイルを直接実行します。このコマンドを実行することで、データベースの初期設定が行われ、必要なテーブルが作成されます。
npx ts-node scripts/initDB.ts
API ルートでのデータベース使用
pages/api/users.ts を作成します。このコードは、Next.jsのAPI Routeを利用してユーザーの取得と登録を行うAPIを実装しています。GETリクエストでは、作成日時の新しい順にユーザー一覧を取得し、POSTリクエストでは入力内容を検証したうえで新しいユーザーを登録します。
登録後は、追加されたユーザー情報を取得して返しています。また、未対応のHTTPメソッドには405、予期しないエラーには500を返すことで、適切なエラーハンドリングも行っています。
import { NextApiRequest, NextApiResponse } from 'next';
import { query } from '../../lib/db';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
try {
switch (req.method) {
case 'GET':
const users = await query('SELECT * FROM users ORDER BY created_at DESC');
res.status(200).json(users);
break;
case 'POST':
const { name, email } = req.body;
if (!name || !email) {
return res.status(400).json({ message: 'Name and email are required' });
}
const result = await query(
'INSERT INTO users (name, email) VALUES (?, ?)',
[name, email]
);
// MySQLの場合は挿入されたIDを別途取得
const [newUser] = await query('SELECT * FROM users WHERE id = LAST_INSERT_ID()');
res.status(201).json(newUser);
break;
default:
res.setHeader('Allow', ['GET', 'POST']);
res.status(405).end(`Method ${req.method} Not Allowed`);
}
} catch (error) {
console.error('API Error:', error);
res.status(500).json({ message: 'Internal server error' });
}
}
ページコンポーネントでのデータ取得
SSG (静的生成) でのデータ取得
pages/users/index.tsxでは以下のように設定します。このコードは、Next.jsの静的生成(SSG)を利用してユーザー一覧を表示するページを作成しています。
getStaticProps()でMySQLからユーザーデータを取得し、ページ生成時にデータを渡しています。取得したデータは画面上で一覧表示され、revalidateを60に設定することで、ISR(Incremental Static Regeneration)により60秒ごとにページが自動で再生成され、最新のデータが反映されます。
import { GetStaticProps } from 'next';
import { query } from '../../lib/db';
interface User {
id: number;
name: string;
email: string;
created_at: string;
}
export default function UsersPage({ users }: { users: User[] }) {
return (
<div>
<h1>Users</h1>
<ul>
{users.map((user) => (
<li key={user.id}>
{user.name} - {user.email}
</li>
))}
</ul>
</div>
);
}
export const getStaticProps: GetStaticProps = async () => {
const users = await query('SELECT * FROM users ORDER BY created_at DESC');
return {
props: {
users: JSON.parse(JSON.stringify(users)), // Dateオブジェクトをシリアライズ
},
revalidate: 60, // ISR: 60秒ごとに再生成
};
};
SSR (サーバーサイドレンダリング) でのデータ取得
pages/users/ssr.tsxでは以下のように設定します。このコードは、Next.jsのサーバーサイドレンダリング(SSR)を利用してユーザー一覧を表示するページを作成しています。
getServerSideProps()がリクエストごとに実行され、MySQLから最新のユーザーデータを取得してページへ渡します。そのため、ページを表示するたびに最新の情報が反映される点が特徴です。取得したデータはコンポーネント内で一覧表示され、常に最新の状態をユーザーへ提供できます。
import { GetServerSideProps } from 'next';
import { query } from '../../lib/db';
interface User {
id: number;
name: string;
email: string;
created_at: string;
}
export default function UsersSSRPage({ users }: { users: User[] }) {
return (
<div>
<h1>Users (SSR)</h1>
<ul>
{users.map((user) => (
<li key={user.id}>
{user.name} - {user.email}
</li>
))}
</ul>
</div>
);
}
export const getServerSideProps: GetServerSideProps = async () => {
const users = await query('SELECT * FROM users ORDER BY created_at DESC');
return {
props: {
users: JSON.parse(JSON.stringify(users)),
},
};
};
Vercel へのデプロイ
Vercel でプロジェクトを作成
まずはVercelで新しいプロジェクトを作成し、Next.jsアプリケーションをデプロイできる環境を準備します。
環境変数を設定
Vercelの「Settings」→「Environment Variables」で、本番環境用のMySQL接続情報を登録します。ソースコードに機密情報を記述せず、安全にデータベースへ接続できます。
vercel.json をプロジェクトルートに作成(必要に応じて)
必要に応じてvercel.jsonを作成し、ビルド設定や環境変数を定義します。
{
"version": 2,
"builds": [
{
"src": "package.json",
"use": "@vercel/next"
}
],
"env": {
"MYSQL_HOST": "@mysql_host",
"MYSQL_USER": "@mysql_user",
"MYSQL_PASSWORD": "@mysql_password",
"MYSQL_DATABASE": "@mysql_database"
}
}
これにより、デプロイ時の設定をプロジェクト内で管理できます。
デプロイ
vercelコマンドを実行するか、GitHubと連携して自動デプロイを設定することで、アプリケーションをVercelへ公開できます。
vercel
# または GitHub と連携して自動デプロイ
MySQL 固有の注意点
接続プーリング
- MySQLでは接続プールの管理が重要
createPoolを使用して接続を効率的に管理
日付処理
- MySQLの日付型はJavaScriptのDateオブジェクトに自動変換
- SSG/SSRでpropsに渡す際は
JSON.parse(JSON.stringify())でシリアライズ
LAST_INSERT_ID()
- 挿入後のID取得には
LAST_INSERT_ID()関数を使用
PlanetScale を使用する場合
- サーバーレス環境に最適化されたMySQL互換データベース
- 接続に
ssl: { rejectUnauthorized: true }が必要
接続プーリングの最適化(本番環境向け)
lib/db.ts を更新して本番環境用に最適化します。
import { createPool, Pool } from 'mysql2/promise';
import dotenv from 'dotenv';
dotenv.config();
const isProduction = process.env.NODE_ENV === 'production';
let pool: Pool;
export async function initializePool() {
pool = createPool({
host: process.env.MYSQL_HOST,
port: parseInt(process.env.MYSQL_PORT || '3306'),
database: process.env.MYSQL_DATABASE,
user: process.env.MYSQL_USER,
password: process.env.MYSQL_PASSWORD,
ssl: isProduction ? { rejectUnauthorized: true } : undefined,
waitForConnections: true,
connectionLimit: isProduction ? 20 : 10,
queueLimit: 0,
idleTimeout: 60000, // アイドル接続のタイムアウト(ms)
enableKeepAlive: true, // 接続を維持
keepAliveInitialDelay: 0,
});
}
export async function query(sql: string, values?: any[]) {
if (!pool) await initializePool();
try {
const [rows] = await pool.query(sql, values);
return rows;
} catch (error) {
console.error('MySQL query error:', error);
throw error;
}
}
// アプリケーション終了時に接続プールをクリーンアップ
process.on('SIGINT', async () => {
if (pool) {
await pool.end();
console.log('MySQL connection pool closed');
}
process.exit(0);
});
セキュリティのベストプラクティス(MySQL版)
SSL接続の強制:
- 本番環境では必ずSSLを使用
ssl: { rejectUnauthorized: true }
プリペアドステートメント:
- SQLインジェクション防止のために常にパラメータ化クエリを使用
await query('SELECT * FROM users WHERE id = ?', [userId]);
最小権限の原則:
- アプリケーションユーザーには必要な権限のみ付与
接続情報の保護:
- 環境変数を使用
- ソースコードに直接記述しない
定期的なパスワードローテーション:
- データベース認証情報を定期的に更新
代替ORMオプション(MySQL)
MySQLでORMを使用する場合
Prisma
npm install prisma @prisma/client
npx prisma init
schema.prisma でMySQLを指定
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
Sequelize
npm install sequelize mysql2
TypeORM
npm install typeorm reflect-metadata mysql2
トラブルシューティング(MySQL版)
接続エラー
- エラー:
ER_NOT_SUPPORTED_AUTH_MODE
ALTER USER 'username'@'localhost' IDENTIFIED WITH mysql_native_password BY 'password';
タイムゾーン問題
// 接続設定に追加
timezone: '+09:00' // 日本時間の場合
Vercelで接続できない
- VercelのIPをデータベースの許可リストに追加
- PlanetScaleなどVercelと相性の良いサービスを検討
接続タイムアウト
// プール設定に追加
connectTimeout: 10000, // 10秒
パフォーマンスチューニング
インデックスの追加
CREATE INDEX idx_users_email ON users(email);
EXPLAIN の使用
const explain = await query('EXPLAIN SELECT * FROM users WHERE email = ?', [email]);
console.log(explain);
適切なデータ型の選択
- VARCHAR ではなく TEXT が必要な場合
- INT ではなく TINYINT で十分な場合
このガイドで、Next.js アプリケーションから MySQL データベースに接続する方法を理解できたはずです。実際のプロジェクトでは、要件に応じてさらに最適化を加えてください。