
Next.js で PlanetScale を使ってみた
この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
西田@MADグループです
今回はフルマネージドなサーバーレスRDBである、 PlanetScale を Next.js で作成したアプリケーションから使ってみたいと思います
なお、この記事で作成するアプリケーションのソースコードはこちらのリポジトリにアップロードしてます
PlanetScale とは?
PlnetScale は MySQL互換のサーバーレスデーターベースです
シームレスに水平スケーリングでき、その裏側として Vitess が採用されています
Vitess は Youtube や Slack でも採用された実績があるオープンソースデータベースです
PlanetScale の主な特徴
- 設定なしで水平スケールが可能で、大規模なデータやトラフィックに対応
- ダウンタイムなしのテーブル定義変更
- Git のようなブランチ機能
- サーバーレス
PlanetScale にサインアップ
ます PlanetScale にアカウントを以下のページから作成します
https://auth.planetscale.com/sign-up
※ 執筆時点では Github と Email でのサインアップが可能でした
今回の構成
- Next.js
- Prisma
- PlanetScale
データベースを作成
データベースを作成します
任意の名前を入力して、リージョンを選択します
リージョンには ap-northeast-1 (東京) を選択しました

Next.js のセットアップ
Next.js のプロジェクトを作成します
今回は TypeScript で作成し、 src 配下に pages と styles を配置します
$ npx create-next-app --typescript sample $ cd sample $ mkdir src $ mv pages styles src/
この時点で一度動作確認をしておきます
npm run dev もしくは yarn dev をターミナルで実行して、ブラウザで http://localhost:3000 にアクセスします
$ npm run dev

Prisma をセットアップ
今回はORMとして Prisma を使用します
npx prisma init で必要なファイルを生成し、Prisma を使って PlanetScale に接続するために必要な設定をします
prisma initコマンドで必要なファイルを生成
$ npx prisma init
prisma initコマンドで生成された prisma/schema.prismaファイルを編集
generator client {
  provider = "prisma-client-js"
  // 以下の一文を追加
    previewFeatures = ["referentialIntegrity"]
}
datasource db {
  // provider に mysql を指定
  provider = "mysql"
  url      = env("DATABASE_URL")
  // 以下の一文を追加
  referentialIntegrity = "prisma"
}
referentialIntegrity を有効にする理由
Prisma ではMongoDB 以外のコネクタの場合、デフォルトで外部キー制約を使って、参照整合性を担保します
ただし、 PlanetScale では外部キー制約をサポートしません
Prisma で PlanetScale を使用する際は、referentialIntegrity を “prisma” にし外部キー制約を使用しないようにします
参考: Setting the referential integrity
PlanetScale CLI をインストール
PlanetScale をCLIで操作するためのコマンドを pscale をインストールします
$ brew install planetscale/tap/pscale $ brew install mysql-client
※ 上記は Mac でのインストール方法です。その他のOSは以下を参考にしてください
参考: PlanetScale CLI Installation
PlanetScale にサインインしてCLIを利用する準備をします
$ pscale auth login
pscale auth loginコマンドを実行するとブラウザが起動し Confirmation Code を確認するページに遷移するので、ターミナルに表示された、 Confirmation Code と一致してるか確認して「Confirm code」をクリックします


※ mysql-client がまだ未インストールの場合
pscale コマンドでクエリを実行するのに mysql-client が必要なので、まだインストールしていない場合はインストールします
$ brew install mysql
※ 上記は Mac でのインストール方法です。
PlanetScale への接続をプロキシ
PlanetScale CLI を使って、ローカルマシンの 3309ポート(任意) に PlanetSclae への接続をプロキシし、ローカルマシンの 3309番ポートから PlanetScale に接続できるようにします
$ pscale connect $DB_NAME main --port 3309
$DB_NAME にはデータベースを作成時に設定した Name (この記事なら test-branch)を指定します
PlanetScale の接続情報を設定
.env ファイルにPlanetScaleへの接続文字列を記述し、環境変数として参照できるようにします
DATABASE_URL="mysql://root@127.0.0.1:3309/$DB_NAME"
Prisma のスキーマを定義
prisma/schema.prismaファイルにスキーマを追記し、Inquiry モデルを定義します
model Inquiry {
  id Int @default(autoincrement()) @id
  name String
  email String
  subject String
  message String
}
Prisma のスキーマを PlanetScale に反映
Prisma のスキーマを PlanetScale に反映します
この操作でデータベースとテーブルが作成されます
$ npx prisma db push
スキーマの反映を確認
psclae shell コマンドでPlanetScale上で動作中のデータベースにアクセスし、スキーマが反映されたのを確認します
$ pscale shell $DB_NAME main $DB_NAME/main> show tables; +-----------------------+ | Tables_in_test-branch | +-----------------------+ | Inquiry | +-----------------------+ $DB_NAME/main> desc Inquiry; +---------+--------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +---------+--------------+------+-----+---------+----------------+ | id | int | NO | PRI | NULL | auto_increment | | name | varchar(191) | NO | | NULL | | | email | varchar(191) | NO | | NULL | | | subject | varchar(191) | NO | | NULL | | | message | varchar(191) | NO | | NULL | | +---------+--------------+------+-----+---------+----------------+
Next.js の API Route でデータを登録するAPIを作成
プロジェクト内のファイルをインポートするときに、プロジェクトのルートディレクトリからの絶対パスでインポートできるよう tsconfig.jsonを変更します
{
  "compilerOptions": {
        // 省略...
        "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    }
}
src/libs/database.tsファイルを作成し、データベースにデータを登録する関数を作成します
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient()
export type InquiryInput = {
    name: string,
    email: string,
    subject: string,
    message: string,
}
export async function createInquiry(params: InquiryInput) {
    return await prisma.inquiry.create({data: params});
}
src/pages/api/inquiries/index.tsファイルを作成し Inquiry を登録するAPI(POST /api/inquiries) を作成します
import { createInquiry } from "@/libs/database";
import type { NextApiRequest, NextApiResponse } from "next";
const postInquiry = async (req: NextApiRequest, res: NextApiResponse) => {
    const body = req.body
    const inquiry = await createInquiry({
        name: body.name,
        email: body.email,
        subject: body.subject,
        message: body.message,
    })
    return res.status(200).json(inquiry);
}
export default async (req: NextApiRequest, res: NextApiResponse) => {
    switch(req.method) {
        case "POST":
            return await postInquiry(req, res);
        default:
            return res.status(405).json({message: "Method not allowed"})
    }
}
参考: Next.js API Route (TypeScript)
curl コマンドを使って動作確認をします
$ curl -XPOST -H "content-type: application/json" localhost:3000/api/inquiries -d "{\"name\": \"abc\", \"email\":\"abc@example.com\",\"subject\": \"sample\",\"message\":\"sample message\"}"
> {"id":1,"name":"abc","email":"abc@example.com","subject":"sample","message":"sample message"}⏎
Next.js の API Route でデータを取得するAPIを作成
src/libs/databse.tsにデータベースからデータを取得する関数を追加します
export async function fetchInquiries() {
    return await prisma.inquiry.findMany();
}
src/pages/api/inquiries/index.tsファイルに Inquiry を取得するAPI (GET /api/inquiries) を追加します
export default async (req: NextApiRequest, res: NextApiResponse) => {
    switch(req.method) {
        case "POST":
            return await postInquiry(req, res);
        case "GET":
            return await getInquiries(req, res);
        default:
            return res.status(405).json({message: "Method not allowed"})
    }
}
作成したAPIを使って画面表示します
今回は Tailwind CSS を使用してスタイルを設定するので、その準備をします
$ npm install -D tailwindcss postcss autoprefixer $ npx tailwindcss init -p
tailwind.config.jsを編集
module.exports = {
    mode: "jit",
    content: [
        "./src/**/*.{js,ts,jsx,tsx}",
    ],
    theme: {
        extend: {},
    },
    plugins: [],
}
続いてデータ取得ライブラリの SWR をインストールします
$ npm install swr
src/pages/index.tsx を以下のように変更します
主に以下の処理をしています
- 先ほど作成したデータ取得 API を呼び出し
- 取得したデータを使用してテーブルのデータ行部分をレンダリング
import type { NextPage } from 'next'
import useSWR from 'swr'
import { Inquiry } from '@prisma/client'
type Inquiries = Array<Inquiry>
const fetcher = (url: string) => fetch(url).then(res => res.json())
const Home: NextPage = () => {
  const {data, error} =  useSWR<Inquiries>("/api/inquiries", fetcher);
  if (error) return <div>Error has oocurred</div>
  if (!data) return <div>Now loading...</div>
  return (
    <div className="container mx-auto">
      <table className="w-full text-sm text-left text-gray-500 mt-3">
        <thead className="text-xs text-gray-700 bg-gray-50">
          <tr>
            <th className="py-3 px-6">Name</th>
            <th className="py-3 px-6">Email</th>
            <th className="py-3 px-6">Subject</th>
            <th className="py-3 px-6">Message</th>
          </tr>
        </thead>
        <tbody>
          {data.map((inquiry) => {
            return (
              <tr className="bg-white border-b" key={inquiry.id}>
                <td className="py-4 px-6">{inquiry.name}</td>
                <td className="py-4 px-6">{inquiry.email}</td>
                <td className="py-4 px-6">{inquiry.subject}</td>
                <td className="py-4 px-6">{inquiry.message}</td>
              </tr>
            )
          })}
        </tbody>
      </table>
    </div>
  )
}
export default Home
作成したAPIを使って画面から登録できるようにします
以下のコードを src/pages/index.tsxに書き加えます。
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [subject, setSubject] = useState("");
const [message, setMessage] = useState("");
const clearForm = () => {
  setName("");
  setEmail("");
  setSubject("");
  setMessage("");
}
const {data, error, mutate} =  useSWR<Inquiries>("/api/inquiries", fetcher);
const onSubmit = async (e: React.SyntheticEvent) => {
  e.preventDefault();
  const response = await fetch("/api/inquiries", {method: "POST", body: JSON.stringify({
    name, email, subject, message
  }),
  headers: {
    "Content-Type": "application/json"
  }})
  clearForm();
  mutate();
}
SWRはローカルにキャッシュを持つので、更新処理後に即座に画面に反映させるには、mutate 関数を使って、SWR のローカルキャッシュをクリアし、再度データを取得させる必要があります
const {data, error, mutate} =  useSWR<Inquiries>("/api/inquiries", fetcher);
mutate();
src/pages/index.tsxに以下をフォームを書き加えます
<form action="" className="bg-white rounded px-8 shadow-md mt-3 p-3" onSubmit={onSubmit}>
  <div className="mb-4">
    <label className="block text-gray-700 text-sm font-bold mb-1">
      Name
    </label>
    <input onChange={(e) => setName(e.target.value)} type="text" value={name} className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700" />
  </div>
  <div className="mb-4">
    <label className="block text-gray-700 text-sm font-bold mb-1">
      Email
    </label>
    <input onChange={(e) => setEmail(e.target.value)} type="text" value={email} className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700" />
  </div>
  <div className="mb-4">
    <label className="block text-gray-700 text-sm font-bold mb-1">
      Subject
    </label>
    <input onChange={(e) => setSubject(e.target.value)} type="text" value={subject} className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700" />
  </div>
  <div className="mb-4">
    <label className="block text-gray-700 text-sm font-bold mb-1">
      Message
    </label>
    <textarea onChange={(e) => setMessage(e.target.value)} value={message} className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700" />
  </div>
  <div className="flex items-center justify-between">
    <button className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">Post</button>
  </div>
</form>
アプリケーションを Vercel にデプロイ
最後に今回作成したアプリケーションを Vercel で動かしてみます
PlanetScale でデータベース接続情報を取得
PlanetScale でブランチを選択し、「Connect」ボタンを押します

「New Password」 をクリックしデータベースへ接続するためのパスワードを生成し、 「Connect With」に Prismaを選択して、データベース接続文字列を表示させます

コードを Github に Push
Vercel にデプロイできるようにするため、今回作成したコードを Github に Push します
$ git add -A $ git commit -m "initial commit" $ git push origin main
Vercel にアカウントを作成
Vercel にアカウントを作成します。
Vercel にデプロイするリポジトリをインポート
今回は Github のリポジトリをデプロイするため、Githubからリポジトリをインポートします

今回作成したリポジトリを選択します

プロジェクト設定の Environment Variables に DATABSE_URL という名前で、先ほど PlanetScale で表示したデータベース接続情報文字列 mysql://で始まる部分を抜き出し設定します
そのあと「Deploy」をクリックします

デプロイしたプロジェクトのドメインにアクセスし

画面が見えれば完了です











