Next.js(13/AppRouter)+Prisma(MySQL)+PandaCSSでTODOアプリを作る
今回は Next.js 13 の新機能 App Router をキャッチアップしたかったので、ついでに色々試してみました。
当方 pages ルーターでの実装や Prisma の経験は何度かあるので、その前提で記載します。
準備
検証時の環境は以下です。
$ node -v
v18.16.1
$ npm -v
9.5.1
$ yarn -v
1.22.19
またサンプルリポジトリは以下にあります。
Next.js のプロジェクト作成
まずは Next.js のプロジェクトを作成します。
2023/9/9 現在の最新版のcreate-next-appでは以下のような質問でした。
npx create-next-app@latest
✔ What is your project named? … my-app
✔ Would you like to use TypeScript? … No / Yes
✔ Would you like to use ESLint? … No / Yes
✔ Would you like to use Tailwind CSS? … No / Yes
✔ Would you like to use `src/` directory? … No / Yes
✔ Would you like to use App Router? (recommended) … No / Yes
✔ Would you like to customize the default import alias? … No / Yes
✔ What import alias would you like configured? … @/*
基本デフォルトですが、Tailwind CSS は No(Panda CSS 使いたかったので)、import aliasは Yes を選択しています。(好み)
Prisma 導入
Prisma はTypeScript(Node.js)でおそらく最もスタンダードな ORM パッケージです。
schema.prismaというファイルにスキーマを定義すると自動で Type Safe なクライアントコードが生成されます。
以下のコマンドでパッケージを追加します。
yarn add -D prisma
yarn prisma init
package.json に以下のスクリプトを追加しておきます。
...
"scripts": {
"dev": "rm -rf .next && next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"export": "next build && next export",
+ "db:migrate": "prisma migrate dev",
+ "db:migrate:reset": "prisma migrate reset",
+ "db:seed": "prisma db seed",
+ "db:studio": "prisma studio"
},
...
ついでに、prisma initをしたときに生成されるprismaというフォルダをsrcの下に移動させておきます。
prisma の cli に場所を教えてあげるために、package.json に以下の記載を追加しておきます。
},
+ "prisma": {
+ "schema": "src/prisma/schema.prisma",
+ }
}
MySQL の Docker 環境構築
Prisma はデフォルトで PostgreSQL の設定がされていますが、今回は MySQL を使います。
まず、ローカル DB として、以下のcompose.ymlでコンテナを用意しておきます。
version: '3.9'
services:
db:
image: mysql:latest
ports:
- "3306:3306"
environment:
MYSQL_ROOT_PASSWORD: mysql
MYSQL_DATABASE: db
MYSQL_USER: user
MYSQL_PASSWORD: password
restart: always
volumes:
- mysql:/var/lib/mysql
volumes:
mysql:
コンテナを起動します。
docker compose up -d
スキーマファイルを以下のように変更します。
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
.envファイルに接続設定がされていると思いますが、MySQL のものに書き換えます。
先程のcompose.ymlの定義の場合は、以下のようにしてやると動くと思います。
DATABASE_URL="mysql://root:mysql@localhost:3306/db"
PandaCSS 導入
今回は最近話題(?)の Panda CSS を使ってみることにしました。
Panda CSS は簡単に言うと、ゼロランタイムで型安全で RSC(React Server Component)に互換性のある CSS in JS ライブラリです。
Emotion や styled-components のようなランタイム CSS in JS が RSC では使えないため、create-next-appの際は、単なるCSSフレームワークであるTailwind CSS がデフォルトで選択されます。
後発であり、Chakra UI や Tailwind CSS の良いところが受け継がれているようなライブラリになっています。(Chakra UI のチームが開発しています。)
以下のコマンドでパッケージを追加して初期化します。
yarn add -D @pandacss/dev
yarn panda init --postcss
styled-systemというフォルダが生成されると思いますが、これが次回以降自動生成されるように package.json にスクリプトを追加しておきます。
...
"scripts": {
+ "prepare": "panda codegen",
"dev": "rm -rf .next && next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"export": "next build && next export",
"db:migrate": "prisma migrate dev",
"db:migrate:reset": "prisma migrate reset",
"db:seed": "prisma db seed",
"db:studio": "prisma studio"
},
...
.gitignore に以下を追加して、
# panda CSS
styled-system
path alias にも追加しておきます。
{
"compilerOptions": {
...
"paths": {
"@/*": ["./src/*"],
+ "@/styled-system/*": ["./styled-system/*"]
}
},
...
最後にglobal.css を以下のように書き換えておきます。
@layer reset, base, tokens, recipes, utilities;
参考
以上で準備としては完了です。
サンプルのプロジェクトではswrとreact-hook-formも使っているので、以下でインストールしました。
yarn add swr react-hook-form
アプリケーションコードの解説
アプリケーションの環境構築は終わったので実際に記述していきます。
Prisma を使って型定義する
まずはスキーマ定義します。
技術キャッチアップするときに作るアプリは簡単な Todo アプリと決めている(謎)ので、今回も Todo アプリでいきます。
簡単に Todo モデルだけ定義しておきました。
model Todo{
id Int @id @default(autoincrement())
title String
description String
status Boolean @default(false)
}
以下のコマンドで DB を作成し、マイグレーションを行います。
yarn db:generate
yarn db:migration
migrationsフォルダにマイグレーションファイルが作成され、マイグレーションが実行されます。
アプリケーションで使うためのクライアントも定義しておきます。
yarn add @prisma/client
import { PrismaClient } from "@prisma/client";
export * from "@prisma/client";
export const prisma = new PrismaClient({
log: ["query", "info", "warn"],
});
こうしておくことで、prismaを ORM として使うことができます。
// todosは自動的にTodo[]の型がついている
const todos = await prisma.todo.findMany();
PandaCSS でのスタイリング
Panda CSS でのスタイリングは今回そこまでちゃんと使えていませんが、例えば以下のように記述することができます。
import { css } from "@/styled-system/css";
export default function Home() {
return (
<div
className={css({
fontSize: "2xl",
fontWeight: "bold",
textAlign: "center",
})}
>
Todo App
</div>
)
}
自動生成されるstyled-systemというフォルダにcss が export されていますので、それを使って css の class を直接定義できます。
型補完が効くので思っているよりもコーディングは楽です。
その他にstyled-system/patternsの中にいくつかよく使うパターンが入っていて、これを使うと記述量が減らせて便利そうです。
App Router を使う
Next.js 13 では App Router という機能が追加され、13.4 で stable となりました。
追加されてから暫く経つので、詳細な説明は公式ページなどを参考にしていただいたり、他の記事などを調べていただくのが良いと思います。
ディレクトリベースのルーティングである点は pages ルーターと同じですが、ファイル名に特別な意味を持つようになりました。
最初から定義されているファイルでいうと、layout.tsxに共有のレイアウトを記載して、page.tsxに実際のページを記載します。
その他、pages ルーターでは api を作成する場合はpages/apiの下にファイルを置いていましたが、App Router ではroute.tsというファイルを置くことで api を作成できるようになりました。
詳細は以下で確認してください。
また App Router になって大きく変わった点が RSC(React Server Components) がデフォルトであるということです。
React Server Components とは文字通り、サーバー側でコンポーネントの生成を行うものです。
useStateやuseEffectなどの再描画系の処理は RSC では書けないのと、Emotion に依存している UI ライブラリも使えないため注意が必要です。
例えばサンプルアプリでは以下のコンポーネントが RSC です。
import { prisma } from "@/prisma";
import Link from "next/link";
const getTodo = async (id: string) => {
const todo = await prisma.todo.findUnique({ where: { id: parseInt(id) } });
return todo;
};
export default async function Todo({ params }: { params: { id: string } }) {
const { id } = params;
const todo = await getTodo(id);
return (
<>
<h1>Todo detail</h1>
<p>id: {id}</p>
<p>title: {todo?.title}</p>
<p>description: {todo?.description}</p>
<Link href="/">戻る</Link>
</>
);
}
getServerSidePropsではない場所で prisma のコードが実行できています。(NEXT_PUBLIC_でない環境変数がちゃんと読めている。)
またクライアントコンポーネントを定義したいときはtsxファイル内に、use clientと書きます。
"use client";
import { useGetTodos } from "@/hooks/useGetTodos";
import { css } from "@/styled-system/css";
import { hstack } from "@/styled-system/patterns";
import { useForm } from "react-hook-form";
export default function TodoForm() {
const { handleSubmit, register, reset } = useForm<{ title: string }>();
const { mutate } = useGetTodos();
return (
<form
onSubmit={handleSubmit(async (value) => {
const res = await fetch("/api/todo", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(value),
});
console.log(res);
reset();
mutate();
})}
>
<div className={hstack({})}>
<input
type="text"
className={css({
height: "40px",
width: "100%",
padding: "8px",
borderRadius: "4px",
border: "none",
boxShadow: "0 0 0 1px rgba(0,0,0,0.1)",
})}
placeholder="Enter your todo"
{...register("title")}
/>
<button
type="submit"
className={css({
display: "flex",
borderWidth: "1px",
borderColor: "gray",
color: "gray",
padding: "8px",
fontSize: "12px",
})}
>
Add
</button>
</div>
</form>
);
}
これを RSC から呼ぶ事もできます。
import TodoForm from "@/components/TodoForm";
import TodoList from "@/components/TodoList";
import { css } from "@/styled-system/css";
import { vstack } from "@/styled-system/patterns";
export default function Home() {
return (
<main>
<div
className={css({
fontSize: "2xl",
fontWeight: "bold",
textAlign: "center",
})}
>
Todo App
</div>
<div className={vstack({ padding: "8" })}>
<TodoForm /> {/* Client Component */}
<TodoList /> {/* Client Component */}
</div>
</main>
);
}
App Router で API を作る
API は Route Handler というものを使うことになります。
GETやPOSTなどの名前で関数を定義してやれば、それがディレクトリベースのルーティングに沿ってエンドポイントとして作成されます。
import { prisma } from "@/prisma";
import { NextResponse } from "next/server";
export async function GET(_: Request, { params }: { params: { id: string } }) {
const id = Number(params.id);
const todo = await prisma.todo.findUnique({ where: { id } });
return NextResponse.json({ todo });
}
以下のように GET,POST など、複数の method に対して同時に定義することもできます。
import { prisma } from "@/prisma";
import { NextResponse } from "next/server";
export const dynamic = "force-dynamic";
export async function GET(_: Request) {
const todos = await prisma.todo.findMany();
return NextResponse.json({ todos });
}
export async function POST(request: Request) {
const body = await request.json();
const todo = await prisma.todo.create({
data: {
title: body.title,
description: "",
status: false,
},
});
return NextResponse.json({ todo });
}
途中export const dynamic = "force-dynamic";という記述がありますが、検証時点ではPOSTなどでrequest.json()を呼ぶとそんなメソッドは無いというエラーが出てしまったので、この設定を記載しています。
所感
主に App Router のキャッチアップのためにこの技術スタックにしましたが、Panda CSS は情報も少なく(ChatGPT もまだ知らないらしい 🥹)、少し攻めすぎていて本番での採用は見送りかなぁと感じました 😭
最近話題のT3 StackではTailwind CSSやtRPC、NextAuthを使うようなので、これらを使った App Router での実装も試してみたいところです 👀
Discussion