Shu Digital Works

Articles

投稿記事

Top > Articles > プログラミング > Next.js × Rails で API通信を共通化するBFF(Proxy)構成を実装する

Next.js × Rails で API通信を共通化するBFF(Proxy)構成を実装する

プログラミング API BFF Next.js Proxy Rails
公開日 更新日
Next.js × Rails で API通信を共通化するBFF(Proxy)構成を実装する

自作アプリではNext.js と Rails の間で、
似たような通信処理が何度も発生していました。
スクールの先生からproxyの概念を学んだので、
その仕組みを文章として整理しながら、
理解を深めたいと思います。

この構成では、Next.js のバックエンド部分が 、
BFF(Backend For Frontend) の役割を担っています。
Rails との間で認証や Cookie の処理、API 転送などを一手に引き受けることで、
コードを共有化し、シンプルにデータを扱えるようになります。
page.tsxやroute.tsの中のfetchからアクセスします。

リポジトリはこちら
https://github.com/shu915/digital_ichiba_frontend
https://github.com/shu915/digital_ichiba_backend

バージョン情報

フロントエンド

  • TypeScript 5.9.2
  • React 19.16
  • Next.js 15.5.0
  • Auth.js 5.0.0-beta.29
  • TailwindCSS 4.1.12

バックエンド

  • Ruby 3.3.9
  • Rails 8.0.2.1
  • Postgresql 16.10
  • Docker 28.4.0

処理の流れ

Next.js では、API通信をプロキシ経由でRailsに転送しています。
処理の全体像は次のようになります。

1. クライアントまたはサーバーから /api/* にリクエストが送信される

2. src/app/api/[…proxy]/route.ts が HTTPメソッドごとに handleProxyRequest を呼び出す

3. getProxyFlags.ts で JWTトークンや Cookie を付与する必要があるか判定する

4. handleProxyRequest.ts で Rails API へリクエストを転送し、レスポンスを処理する

この3つのファイルを使って、Next.jsが中継(Proxy)の役割を担っています。

ルートハンドラーでAPIリクエストを処理する

ファイルはsrc/app/api/[…proxy]/route.ts
このパスの意味はURLが/api/*のように、
URLが/api/のあとに続くすべてのリクエストを受け付ける、
という意味になります。
[…proxy]というのは、キャッチオールルートと呼ばれます。
来たすべてのものをproxyとして扱い、
処理をどのようにするかはroute.tsの中に記述します。
ファイルの中身は以下のようになります。

import { NextRequest } from "next/server";
import handleProxyRequest from "@/lib/handleProxyRequest";
export const dynamic = "force-dynamic";

/*
  GET, POST, PUT, DELETE, PATCH リクエストを受け取り、APIに転送する
*/
export async function GET(
  request: NextRequest,
  context: { params: Promise<{ proxy: string[] }> }
) {
  const params = await context.params;
  const proxyPaths = params.proxy;
  
  return handleProxyRequest(request, proxyPaths, "GET");
}

export async function POST(
  request: NextRequest,
  context: { params: Promise<{ proxy: string[] }> }
) {
  const params = await context.params;
  const proxyPaths = params.proxy;
  return handleProxyRequest(request, proxyPaths, "POST");
}

export async function PUT(
  request: NextRequest,
  context: { params: Promise<{ proxy: string[] }> }
) {
  const params = await context.params;
  const proxyPaths = params.proxy;
  return handleProxyRequest(request, proxyPaths, "PUT");
}

export async function DELETE(
  request: NextRequest,
  context: { params: Promise<{ proxy: string[] }> }
) {
  const params = await context.params;
  const proxyPaths = params.proxy;
  return handleProxyRequest(request, proxyPaths, "DELETE");
}

export async function PATCH(
  request: NextRequest,
  context: { params: Promise<{ proxy: string[] }> }
) {
  const params = await context.params;
  const proxyPaths = params.proxy;
  return handleProxyRequest(request, proxyPaths, "PATCH");
}


解説します。

import { NextRequest } from "next/server";
import handleProxyRequest from "@/lib/handleProxyRequest";

NextRequest はクライアントから、
送られてきたリクエスト情報(URL・ヘッダー・Cookie・リクエストボディなど)を、
扱うクラスです。

handleProxyRequestは、
ルートハンドラーの最後の方で呼び出して、
Railsに送る処理、帰ってきたデータの処理をします。

export const dynamic = "force-dynamic";

これを明示的に記述することで、
このファイルの中の処理では、
キャッシュをしないようにします。

export async function GET(
  request: NextRequest,
  context: { params: Promise<{ proxy: string[] }> }
) {
  const params = await context.params;
  const proxyPaths = params.proxy;
  
  return handleProxyRequest(request, proxyPaths, "GET");
}

ここでは、GET、POST、PUT、DELETE、PATCHについて、
同じような記述をしています。
これはルーティングでそれぞれのHTTPメソッドに対応しています。
route.tsの中で記述されるこのような関数をルートハンドラーと呼びます。
同じように処理するので、GETだけを解説します。

この関数は、クライアントからリクエストが送られたときに、
Next.js が自動的に生成する NextRequest インスタンスと、
context(ルート情報などを含むオブジェクト) を引数として受け取ります。
NextRequest は Web 標準の Request を拡張したクラスで、
リクエストURL・ヘッダー・Cookie などにアクセスできます。
context は、動的ルートのパラメータなどをまとめたオブジェクトで、
たとえば […proxy] の場合は context.params.proxy として取得できます。
api/[…proxy]なので、api/の後ろのurlを/で区切ったものが、
配列に入った状態で取得できます。
example.com/api/aaa/bbb/cccならば、
[“aaa”,”bbb”,”ccc”]のという配列が取得できます


request: NextRequest,

第一引数のrequestは先ほどインポートしたNextRequestクラスのインスタンスです。

context: { params: Promise<{ proxy: string[] }> }

第二引数のcontextはオブジェクトです。
型を確認すると、キーがparamsで、
バリューがPromiseに包まれたオブジェクトです。
Promiseの中には更にオブジェクトになっており、
キーはproxyで、
バリューは文字列型の配列です。
paramsのバリューがPromiseに包まれるようになったのはNext.js 15からです。

const params = await context.params;
const proxyPaths = params.proxy;

関数の処理部の中では、
awaitで context.paramsのPromiseを解決してparamsに代入します。
さらにparams.proxyをproxyPathsに代入します。
ここは複雑なので、
省略する書き方を避けました。

return handleProxyRequest(request, proxyPaths, "GET");

handleProxyRequestに対して、
request, proxyPaths , “GET”を渡して、
実行して返したものを更に返させます。
handleProxyRequestは、ルートハンドラーの中で呼び出され、
Railsに送信する処理、帰ってきたデータの処理をします。

APIリクエストのフラグを設定する

ファイルは、lib/getProxyFlags.ts
私の作っているアプリでは、
ログインしないと見れないようなページがあります。
また、情報を更新したらCookieを更新する必要があるページがあります。
1.バックエンドに署名付きJWTを送る必要あるのか。
2.帰ってきたデータをCookieに挿入する必要があるのか。
この2つで分岐を作ります。

export default function getProxyFlags(proxyPaths: string[], method: string) {
  let setJwtFlag = false;
  let setCookieFlag = false;
  const path = proxyPaths.join("/");

  switch (true) {
    case path === "shop" && method === "GET":
      setJwtFlag = true;
      break;

    case path === "shop" && method === "POST":
      setJwtFlag = true;
      setCookieFlag = true;
      break;
    
    //略
  }

  return { setJwtFlag, setCookieFlag };
}

解説します。

export default function getProxyFlags(proxyPaths: string[], method: string) {

第一引数として、
proxyPathsを文字列の配列として受け取ります。
exmaple.com/api/aaa/bbb/cccだと、
route.tsの方で処理して、
[“aaa”, “bbb”, “ccc”]になって、渡ってきます。

第二引数のはGETやPOSTなどのHTTPメソッドを引き取ります。

let setJwtFlag = false;
let setCookieFlag = false;

初期値としてsetJwtFlagもsetCookieFlagもfalseにして、
バックエンドJWTも、Cookieもセットしません。
必要に応じてswitch文で、trueに書きまえます。

const path = proxyPaths.join("/");

proxyPathsはapi/の後ろのものをスラッシュで区切ったものなので、
それをつなぎ直して、pathとします。

switch (true) {

内部のcaseの条件がtrueならば、
そのcaseの内部の処理を実行します。

署名付きJWTもCookieも必要ない場合は、
case文を書く必要がありません。
例えば、一般ユーザーがショップを閲覧するだけの場合
JWTもCookieの挿入も発生しないので、
何も記述しません。

case path === "shop" && method === "GET":
  setJwtFlag = true;
  break;

GETのapi/shopはショップオーナーが、
ショップを管理するRailsのルーティングに届くので、
バックエンドJWTは必要なのでtrueにします。
break;で他のcaseに入らないようにします。

case path === "shop" && method === "POST":
 setJwtFlag = true;
 setCookieFlag = true;
 break;

POSTの/api/shopのルーティングは、
ショップ管理人が、ショップ情報を更新したときに、
使うルーティングなので、
バックエンドJWTも必要であり、
更に帰ってきた新しいデータを、
Cookieに再挿入するので、setCookieもtrueにします。

このように必要に応じて、caseを追記していきます。

 return { setJwtFlag, setCookieFlag };

最後に呼び出したところに
setJwtFlagとsetCookieFlagを返します。
trueであれば、呼び出し元で、
JWTトークンやCookieのセットする処理をします。

RailsとのAPI通信を処理する関数

ファイルは、lib/handleProxyRequest.ts

import { NextRequest, NextResponse } from "next/server";
import getProxyFlags from "@/lib/getProxyFlags";
import createBackendJwtFromRequest from "@/lib/createBackendJwtFromRequest";

export default async function handleProxyRequest(
  request: NextRequest,
  proxyPaths: string[],
  method: string
) {
  try {    const { setJwtFlag, setCookieFlag } = getProxyFlags(
      proxyPaths,
      method
    );
    const endpoint = `/${proxyPaths.join("/")}`;
    const url = new URL(request.url);
    const apiUrl = `${process.env.RAILS_URL}/api${endpoint}${url.search}`;

    const contentType = request.headers.get("content-type") || "";
    const headers = new Headers();
    let body;
    if (["POST", "PUT", "PATCH"].includes(method)) {
      if (contentType.includes("multipart/form-data")) {
        body = await request.formData();
      } else {
        const json = await request.json().catch(() => null);
        if (json != null) {
          headers.set("content-type", "application/json");
          body = JSON.stringify(json);
        }
      }
    }

    if (setJwtFlag) {
      const jwt = await createBackendJwtFromRequest(request);
      headers.set("Authorization", `Bearer ${jwt}`);
    }

    // APIにリクエストを転送
    const response = await fetch(apiUrl, {
      method,
      headers,
      body,
      cache: "no-store",
    });

    // APIからのレスポンスを処理する
    const railsRes = await response.json();
    if (!response.ok) {
      return NextResponse.json(
        { message: "データの取得に失敗しました" },
        { status: response.status }
      );
    }

    const nextRes = NextResponse.json(railsRes, { status: response.status });

    if (setCookieFlag) {
      const diDataForCookie = {
        user: railsRes.user,
        shop: {
          id: railsRes.shop.id,
          name: railsRes.shop.name,
        },
      };

      const diData = JSON.stringify(diDataForCookie);

      nextRes.cookies.set("di_data", diData, {
        httpOnly: true,
        sameSite: "lax",
        secure: process.env.NODE_ENV === "production",
        path: "/",
        maxAge: 60 * 60 * 24 * 3,
      });
    }

    return nextRes;
  } catch (error) {
    console.error("API呼び出しエラー:", error);

    return NextResponse.json(
      { message: "サーバーとの通信中にエラーが発生しました" },
      { status: 500 }
    );
  }
}

handleProxyRequestという関数は、
src/app/api/[…proxy]/route.tsの中で呼び出され、
Railsに対する送信や、帰ってきたデータの処理を実行する関数です。
解説します。

import { NextRequest, NextResponse } from "next/server";
import { cookies } from "next/headers";
import getProxyFlags from "@/lib/getProxyFlags";
import createBackendJwtFromRequest from "@/lib/createBackendJwtFromRequest";

NextRequestとNextResponseをnext/serverからインポートします。
NextRequestは、
ルートハンドラーで受け取ったrequestをさらに、
handleProxyRequestに引数として渡すときの型として使います。

NextResponseは、
バックエンドから帰ってきたデータを処理するもので、
リダイレクトをさせたり、
Cookieにデータを挿入したりする役割を持っています。
ルートハンドラー中で使い、
最後にNextResponseをreturnすることで、
ルーティングの処理を完了させます。

cookiesをnext/headersからインポートします。
これはNext.jsのサーバーサイドから、
ブラウザにあるクッキーを操作するのに使います。
Next.js15から非同期になったので、気をつける必要があります。

先ほど作った、getProxyFlagsもインポートしておきます。

createBackendJwtFromRequestは、
リクエストが発生したときに、バックエンド用の署名付きJWTを発行する自作関数です。
こちらで詳細に解説しています。

export default async function handleProxyRequest(
  request: NextRequest,
  proxyPaths: string[],
  method: string

requestはルートハンドラーが引き取ったらrequestをそのまま入れます。
proxyPathsで、/api以降のパスを配列で引き取ります。
methodでHTTPメソッドを引き取ります。

try {

try-catch文を開始します。
エラーが出た場合中断せず、catchの中の処理に入るようにします。


const { setJwtFlag, setCookieFlag } = getProxyFlags(proxyPaths, method);

自作のgetProxyFlagsへ、proxyPathsとmethodを渡して、
中でcaseで分岐して、
setJwtFlagとsetCookieFlagを返してもらいます。
あとでtrueの時の処理を書きます。

const endpoint = `/${proxyPaths.join("/")}`;
const url = new URL(request.url);
const apiUrl = `${process.env.RAILS_URL}/api${endpoint}${url.search}`;

proxyPathsはapi/のあとの文字列の配列なので、
文字列に戻して、endpointに代入します。
これをやることで、Next.jsのバックエンドのルーティングに送ったものを、
そのままのパスで、Railsに送れるようになります。

new URL()を使って、request.urlからURLのインスタンスを作りurlに代入します。
urlはurl.searchとすることで、クエリパラメータを取ることができます。

process.env.RAILS_URLは、.env.localに乗っている、
RailsのURLです。

これらをつなげると、
Railsに対するfetchのURLになります。

const contentType = request.headers.get("content-type") || "";

page.tsxやroute.tsから、
Next.jsのproxyへのリクエストの、
headersからcontent-typeを取得します。
取得できない場合は空文字を入れます。

const headers = new Headers();
let body;

Next.jsのバックエンドから、
Railsのバックエンドへ渡すリクエストの
headersとbodyを定義します。

if (["POST", "PUT", "PATCH"].includes(method)) {

最初のif文で、page.tsxやroute.tsのfetchから渡ってきた、
methodの中にHTTPメソッドが入っているので、
“POST”, “PUT”, “PATCH”のリクエストならば、中の処理をするということです。

if (contentType.includes("multipart/form-data")) {
  body = await request.formData();

先程のcontentTypeが”multipart/form-data”の場合
画像やファイルが添付されているので、
bodyにrequest.formData()をawaitして代入します。
注意点として、formDataのときは、
RailsにfetchしたときContent-typeは自動で挿入されるので、
自分でheaders.set()を書かないようにします。


} else {
  const json = await request.json().catch(() => null);
  if (json != null) {
    headers.set("content-type", "application/json");
    body = JSON.stringify(json);
  }

dataFormではなくjsonが来た場合の処理です。
await request.json()で、jsonを取ってきて、json()でオブジェクト化します。
エラーがあった場合は、nullを返すようにします。
jsonをちゃんと取れた場合、
Railsにわたすリクエストとして
headers.set(“content-type”, “application/json”)でヘッダーをセットして、
JSON.stringify(json)でまた、文字列に戻して、bodyに代入します。
これでRailsへfetchするためのheadersとbodyができました。

if (setJwtFlag) {
  const jwt = await createBackendJwtFromRequest(request);
  headers.set("Authorization", `Bearer ${jwt}`);
}

ログインしないと見れないページでは、
lib/getProxyFlags.tsでsetJwtFlagをtrueにしています。
trueの場合、署名付きJWTを発行して、headersにセットします。
誰でも閲覧できるページはfalseなので、スルーします。
バックエンドJWTのロジックは、こちらで詳細に解説しています。

// APIにリクエストを転送
const response = await fetch(apiUrl, {
  method,
  headers,
  body,
  cache: "no-store",
});

Railsにfetchをします。
第一引数に組み立てたapiUrlを取ります。
第二引数はオブジェクトとして取ります。
methodはリクエストから来たものをそのまま代入、
headersとbodyは、先程作ったものを代入、
cacheはつくらせないようにします。

const railsRes = await response.json();
if (!response.ok) {
  return NextResponse.json(
    { message: railsRes.message },
    { status: response.status }
  );
}

Railsから帰ってきたresponseをjson()でオブジェクト化して、
railsResに代入します。
取るのに失敗した場合は呼び出し元にエラーを返すようにします。

const nextRes = NextResponse.json(railsRes, { status: response.status });

ここでNextResponseを作ります。
第一引数に、Railsから帰ってきたresponseをそのまま入れます。
第二引数に、ステータスをそのまま返します。

if (setCookieFlag) {

クッキー挿入フラグがある場合、
以下の処理をします。

const diDataForCookie = {
  user: railsRes.user,
  shop: {
    id: railsRes.shop.id,
    name: railsRes.shop.name,
  },
};

railsResから一部の情報だけをCookieに挿入するのは、
すべてを入れようとするとCookieのサイズをオーバーするからです。
userの情報は全て入れます。
shopはidとnameだけを入れます。

const diData = JSON.stringify(diDataForCookie);

オブジェクトになっているのでstringifyをして、
クッキーに挿入できる形にします。

nextRes.cookies.set("di_data", diData, {
  httpOnly: true,
  sameSite: "lax",
  secure: process.env.NODE_ENV === "production",
  path: "/",
  maxAge: 60 * 60 * 24 * 3,
});

nextResは、先程作ったNextResponseが入っているので、
cookies.setを使って、Cookieをセットします。
第一引数にキーを入れます。
第二引数にオブジェクトでいれます。
httpOnly: trueでJSから操作できないようにします。
sameSite: “lax”で、違うオリジンからPOSTなどをできないようにします。
本番環境では、httpsでないと扱えないようにします。
path: “/”とすることで、サイト全体で有効化します。
有効期限は3日です。

return nextRes;

nextResをreturnすることで、
このときにクッキーの上書きをします。
呼び出し元では、これをresponseとして受け止めます。

} catch (error) {
  console.error("API呼び出しエラー:", error);

  return NextResponse.json(
    { message: "サーバーとの通信中にエラーが発生しました" },
    { status: 500 }
  );
}

tryの中でエラーになった場合はこちらでキャッチします
コンソールにエラーを出力します。
また呼び出し元に、
メッセージとステータスを返します。

page.tsxやroute.tsから呼びだす

サーバーコンポーネントからのfetch

const res = await fetch(`${process.env.NEXT_URL}/api/shops/${id}`, {
  method: "GET",
  cache: "no-store",
});
const data = await res.json();
const shop: Shop = data.shop;

このようにfetchします
Next.jsのドメインを環境変数から取ってきて、URLをつなげます。
今回はGETします。
キャッシュはしません。
これで一般ユーザーからお店を閲覧できます。

クライアントコンポーネントからのfetch

"use client"

//略

const res = await fetch("/api/shop", {
  method: "POST",
  cache: "no-store",
});

このようにドメインをつけない、相対パスで書きます。

まとめ

Rails側のJWT認証処理については別記事にまとめており、
また今回の通信では特別な処理も必要ないため、
本記事ではRails側のコードは割愛しました。

Next.jsからRailsにAPI通信を行う際、
認証・Cookieの制御・エラーハンドリングなど、
毎回同じような処理が必要になります。
これらを 共通化するProxyの仕組みを作ることで、
クライアント側のコードを非常にシンプルに保つことができます

このように、フロントとバックの間にBFFを挟む設計は、
アプリ全体の保守性と可読性を高めるうえで非常に有効です。

関連記事

2025年6月の学習の振り返り

2025年6月の学習の振り返り

プログラミング

デザインの基礎とUI/UXとGo言語を学習。自分のメンテナンスをしました。

2025年7月の学習の振り返り

2025年7月の学習の振り返り

プログラミング

7月はGoの学習をしました。
自分で認証周りを作ることができました。

Next.js × Rails × React Hook Form × Zod で画像付きフォームを実装する

Next.js × Rails × React Hook Form × Zod で画像付きフォームを実装する

プログラミング

画像アップロード対応の商品登録フォームを実装。Zod のカスタムスキーマで型安全に。