Next.js から Prisma ORM を利用する
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.json
で next-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.db
と prisma/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.tsx
の getServerSideProps()
で、 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 による環境変数管理
Discussion
Pick
の Keys にタイポがあります。。