はじめに
Vercelは5月の1日から5日間続けて新機能の発表を行なってます。
1日目にはVercel KVとVercel Postgres、Vercel Blobの3種類のStorage機能が発表されました。
この記事ではそのうちVercel Postgresについて焦点を当てて簡単なアプリケーションを作成してみます。
Vercel Postgresとは
Vercel PostgresとはVercel Functionやフロントエンド用に構築された、Neonを搭載したサーバーレスSQLデータベースです。
提供されるPostgres SQLのバージョンは15となります。
Vercel Postgresのデータベースはコールドスタートに最大5秒間かかり、最後のアクセスから5分間アクセスされない場合は停止されます。
プラン
ProプランとHobbyプランからbeta版として利用することが可能で、どちらのプランからも1つのデータベースを無料で作成できます。Hobbyプランの場合1データベース1USDで追加で作成できます。Computing Timeなどの詳細な制限はこちらを参考にしてください。
利用できるORM
連携できるORMはkyselyまたは、prismaです。将来的にはdrizzleもサポートされるそうです。
利用可能リージョン
Neonで提供されているリージョンと同じところを選択できます。利用するEdgeやVercel Functionで指定しているリージョンと同箇所を選択することが推奨されています。
利用可能なリージョンは以下の通りです。
Region Code | Region Name | Location |
---|---|---|
cle1 | us-east-2 | Cleveland, USA |
iad1 | us-east-1 | Washington, D.C., USA |
pdx1 | us-west-2 | Portland, USA |
fra1 | eu-central-1 | Frankfurt, Germany |
sin1 | ap-southeast-1 | Singapore |
日本がないのが残念ですね。現段階ではSingapore🇸🇬が最も近いので、リージョンに制約がない場合はSingaporeを選ぶのが無難と考えられます。
簡単なアプリケーションを作成する
サンプルアプリケーションとしてToDoリストを作成します。このアプリケーションではタスクの閲覧と完了だけが行えるものとします。
準備
まず、シンプルなNextjsのアプリケーションを作成します。
pnpm create next-app
todo-list
というアプリケーションをTypeScriptとESLintとTailwind CSSを有効にして作成しました。src
ディレクトリの利用とApp Router
も利用して、import aliasは@/*
に設定しました。
そして、先にアプリケーションのコンポーネントを準備します。
まずはタスクの内容の表示と完了を行うためのTask
コンポーネントです。
import dayjs from "dayjs";
export default async function Task({ task }: { task: any }) {
return (
<div className="flex items-end justify-between bg-white p-4 border border-slate-200">
<div className="flex">
<TaskCompleted task={task} />
<h2 className="text-4xl font-bold text-gray-700 ml-3">
{task.title}
</h2>
</div>
<p className="ml-3 text-xl text-gray-700">
{dayjs(task.createdAt).format('YYYY/MM/DD')}
</p>
</div>
);
}
チェックボックスはクライアントコンポーネントとして分けて定義します。
'use client';
import { ReactEventHandler, useState } from "react";
export default function TaskComplete({ task }: { task: any }) {
const [checked, setChecked] = useState(task.completed);
const onChange: ReactEventHandler<HTMLInputElement> = async (e) => {
setChecked(e.currentTarget.checked);
};
return (
<input type="checkbox" onChange={onChange} checked={checked} />
);
}
DBの値を取得する箇所や、チェックボックスによるタスクの切り替えは後ほど行います。
次にそれらを一覧で表示するTaskBox
コンポーネントです。
import Task from "./Task";
export default async function TaskBox() {
const tasks = [{
id: 1,
title: 'Task 1',
completed: false,
}, {
id: 2,
title: 'Task 2',
completed: false,
}, {
id: 3,
title: 'Task 3',
completed: false,
}];
return (
<div className="bg-slate-300">
<h1 className="p-4 text-6xl font-bold text-gray-700">Task Box</h1>
{tasks.map((task) => {
{/* @ts-expect-error Async Server Component */}
return <Task key={task.id} task={task} />;
})}
</div>
);
}
そしてそれをpage.tsxに埋め込みます。
import TaskBox from '@/components/TaskBox'
import { Suspense } from 'react'
export default function Home() {
return (
<main>
<Suspense fallback={<></>}>
{/* @ts-expect-error Async Server Component */}
<TaskBox />
</Suspense>
</main>
)
}
現段階では定義されたtasks
の個数だけタスクを表示するだけのページです。layout.tsx
の余計な情報も削除します。
import './globals.css'
import { Inter } from 'next/font/google'
const inter = Inter({ subsets: ['latin'] })
export const metadata = {
title: 'ToDo App',
description: 'Example ToDo App by Vercel Postgres',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="ja">
<body className={inter.className}>{children}</body>
</html>
)
}
これで準備が完了です。
データベースを作成する
VercelのダッシュボードからStorageを選択し、Create Databaseボタンを押します。
Postgresを選択してデータベースの作成を開始します。
Beta版での利用についての条文に同意後、作成するテーブル名とリージョンを聞かれます。テーブル名はtodo-list
、リージョンは一番近いSingaporeを選択しました。
Createを押すとそのテーブルのダッシュボードが開きます。無事ダッシュボードが表示されたらデータベースの作成は完了です。
プロジェクトとデータベースを紐付ける
アプリケーションで先ほど作成したデータベースを利用できるようにします。
この記事ではVercelプロジェクトとの連携のためにvercelのcliを利用します。
vercelのcliは以下のようにインストールして利用できます。
pnpm i -g vercel@latest
vercel -v
で正常に導入できていることを確認してください。
導入できたら、アプリケーションとcliをリンクさせます。
vercel link
このコマンドを入力して表示される情報に従って設定を完了したら、Vercelにプロジェクトが作成されます。作成したプロジェクトのダッシュボードでStorageを選択して先ほど作成したデータベースと連携させます。
そして、以下のコマンドを実行してVercelが持つプロジェクトの環境変数を取得します。
vercel env pull .env
.gitignore
に.env
を追加してgitを用いた管理を行わないようにするのを忘れないようにしてください。
データベースの値を取得する
次はprismaを介してデータベースから値を取得します。
まずは準備です。
# 依存パッケージのインストール
pnpm i -D prisma
# prismaの準備
pnpm prisma init
作成された.env
は先ほどVercelから取ってきた値に戻してください。
さらに、生成されたschema.prisma
を以下のように書き換えます。
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
previewFeatures = ["jsonProtocol"]
}
datasource db {
provider = "postgresql"
url = env("POSTGRES_PRISMA_URL") // uses connection pooling
directUrl = env("POSTGRES_URL_NON_POOLING") // uses a direct connection
shadowDatabaseUrl = env("POSTGRES_URL_NON_POOLING") // used for migrations
}
model tasks {
id Int @id @default(autoincrement())
title String
completed Boolean @default(false)
createdAt DateTime @default(now())
}
このファイルでToDoリストのテーブル定義を行なっています。
定義したテーブル情報を以下のコマンドでVercel Postgresに反映させます。
prisma generate && prisma db push
次にprismaをclientで扱うためのファイルを作ります。
import { PrismaClient } from '@prisma/client'
declare global {
var prisma: PrismaClient | undefined
}
const prisma = global.prisma || new PrismaClient()
if (process.env.NODE_ENV === 'development') global.prisma = prisma
export default prisma
それを利用してテーブルの初期データを生成するseed.tsを作ります。
import prisma from '../src/lib/prisma'
async function main() {
const response = await Promise.all([
prisma.$executeRawUnsafe(`TRUNCATE TABLE tasks CASCADE;`),
prisma.tasks.createMany({
data: [
{
title: 'ゴミ捨て',
completed: true,
},
{
title: '宿題',
},
{
title: '世界征服',
completed: true,
},
{
title: 'Learn React',
},
],
}),
])
console.log(response)
}
main()
.then(async () => {
await prisma.$disconnect()
})
.catch(async (e) => {
console.error(e)
await prisma.$disconnect()
process.exit(1)
})
テーブルの初期値を簡単なコマンドで追加できるように、package.json
に下記を書き足します。
"prisma": {
"seed": "ts-node --compiler-options {\"module\":\"CommonJS\"} prisma/seed.ts"
},
ts-node
がインストールされていない場合はpnpm i ts-node
も行ってください。
完了後、下記コマンドを実行してテーブルに初期データを挿入します。
pnpm prisma db seed
初期データが挿入されていることはVercel Postgresのダッシュボードから確認できます。
最後に、next dev
とnext build
を行う前にprismaの準備を行うようにコマンドを書き換えます。
"dev": "prisma generate && next dev",
"build": "prisma generate && prisma db push && next build",
これでprismaとの連携は完了です。
コンポーネントの書き換え
先ほど作成したコンポーネントをデータベースと通信するように書き換えます。
まずは、TaskBox
です。
import prisma from '@/lib/prisma';
import Task from "./Task";
export default async function TaskBox() {
const tasks = await prisma.tasks.findMany({
orderBy: {
id: 'asc',
}
});
return (
<div className="bg-slate-300">
<h1 className="p-4 text-6xl font-bold text-gray-700">Task Box</h1>
{tasks.map((task) => {
{/* @ts-expect-error Async Server Component */}
return <Task task={task} key={task.id} />;
})}
</div>
);
}
tasks
をprisma経由でデータベースから取得したものに変更しました。
次に、チェックボックスを押した時にデータベースを更新するようにします。
まずは、データベースを更新するAPIを作ります。
import prisma from "@/lib/prisma";
import { NextResponse } from 'next/server';
export async function PATCH(
request: Request,
{
params,
}: {
params: { id: string };
},
) {
const req = await request.json();
const task = await prisma.tasks.update(
{
where: { id: Number(params.id) },
data: { completed: req.completed }
}
);
return NextResponse.json({ task });
}
次に、TaskCompleted
を書き換えます。
'use client';
import { tasks } from "@prisma/client";
import { ReactEventHandler, useState } from "react";
export default function TaskComplete({ task }: { task: tasks }) {
const [checked, setChecked] = useState(task.completed);
const onChange: ReactEventHandler<HTMLInputElement> = async (e) => {
const res = await fetch(`/api/tasks/${task.id}`, {
method: 'PATCH',
body: JSON.stringify({
completed: e.currentTarget.checked
})
}
);
setChecked((await res.json()).task.completed);
};
return (
<input type="checkbox" onChange={onChange} checked={checked} />
);
}
先ほど作成したAPIを用いてチェックボックスの切り替えと同時にデータベースの値も更新するようにしました。
これでコンポーネントの書き換えは完了です。
デプロイ
最後にこれまで作成したアプリケーションをVercel上にデプロイします。
vercel --prod