Next.js から Prisma ORM を利用する

公開:2020/12/22
更新:2020/12/24
9 min読了の目安(約8100字TECH技術記事 1

Next.js に Prisma ORM を導入する方法について解説します。

Next.js プロジェクトの雛形を作成

$ mkdir hello-next-app && cd hello-next-app
$ npm init -y
$ npm install next react react-dom --save
$ npm install typescript @types/node @types/react --save-dev
$ code src/index.tsx

scr/index.tsx を作成

export default function Index() {
  return <div>index</div>;
}

一度 next build すると、 typescript の型定義が生成されるので、一回ビルド。

$ npx next build

ビルド対象に tsx があることで、 next が tsconfig.jsonnext-env.d.ts を生成します。

Prisma をセットアップ

依存に追加。

$ npm install @prisma/client@2 --save
$ npm install @prisma/cli@2 --save-dev

prisma ディレクトリにスキーマを追加します。今回は sqlite3 を選択。

prisma/schema.prisma

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "sqlite"
  url      = "file:./dev.db"
}

model Post {
  authorId  Int?
  content   String?
  id        Int     @id @default(autoincrement())
  published Boolean @default(false)
  title     String
  author    User?   @relation(fields: [authorId], references: [id])
}

model User {
  email String  @unique
  id    Int     @id @default(autoincrement())
  name  String?
  posts Post[]
}

VSCode でシンタックスハイライトするには、 Prisma - Visual Studio Marketplace を入れておくといいです。

migration

$ npx prisma migrate dev --preview-feature --name init
$ npx prisma generate # @prisma/client の型の生成

(このへんの API が頻繁に変わっていて、すぐに変わりそう)

migration が成功すると、 prisma/dev.dbprisma/migrations が生成されているはず。

$ tree prisma/
prisma/
├── dev.db
├── migrations
│   └── 20201222113842_init
│       └── migration.sql
└── schema.prisma

Next.js から Prisma を利用する

lib/prisma.ts で prisma client をインスタンス化。中で発行してる query を見たいので、 log オプションに query を指定。

import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient({
  log: ["query", "error", "info", "warn"],
});
export default prisma;

export * from "@prisma/client";

準備が整いました!

pages/index.tsxgetServerSideProps() で、 Prisma を叩いてみましょう。

import type { GetServerSideProps } from "next";
import prisma from "../lib/prisma";

type Props = {
  count: number;
};

export const getServerSideProps: GetServerSideProps<Props> = async (ctx) => {
  const count = await prisma.user.count();
  return {
    props: {
      count,
    },
  };
};

export default function Index(props: Props) {
  return <div>user count: {props.count}</div>;
}

getServerSideProps はサーバーで実行されるので、Dead Code Elimination によってクライアントに露出しません。

npx next でサーバーを起動して、ログを見ながら http://localhost:3000 を開いてみます。

user count: 0 と表示されたでしょうか?そのときのログがこちらです。

prisma:query SELECT COUNT(*) FROM (SELECT `dev`.`User`.`id` FROM `dev`.`User` WHERE 1=1 LIMIT ? OFFSET ?) AS `sub`

Post の投稿

validation に zod、 Form ライブラリとして react-final-form を使います。

$ npm install zod final-form react-final-form --save

title content を受け取って、prisma のレコードを生成する API Route を作成します。

pages/api/createPost.ts

import type { NextApiHandler } from "next";
import prisma from "../../lib/prisma";
import * as z from "zod";

const requestBodySchema = z.object({
  title: z.string().min(1),
  content: z.string(),
});

const handler: NextApiHandler = async (req, res) => {
  try {
    const result = requestBodySchema.parse(req.body);
    await prisma.post.create({
      data: {
        title: result.title,
        content: result.content,
        published: true,
      },
    });
    res.json({
      ok: true,
    });
    return;
  } catch (error) {
    res.json({ ok: false, error });
  }
};
export default handler;

この API に post する Form を react-final-form で作ります。

components/PostForm.tsx

import { useCallback } from "react";
import { Form, Field } from "react-final-form";
import { useRouter } from "next/router";

export function PostForm() {
  const router = useRouter();
  const onSubmit = useCallback(async (formData) => {
    const res = await fetch("/api/createPost", {
      method: "POST",
      body: JSON.stringify(formData),
      headers: {
        "Content-Type": "application/json",
      },
    });
    const json = await res.json();
    if (json.ok) {
      router.push("/");
    } else {
      alert(JSON.stringify(json));
    }
  }, []);
  return (
    <Form
      onSubmit={onSubmit}
      render={({ handleSubmit }) => {
        return (
          <form onSubmit={handleSubmit}>
            <Field<HTMLInputElement>
              name="title"
              placeholder="title"
              render={(props) => {
                return (
                  <div>
                    <input
                      {...(props.input as any)}
                      style={{ width: "80vw" }}
                    />
                  </div>
                );
              }}
            />
            <Field<HTMLTextAreaElement>
              name="content"
              placeholder="content"
              render={(props) => {
                return (
                  <div>
                    <textarea
                      {...(props.input as any)}
                      style={{ width: "80vw", height: "300px" }}
                    />
                  </div>
                );
              }}
            />
            <button type="submit">Submit</button>
          </form>
        );
      }}
    />
  );
}

一筆書きで雑な Form ですが、要は submit すると、次の API を実行します。

const res = await fetch("/api/createPost", {
  method: "POST",
  body: JSON.stringify(formData),
  headers: {
    "Content-Type": "application/json",
  },
});

ちょっと実装をサボっていて、再取得を API で実装するのが面倒なので、自分自身のページに再ルートして取得し直しています。

ここを真面目に作るなら、Post 一覧の取得を API 化して、 react-query でキャッシングすることになりそうです。

tannerlinsley/react-query: ⚛️ Hooks for fetching, caching and updating asynchronous data in React

index ページでは、Twitter みたいに現在のポスト一覧を表示してみます。

pages/index.tsx

import type { GetServerSideProps } from "next";
import React from "react";
import { PostForm } from "../components/PostForm";
import prisma, { Post } from "../lib/prisma";

type Props = {
  posts: Pick<Post, "id", "title" | "content">[];
};

export const getServerSideProps: GetServerSideProps<Props> = async (ctx) => {
  const posts = await prisma.post.findMany({
    select: {
      title: true,
      content: true,
      id: true,
    },
  });
  return {
    props: {
      posts,
    },
  };
};

export default function Index(props: Props) {
  return (
    <>
      <PostForm />
      <div>post count: {props.posts.length}</div>
      {props.posts.map((post) => {
        return (
          <div key={post.id}>
            <h3>{post.title}</h3>
            <p>{post.content}</p>
          </div>
        );
      })}
    </>
  );
}

サーバーを起動して、できたフォームから投稿してみます。

このときの内部で発行されてるクエリ

prisma:query BEGIN
prisma:query INSERT INTO `dev`.`Post` (`content`, `published`, `title`) VALUES (?,?,?)
prisma:query SELECT `dev`.`Post`.`id`, `dev`.`Post`.`authorId`, `dev`.`Post`.`content`, `dev`.`Post`.`published`, `dev`.`Post`.`title` FROM `dev`.`Post` WHERE `dev`.`Post`.`id` = ? LIMIT ? OFFSET ?
prisma:query COMMIT

以上です。この範囲では、そんなに難しくない感じですね。

なぜこれを書いたか

年末年始に向けて、自分は Next.js についての本を書いています。有料記事として zenn で販売するつもりです。この記事は、有料本の一部として、このテーマで書いたらどうなるか?というのを、試し書きしてみたものです。

その章の一つとして、フルスタックフレームワークの章を書こうとしていて、そのために、 Blitz.js や Next.js Follower を検証していました。

2021 年 は Fullstack Next.js 元年なので、有望な Next.js 系フレームワークを全部試した

やってみて思ったのが、prisma 以外に決定打と言えるものがない、という現状です。

最初は Blitz にフォーカスしようかと思っていたのですが、今日発表された React Server Component を使えば、 Blitz の isomorphism のようなものも比較的簡単に実装できそうで、 Blitz の良さの一つを Next.js 本体が取り込みそうな気がしています。あえて Blitz にロックインする必要はないように思いました。

Introducing Zero-Bundle-Size React Server Components – React Blog

とはいえ、Next.js + Prisma を普及してもいないので、この記事自体はまず無料で公開したほうがいいと思い投稿しました。

有料版では、これに加えて、次の要素を解説するつもりです。

  • docker-compose で postgres を立てて prisma から接続
  • react-query でレスポンスをキャッシュする
  • Amazon RDS に Postgres に用意する
  • hygen による isomorphic な API の作成
  • Vercel によるデプロイ時にマイグレーション
  • .env と Vecrel Secrets による環境変数管理