5
3

Vercel Postgresを使って簡単なToDoアプリケーションを作成する

Last updated at Posted at 2023-05-05

はじめに

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

スクリーンショット 2023-05-05 15.42.07.png

todo-listというアプリケーションをTypeScriptとESLintとTailwind CSSを有効にして作成しました。srcディレクトリの利用とApp Routerも利用して、import aliasは@/*に設定しました。
そして、先にアプリケーションのコンポーネントを準備します。
まずはタスクの内容の表示と完了を行うためのTaskコンポーネントです。

src/components/Task.tsx
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>
  );
}

チェックボックスはクライアントコンポーネントとして分けて定義します。

src/components/TaskComplete.tsx
'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} />
  );
}

スクリーンショット 2023-05-05 16.35.30.png
DBの値を取得する箇所や、チェックボックスによるタスクの切り替えは後ほど行います。
次にそれらを一覧で表示するTaskBoxコンポーネントです。

src/components/TaskBox.tsx
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に埋め込みます。

src/app/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>
  )
}

スクリーンショット 2023-05-05 17.07.00.png
現段階では定義されたtasksの個数だけタスクを表示するだけのページです。layout.tsxの余計な情報も削除します。

src/app/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ボタンを押します。
スクリーンショット 2023-05-05 17.30.48.png
Postgresを選択してデータベースの作成を開始します。
スクリーンショット 2023-05-05 17.31.57.png
Beta版での利用についての条文に同意後、作成するテーブル名とリージョンを聞かれます。テーブル名はtodo-list、リージョンは一番近いSingaporeを選択しました。
スクリーンショット 2023-05-05 17.34.32.png
Createを押すとそのテーブルのダッシュボードが開きます。無事ダッシュボードが表示されたらデータベースの作成は完了です。

プロジェクトとデータベースを紐付ける

アプリケーションで先ほど作成したデータベースを利用できるようにします。
この記事ではVercelプロジェクトとの連携のためにvercelのcliを利用します。
vercelのcliは以下のようにインストールして利用できます。

pnpm i -g vercel@latest

vercel -vで正常に導入できていることを確認してください。
導入できたら、アプリケーションとcliをリンクさせます。

vercel link

このコマンドを入力して表示される情報に従って設定を完了したら、Vercelにプロジェクトが作成されます。作成したプロジェクトのダッシュボードでStorageを選択して先ほど作成したデータベースと連携させます。
スクリーンショット 2023-05-05 17.59.46.png

スクリーンショット 2023-05-05 20.23.30.png

そして、以下のコマンドを実行してVercelが持つプロジェクトの環境変数を取得します。

vercel env pull .env

.gitignore.envを追加してgitを用いた管理を行わないようにするのを忘れないようにしてください。

データベースの値を取得する

次はprismaを介してデータベースから値を取得します。
まずは準備です。

# 依存パッケージのインストール
pnpm i -D prisma

# prismaの準備
pnpm prisma init

作成された.envは先ほどVercelから取ってきた値に戻してください。
さらに、生成されたschema.prismaを以下のように書き換えます。

prisma/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で扱うためのファイルを作ります。

src/lib/prisma.ts
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を作ります。

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に下記を書き足します。

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のダッシュボードから確認できます。
スクリーンショット 2023-05-05 22.53.49.png

最後に、next devnext buildを行う前にprismaの準備を行うようにコマンドを書き換えます。

package.json
"dev": "prisma generate && next dev",
"build": "prisma generate && prisma db push && next build",

これでprismaとの連携は完了です。

コンポーネントの書き換え

先ほど作成したコンポーネントをデータベースと通信するように書き換えます。
まずは、TaskBoxです。

src/components/TaskBox.tsx
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を作ります。

src/app/api/tasks/[id]/route.ts
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を書き換えます。

src/components/TaskCompleted.tsx
'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を用いてチェックボックスの切り替えと同時にデータベースの値も更新するようにしました。

これでコンポーネントの書き換えは完了です。

スクリーンショット 2023-05-05 23.19.32.png

デプロイ

最後にこれまで作成したアプリケーションをVercel上にデプロイします。

vercel --prod

でCLIからデプロイできます。GUIの操作でも可能です。
スクリーンショット 2023-05-05 23.20.06.png

5
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
3