🦔

Next.jsのApp RouterとFirebase連携

2023/09/03に公開

Next.js v13からApp Routerが使えるようになりました。

以前のPage Routerと比較して書き方が変わっているのでコードベースで使い方を確認していきたいと思います。

今回利用した技術の一部は以下の通りです。

"next": "13.4.12",
"react": "18.2.0",
"typescript": "5.1.6",
"firebase-admin": "^11.10.1",
"axios": "^1.4.0",

今回はNext.jsのAPI機能を利用します。なので、firebase-adminを使ってバックエンドでFirebaseと連携しています。

フロントエンドのみでFirebaseと連携させたい場合はfirebaseを使えば良いです。

どちらのSDKもnpmからインストールできます

  • firebase: クライアント側で動作するSDK
  • firebase-admin: バックエンド側で動作するSDK

全体像

セットアップ

こちらの前提です。

  • Next.jsのプロジェクトを作成済み
  • Firebaseプロジェクトを作成済み
    • Authenticationでログインプロバイダ(メール/パスワード)が有効になっている
    • Cloud Firestoreでデータベースを作成済み

firebase-adminを使えるようにしたいので以下の操作を行います。

  1. 秘密鍵のダウンロードと配置

    1. Firebase コンソールで対象のプロジェクトを選択する。
    2. プロジェクトの設定 からサービスアカウント タブを開く。
    3. 新しい秘密鍵の生成 をクリックし秘密鍵 (jsonファイル) をダウンロード。
    4. Next.jsプロジェクトのルートフォルダに配置。
  2. SDKのインストール

    npm install firebase-admin
    
  3. Next.js側で初期化

    適当なファイル(firebase.tsなど)を作成してダウンロードした秘密鍵を使ってinitializeAppを行う。

    const { cert } = require("firebase-admin/app");
    const serviceAccount = require("../../board-slate-firebase-adminsdk-p15gz-350b19a43a.json");
    const admin = require("firebase-admin");
    if (admin.apps.length === 0) {
      admin.initializeApp({
        credential: cert(serviceAccount),
      });
    }
    
    const { getAuth } = require("firebase-admin/auth");
    export const auth = getAuth();
    
    const { getFirestore } = require("firebase-admin/firestore");
    export const db = getFirestore();
    

Firestoreにデータ追加・更新をしてみる

データ追加

postリクエストを送ってパラメータの値をplansというコレクションに追加してみます。

http method url API file path
POST /api/plan app/api/plan/route.ts

クライアント側でplanのフォームデータを用意してaxiosを使って送信する必要があるので、コンポーネントでhandleSaveのような関数を定義しpostリクエストします。

async function handleSave() {
  await axios.post("/api/plan", formValue);
  // 後続の処理 ステートの更新とか

フォームやformValueに値をセットする処理は省略します。

ちなみに、formValueの型はこんな感じです。

export interface IPlan {
  id: string;
  title: string;
  content: string;
  date: string;
  section_id: string;
}

サーバー側でリクエストを受ける必要があるので、app/api/plan/route.ts を作成しpostリクエストが来た際の処理を書きます。ファイル名は route.ts にするのがApp Routerでのルールです。

route.ts作成後はNext.jsを再ビルドしないと反映されないことがあるので注意です。

import { QueryDocumentSnapshot, QuerySnapshot } from "firebase-admin/firestore";
import { NextRequest, NextResponse } from "next/server";
import { db } from "../firebase";

const COLLECTION_NAME = "plans";

export async function POST(request: NextRequest) {
  const insertData = await request.json();
  const docRef = await db.collection(COLLECTION_NAME).add(insertData);
  return NextResponse.json({ ...insertData, id: docRef.id });
}

この状態で、実際にリクエスト送るとFirestoreにデータが追加されるはずです!

データの更新

今度は、plansの特定のドキュメントを更新したいと思います。

http method url API file path
PATCH /api/plan app/api/plan/route.ts

コンポーネントでは、対象のplanを取得して更新になると思うので最初にplanのデータをformValueにセットしておきます。

useEffect(() => {
  if (plan) {
    setFormValue(plan);
  }
}, [plan]);

更新なのでpatchリクエストを送るようにします。

async function handleSave() {
  await axios.patch("/api/plan", formValue);
  // 後続の処理 

APIは登録時とほぼ同じで、app/api/plan/route.ts に以下を追加します。今度は受け取るデータの id が入っているので、docで取得してupdateすれば更新できます。

export async function PATCH(request: NextRequest) {
  const updateData = await request.json();
  db.collection(COLLECTION_NAME).doc(updateData.id).update(updateData);
  return NextResponse.json(updateData);
}

この状態で、実際にリクエスト送ると対象データが更新されるはずです!

Firestoreからデータ取得してみる

クエリパラメータを使いたい

対象月の予定を取得するユースケースを考えます。APIにgetリクエストを送る際に対象月の値(2023-09とか)を渡す必要があるのでクエリパラメータを使う例を書いてみます。

http method url API file path
GET /api/plan?month=yyyy-dd app/api/plan/route.ts

コンポーネントで以下のようにリクエストするとします。

useEffect(() => {
  (async () => {
    const { data } = await axios.get(`/api/plan?month=${displayMonth}`);
    // 後続の処理
  })();
}, [displayMonth]);

APIのファイルは app/api/plan/route.ts でOKです。getリクエストが来た時の処理を書きます。

export async function GET(request: NextRequest) {
  const { searchParams } = new URL(request.url);
  const month = searchParams.get("month");
  const snapshot: QuerySnapshot = await db.collection(COLLECTION_NAME).where("date", ">=", `${month}-01`).where("date", "<=", `${month}-31`).get();
  const data = snapshot.docs.map((doc: QueryDocumentSnapshot) => ({ ...doc.data(), id: doc.id }));
  return NextResponse.json(data);
}

request.urlからURLインスタンスを生成しsearchParamsを取得することができ、この中にmonth(対象月)の値が入っています。whereを使ってデータを絞り込み取得したdocsを返してます。

monthの値に応じてplansコレクションの中から該当するドキュメントを取得できるはずです!

URLセグメントの値を使いたい

例えば、”api/section/abcd1234” というパスに対してリクエスト送った際に “abcd1234” という値を使いたいケースがあるかと思います。

http method url API file path
GET /api/section/xxxxx app/api/section/[organizationId]/route.ts

例として organizationId をキーにして sections というコレクションからデータ取得する例を見てみます。

コンポーネントでのリクストは以下のようにします。

const { data } = await axios.get(`/api/section/${organizationId}`);

ちなみに、sectionの型はこちらです。

export interface ISection {
  id: string;
  name: string;
  visible: boolean;
  organization_id: string;
}

APIは app/api/section/[organizationId]/route.ts を準備します。ファイル名は route.ts 固定で、フォルダーは動的にしたい部分を [] で囲みます。

getリクエストが来た時の処理がこのようになります。

const COLLECTION_NAME = "sections";

export async function GET(request: Request, { params }: { params: { organizationId: string } }) {
  const organizationId = params.organizationId;
  const snapshot: QuerySnapshot = await db.collection(COLLECTION_NAME).where("organization_id", "==", organizationId).orderBy("created_at").get();
  const data = snapshot.docs.map((doc: QueryDocumentSnapshot) => ({ ...doc.data(), id: doc.id }));
  return NextResponse.json(data);
}

GET関数のパラメータで params を定義することで簡単に取得できます。

これで organizationId をキーにして sections コレクションから該当のデータを取得できるはずです!

or条件で絞り込みたい

Firestoreは where を使ってクエリを作ることで取得するデータの絞り込みが可能ですが、andやorを使ってクエリを作るケースも良くあるかと思います。

今度は、organizationsコレクションからadmin_uidまたはmember_uidsをキーにしてデータ取得する例を見てみます。

http method url API file path
GET /api/organization/xxxxx app/api/organization/[uid]/route.ts

コンポーネントでのリクエストはこんな感じです。

const { data } = await axios.get(`/api/organization/${user?.uid}`);

このuidにはログインユーザーのidが入る想定です。

organizationの型はこんな感じです。

export interface IOrganization {
  id: string;
  name: string;
  admin_uid?: string;
  member_uids?: string[];
}

APIは app/api/organization/[uid]/route.ts にファイルを用意してGET関数を書きます。

const COLLECTION_NAME = "organizations";

export async function GET(request: NextRequest, { params }: { params: { uid: string } }) {
  const uid = params.uid;
  const snapshot: QuerySnapshot = await db
    .collection(COLLECTION_NAME)
    .where(Filter.or(Filter.where("admin_uid", "==", uid), Filter.where("member_uids", "array-contains", uid)))
    .limit(1)
    .get();
  const doc = snapshot.docs[0];
  if (!doc) {
    return NextResponse.json(null);
  }
  const data = { ...doc.data(), id: doc.id };
  return NextResponse.json(data);
}

firebase-adminで用意されているFilterクラスを使うことでor条件でクエリを作ることができます。

array-containsを使うことで"member_uids"というカラムの配列の値に指定したuidが含まれているかどうかを判定してくれます。

これで、ログインユーザーのuidに応じてadmin_uidまたはmember_uidsからデータを絞り込んで取得することができるはずです!

Authenticationでユーザー登録してみる

よくあるサインアップをする際にもAPI経由でユーザー登録が可能です。

http method url API file path
GET /api/authentication/signup app/api/authentication/signup/route.ts

サインアップをする際、コンポーネントでは以下のようにpostリクエストを送るとします。

const { data: userData } = await axios.post(`/api/authentication/signup`, { email, password });
if (userData.uid) {
  // 後続処理 サインインやリダイレクト
} else {
  // エラーが発生した場合
}

APIは app/api/authentication/signup/route.ts を用意してPOST関数を書きます。

import { NextRequest, NextResponse } from "next/server";
import { auth } from "../../firebase";

export async function POST(request: NextRequest) {
  const param = await request.json();
  const user = await auth
    .createUser({
      email: param.email,
      password: param.password,
    })
    .catch((error) => error);
  return NextResponse.json(user);
}

実際にリクエスト送ると、受け取ったemail, passwordを使ってAuthenticationにユーザーが追加されるはずです!

まとめ

Next.jsのApp Routerを使ってFirebaseと連携する方法についてコードベースで紹介しました。

APIの書き方がPage Routerと違っているので少し戸惑いますが、一度やってみると割と簡単だと感じます。

Next.jsのキャッチアップの参考になれば幸いです。

参考

🔽 App RouterでのRoutingについての説明です。

Building Your Application: Routing

🔽 Firebase Admin SDKの説明です。

サーバーに Firebase Admin SDK を追加する

🔽 Firebase Admin SDKをNode.jsで使う際の説明です。

API Reference  |  Firebase Admin SDK

Discussion