Next.jsのDBアクセスをAPI Routes経由からServer Actions経由に変更する
はじめに
筆者の開発しているNext.jsのプロジェクトで、DBアクセスの実装をAPI Routes経由からServer Actions経由に変更しました。変更して良かった点と変更前、変更後の実装を紹介します。
1. Server Actions経由に変更して良かった点
Server Actions経由に変更して良かった点は以下の通りです。これらの結果、実装量が減ったのが良かったと感じています。
- 初回のレンダーからAPIの応答が返ってくるまでのLoading...実装が不要になった
- DBから読み込んだ値を入れる変数がnon-nullableになって扱いやすくなった
- APIの応答をパースするためのZodのスキーマ定義が不要になった
- APIの例外処理が不要になった
2. 変更前の実装と変更後の実装
ここからは、変更前のAPI Routesを使った場合の実装と、変更後のServer Actionsを使った実装を紹介していきます。本記事では、説明を簡略化するために、DBからの値の読み出しのみを説明し、DBへの値の書き込み部分は省略しています。
/lib/prisma.tsの実装は変更前と変更後で共有です。/lib/prisma.tsの詳細については、筆者の以下の記事に記載されています。興味を持っていただいた方は、こちらの記事も是非読んでみてください。
2.1. 変更前のAPI Routesを使った実装
まずは、変更前のAPI Routesを使った実装を紹介します。
2.1.1. API Routesを使った場合のシステム構成
API Routesを使った場合のシステム構成は以下の通りです。フロントエンドの/app.tsxからAPIを経由してバックエンドのDBにアクセスしています。
2.1.2. API Routesを使った場合のディレクトリ構成
API Routesを使った場合のディレクトリ構成は以下の通りです。Next.jsの一般的なディレクトリ構成になっています。
Project/
├─ app/
│ ├─ api/route.ts
│ └─ page.tsx
├─ schema/schema.ts
└─ lib/prisma.ts
2.1.3. API Routesを使った場合の実装
API Routesを使った場合の実装は以下の通りです。
/app/api/route.tsでは、DBにアクセスするためのAPIを実装しています。
export async function GET(_: Request) {
const bestScore = await prisma.score.findFirst({
orderBy: { score: "desc" },
});
return new Response(JSON.stringify({ score: bestScore?.score || 0 }), {
status: 200,
headers: { "Content-Type": "application/json" },
});
}
/app/page.tsxでは、useEffectを使って初回のレンダー後にAPI経由でDBにアクセスしています。
APIのレスポンスのJSONはany型であるため、/app/schema/schema.tsを使ってパースしています。
bestScore
がnullableであるため、nullの間はLoading...を表示しています。
"use client";
import { useEffect, useState } from "react";
import { ScoreSchema } from "@/schema/schema";
export default function Home() {
const [bestScore, setBestScore] = useState<number | null>(null);
useEffect(() => {
(async () => {
const response = await fetch("/api/score", { method: "GET" });
if (!response.ok) {
return;
}
const json = await response.json();
const parsed = ScoreSchema.safeParse(json);
if(!parsed.success) {
return
}
setBestScore(parsed.score);
})();
}, []);
return (
<div>
{bestScore
? `World Best Score: ${bestScore}`
: "Loading..."}
</div>
);
}
import { z } from "zod";
export const ScoreSchema = z.object({
score: z.number().min(0),
});
2.2. 変更後のServer Actionsを使った実装
次に、変更後のServer Actionsを使った実装を紹介します。
2.2.1. Server Actionsを使った場合のシステム構成
Server Actionsを使った場合のシステム構成は以下の通りです。フロントエンドの/app.tsxからServer Actionsを経由してバックエンドのDBにアクセスしています。
2.2.2. Server Actionsを使った場合のディレクトリ構成
Server Actionsを使った場合のディレクトリ構成は以下の通りです。Next.jsの一般的なディレクトリ構成になっています。
Project/
├─ app/
│ ├─ actions.ts
│ └─ page.tsx
├─ components/client.tsx
└─ lib/prisma.ts
2.2.3. Server Actionsを使った場合の実装
Server Actionsを使った場合の実装は以下の通りです。
/app/actions.tsでDBにアクセスするためのServer Actionsを実装しています。
"use server";
export async function readBestScore() {
const bestScore = await prisma.score.findFirst({
orderBy: { score: "desc" },
});
return { score: bestScore?.score || 0 };
}
/app/page.tsxでは、Server Actionsを使ってDBにアクセスし、取得した値を/app/components/client.tsxに渡しています。
変更前の/app/page.tsxはクライアントコンポーネントでしたが、変更後の/app/page.tsxはサーバーコンポーネントになっています。
export const dynamic = "force-dynamic";
を指定することで、キャッシュを行わずに、毎回最新の値をDBから取得することができます。
export const dynamic = "force-dynamic";
import Client from "@/components/Client";
import { readBestScore } from "./actions";
export default async function Home() {
const bestScore = await readBestScore();
return (<Client bestScore={bestScores} />);
}
変更前はnullableであったbestScore
をnon-nullableにすることができています。
"use client";
import { useState } from "react";
interface Props {
bestScore: { score: number };
}
export default function Client(props: Props) {
const [bestScore, setBestScore] = useState<number>(
props.bestScore.score
);
return (
<div>
{`World Best Score: ${bestScore}`}
</div>
);
}
筆者のWebアプリ
記事を読んでいただいてありがとうございました。今回は、Next.jsのプロジェクトで、DBアクセスの実装をAPI Routes経由からServer Actions経由に変更する方法を紹介しました。この記事の方法で、Server Actionsを使ってDBの世界最高得点の読み書きを行っている筆者のアプリは以下です。
以下のリンクから遊べますので、是非世界最高得点の更新を目指してみてください!
Discussion