🐟

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の詳細については、筆者の以下の記事に記載されています。興味を持っていただいた方は、こちらの記事も是非読んでみてください。

https://zenn.dev/zozooizozzoizio/articles/11a18cc945dad0

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を実装しています。

/app/api/route.ts
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...を表示しています。

/app/page.tsx
"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>
  );
}
/app/schema/schema.ts
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を実装しています。

/app/actions.ts
"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から取得することができます。

/app/page.tsx
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にすることができています。

/app/components/client.tsx
"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の世界最高得点の読み書きを行っている筆者のアプリは以下です。
以下のリンクから遊べますので、是非世界最高得点の更新を目指してみてください!

https://dice-z19i.vercel.app/

Discussion