sawata0324
@sawata0324 (Sawata)

Are you sure you want to delete the question?

Leaving a resolved question undeleted may help others!

Next.js14でRailsAPIのJsonデータをRouteHandlerを使ってフェッチしたい。

解決したいこと

現在バックにRails、フロントにNext.jsとReactを使用しSPA化したメディア系Webアプリケーションを作成しております。

RailsAPIのusersのJsonデータをNext14のRouteHandlerを使ってフェッチしたいと思っているのですが、Railsサーバーは問題なく稼働しているのにroute.jsのデータ取得が失敗してしまいます。
現在はユーザー一覧表示機能でCSRを使って取得しているデータをSSRに変更しようとしています。
そこでSSRを行うためにRouteHandlerを使用した方法で行おうと考えたのですが、うまくいきません。

本来であればlocalhost:3000/api/v1/usersのデータのフェッチに成功してlocalhost:8000/api/usersにJson形式でデータが表示されるはずだと思ったのですが、そうなりません。

CSRでのデータ取得はうまく行っているのと、普通にlocalhost:3000/api/v1/usersでデータは閲覧できるので、Rails側やCOSEの問題ではないと思っています。
公式ドキュメントやZen、Qiitaの投稿などを調べてみたのですが、RailsSPAでAppRouterを使用している人で、RouteHandlerを使用している人が見つからず、行き詰まってしまいました。

まだ初学者なので、根本的なところで何かを勘違いしているのかもしれません。
なにか解決策かヒントをご教授いただけないでしょうか。

発生している問題・エラー

コンソールログ

users:1 
 Failed to load resource: the server responded with a status of 404 (Not Found)

具体的には以下のコードでhttp://localhost:8000/api/users にてデータフェッチを確認できません。

route.js
import { NextResponse } from "next/server"

// export const GET = () => {
//     return NextResponse.json({ hello: "hello" })
// }

export async function GET() {
    try {
        const res = await fetch('http://localhost:3000/api/v1/users');

        if (!res.ok) {
            return NextResponse.json({ error: 'Failed to fetch users data' }, { status: res.status });
        }

        const data = await res.json();

        if (!data) {
            return NextResponse.json({ error: 'No data returned from the API' }, { status: 500 });
        }

        return NextResponse.json({ data });
    } catch (error) {
        console.error('Error fetching users data:', error);
        return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
    }
}

上記の際のブラウザには以下の出力がなされます。

{"error":"Failed to fetch users data"}

現在のCSRでのユーザ一覧ページは以下のように記述されています。

app/users/page.jsx
"use client";
import { useEffect, useState } from "react";
import axios from "axios";

const Users = () => {
  const [users, setUsers] = useState([]);
  const [error, setError] = useState("");
  const [currentPage, setCurrentPage] = useState(1);
  const [totalPages, setTotalPages] = useState(1);

  useEffect(() => {
    const getUser = async () => {
      try {
        const res = await axios.get(
          `http://localhost:3000/api/v1/users?page=${currentPage}`,
          {
            withCredentials: true, // クッキーを含める設定
          }
        );

        if (res.data && Array.isArray(res.data.users)) {
          setUsers(res.data.users);
          setTotalPages(res.data.total_pages);
        } else {
          console.error("APIレスポンスは正しくありません:", res.data);
          setError("データの取得に失敗しました。");
        }
      } catch (err) {
        console.error("API呼び出しに失敗しました:", err);
        setError("データの取得に失敗しました。");
      }
    };

    getUser();
  }, [currentPage]); // currentPageが変わるたびに実行される

  const renderPageNumbers = () => {
    const pages = [];
    const maxPagesToShow = 5; // 表示するページ数を制限
    const halfWindow = Math.floor(maxPagesToShow / 2);
    let startPage = Math.max(currentPage - halfWindow, 1);
    let endPage = Math.min(currentPage + halfWindow, totalPages);

    if (startPage > 1) {
      pages.push(1);
      if (startPage > 2) {
        pages.push("...");
      }
    }

    for (let i = startPage; i <= endPage; i++) {
      pages.push(i);
    }

    if (endPage < totalPages) {
      if (endPage < totalPages - 1) {
        pages.push("...");
      }
      pages.push(totalPages);
    }

    return pages;
  };

  const handlePageClick = (page) => {
    if (page !== currentPage && typeof page === "number") {
      setCurrentPage(page);
    }
  };

  if (error) {
    return <div>{error}</div>;
  }

  return (
    <div className="p-4">
      {users?.map((user) => (
        <div key={user.id} className="bg-white shadow-md rounded-lg p-6 mb-4">
          <p className="text-lg font-semibold mb-2">名前: {user.name}</p>
          <p className="text-gray-600 mb-1">メールアドレス: {user.email}</p>
          <p className="text-gray-600 mb-1">ID: {user.id}</p>
          <p className="text-gray-600 mb-1">ポスト数: {user.posts_count}</p>
          <p className="text-gray-600">
            アカウント作成日: {new Date(user.created_at).toLocaleDateString()}
          </p>
        </div>
      ))}
      <div className="flex justify-center mt-4">
        {renderPageNumbers().map((page, index) => (
          <button
            key={index}
            onClick={() => handlePageClick(page)}
            className={`mx-1 px-3 py-1 border rounded ${page === currentPage ? "bg-blue-500 text-white" : "bg-white text-blue-500"}`}
          >
            {page}
          </button>
        ))}
      </div>
    </div>
  );
};

export default Users;

Githubリンク

コードはfeatureブランチにpushしてあります。

Nextのapp下のディレクトリ構成は以下です

app
├── api
│   ├── posts
│   └── users
│       └── route.js
├── data
│   └── background_image.png
├── diarys
│   ├── [id]
│   │   └── page.jsx
│   ├── components
│   │   ├── EditPostModal.jsx
│   │   ├── PostDelete.jsx
│   │   ├── PostInput.jsx
│   │   ├── PostPagination.jsx
│   │   └── PostView.jsx
│   └── page.jsx
├── edit-password
│   └── page.jsx
├── edit-profile
│   └── page.jsx
├── followers
│   └── page.jsx
├── following
│   └── page.jsx
├── globals.css
├── layout.js
├── mock
├── not-found.js
├── page.jsx
├── rooms
│   └── page.jsx
├── signup
│   └── page.jsx
├── user-profile
│   └── page.jsx
└── users
    └── page.jsx

自分で試したこと

ここに問題・エラーに対して試したことを記載してください。
・GPT4oに解決策を聞く
・docker-compose downからの再build,再up
・CORSの設定確認
・Railsログの確認(何も出力されない)
・console.logの設置と確認
・ThunderClientを使用してAPIエンドポイントにGetリクエストを送信(成功)
http://localhost:3000/api/v1/usersを普通に開く(アクセス成功)

・以下の記事を読んで学習

(下記はUdemyのCodeMafiaさんの2024React/Nextガイドの動画です)

0

1Answer

自分は「Next14のRouteHandlerを使ってフェッチ」とか全くわかりませんが・・・

the server responded with a status of 404 (Not Found)

404 応答が返ってきているということは、要求は Web サーバーまで届いて、Web サーバーで url に指定されたリソースを探したが、見つからなかったということです。(バックの Web アプリが動くところまで行ってない)

その原因はほとんどが指定した url が間違っているということです。そのあたりを確認してみてください。(過去ここのような Q&A サイトのやり取りで絶体間違ってないという質問者も多々いましたが、ほぼ全員間違ってました)

1Like

Comments

  1. app
    ├── api
    │   ├── posts
    │   └── users
    │       └── route.js
    

    @SurferOnWww さんのご指摘通り、urlが間違っているかと思います。
    apiディレクトリ配下にv1ディレクトリがありませんのでアクセスできないと思います。

    export async function GET() {
        try {
            const res = await fetch('http://localhost:3000/api/v1/users');
    
    useEffect(() => {
        const getUser = async () => {
          try {
            const res = await axios.get(
              `http://localhost:3000/api/v1/users?page=${currentPage}`,
              {
                withCredentials: true, // クッキーを含める設定
    
  2. @sawata0324

    Questioner

    ご回答ありがとうございます。
    前述したようにフロントとバックを分離しているので、フロントはサーバーが8000番ポート、バックは3000番ポートです。
    ポート番号が違う中で末尾のapi/v1/usersが変わってもなにも変わらないかと思います。
    試しにhttp://localhost:3000/api/users でのフェッチを行いましたが出力に変化はありませんでした。

    もしよろしければどのようにアクセスURLを確認すれば良いのか教えていただけたらと思います。

  3. フロントが 8000 でバックが 3000 だとするとドメインが異なるとみなされ、フロントからブラウザの fetch API でバックに要求をかけると、何も対応がされてなければ、 HTTP 404 エラー以前にクロスドメインの問題が出て、ブラウザからは要求すら出ないはずなんですけど、 どうなっているんですか?


    【訂正】

    間違っていたので訂正します。間違っていたのは上の文の打ち消し線の部分です。

    バックエンドが CORS 非対応でもフロントエンドから要求は出て応答は返ってきます。

    以下に、Chrome のディベロッパーツールのコンソールと Fiddler の画像を貼っておきます。

    CORS 非対応 URL 誤

    CORS無効URL誤り.jpg

    FiddelerCORS無効URL誤り.jpg

    CORS 非対応 URL 正

    CORS無効URL正.jpg

    FiddlerCORS無効URL正.jpg

    CORS 有効 URL 誤

    CORS有効URL誤り.jpg

    FiddlerCORS有効URL誤り.jpg

  4. @sawata0324

    Questioner

    申し訳ないのですが、確定的なところは私も自信がありません。
    サーバーサイドのRails APIサーバーが立ち上がっている状態でローカルホストのAPIでそちらの方にデータフェッチしているので、CSRでのデータフェッチがうまく行っているのかなと思います。

    docker-compose.ymlは以下の記述をしております。
    またGithubリンクも立ち上げているのでご参照いただければと思います。

    services:
      db:
        image: mysql:8.0
        command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
        environment:
          MYSQL_ROOT_PASSWORD: password
        ports:
          - 4306:3306
        volumes:
          - db_data:/var/lib/mysql
      backend:
        tty: true
        depends_on:
          - db
        build:
          context: ./back/
          dockerfile: Dockerfile
        ports:
          - 3000:3000
        volumes:
          - ./back:/back
          - gem_data:/usr/local/bundle
      frontend:
        build:
          context: ./front/
          dockerfile: Dockerfile
        volumes:
          - ./front/my-app:/usr/src/app
        command: "yarn dev"
        ports:
          - "8000:3000"
    volumes:
      db_data:
      gem_data:
    
    
  5. そう言われても自分は分かりません。

    最初の質問のコード、

    const res = await fetch('http://localhost:3000/api/v1/users');
    

    で、404 Not Found 応答が返ってきて、その結果、

    if (!res.ok) {
       return NextResponse.json({ error: 'Failed to fetch users data' }, { status: res.status });
    }
    

    で !res.ok が true になり、

    上記の際のブラウザには以下の出力がなされます。
    {"error":"Failed to fetch users data"}

    ということになったのではないかと想像してますが、違いますか?

    違わなければ、fetch の引数の 'http://localhost:3000/api/v1/users' が間違っている(その url で指定されるリソースは見つからない)ということになると思いますが。

  6. @sawata0324

    Questioner

    ブラウザに出力される工程はその通りかと思います。

    HTTP 404 エラー以前にクロスドメインの問題が出て
    

    と言う点で、クロスドメインの問題についての知見が私にはないので確定的なことは言えないと伝えました。

    「404エラーが出るのはエラーハンドリングをしているから出る」のでしょうか・・・。
    もしよろしければどのようにアクセスURLを確認すれば良いのか教えていただけたらと思います。

  7. 上の私のコメントの

    HTTP 404 エラー以前にクロスドメインの問題が出て、ブラウザからは要求すら出ないはずなんですけど

    は間違ってました。

    バックエンド側での CORS 対応にかかわらず要求は出て 404 応答が返ってきます。

    詳しくは上の私のコメントの【訂正】を見てください。

    「404エラーが出るのはエラーハンドリングをしているから出る」のでしょうか・・・。

    その「エラーハンドリング」というのがバックエンドの Rails アプリでのハンドリングのことを言っているのであれば違うはずです。リソースが見つからなければアプリが動くところまで行かないはずですから。

    ただし、Rails アプリで、例えば DB にデータが見つからなければ 404 応答を返すというようなコードを書いていれば話は別ですが。

    どのようにアクセスURLを確認すれば良いのか

    それは自分には分かりません。Rails を使った Web API に詳しい人の回答をお待ちください。

  8. Next.jsにはRoute Handlersのほかに、サーバーコンポーネントから直接データフェッチする方法もあります。(サーバーコンポーネント自体が async でラップする形式で、その中でawaitを使える)

    const HolidayPage: NextPage<Props> = async ({ holidays }) => {
      const newYearsDay = dayjs("2023-01-01").format("YYYY/MM/DD");
      const holidays = await fetchHolidays();
    ...
    ..
    .
    

    Route Handlers以外のアプローチを試されてみても良いかもしれません。

    // This request should be cached until manually invalidated.
    // Similar to `getStaticProps`.
    // `force-cache` is the default and can be omitted.
    fetch(URL, { cache: 'force-cache' });
     
    // This request should be refetched on every request.
    // Similar to `getServerSideProps`.
    fetch(URL, { cache: 'no-store' });
     
    // This request should be cached with a lifetime of 10 seconds.
    // Similar to `getStaticProps` with the `revalidate` option.
    fetch(URL, { next: { revalidate: 10 } });
    
  9. @SurferOnWww さん

    バックエンド側での CORS 対応にかかわらず要求は出て 404 応答が返ってきます。

    500番系とかではなく404なのですね。勉強になりました。ありがとうございます。

  10. @sawata0324

    Questioner

    お二人とも詳細なご回答ありがとうございます。

    バックエンドが CORS 非対応でもフロントエンドから要求は出て応答は返ってきます。
    

    と言う箇所の説明は私には高度ですぐには理解できませんが、後々復習してみようかと思います。

    RouteHandlerを使わずSCを使った方法でのデータフェッチにも挑戦してみます。
    ありがとうございました。

  11. この先どう進めるつもりか分かりませんが、何にせよ、先のコメントで私が書いた、以下の件、

    最初の質問のコード、

    const res = await fetch('http://localhost:3000/api/v1/users');
    

    で、404 Not Found 応答が返ってきて

    ・・・というのが、

    ブラウザに出力される工程はその通りかと思います。

    ということであれば、先に進む前に、なぜ 404 応答になるのかをクリアにしないと、同じトラブルが出て悩むことになるような気がしますけど、いかがですか?

  12. @sawata0324

    Questioner

    @SurferOnWww
    現在、CSRでは'http://localhost:3000/api/v1/users' でデータ取得が一応できています。SSR自体は後回しにしても初期絵画の速度が遅くなるだけでプログラム自体は動くのでそのまま先に進めています。
    今後はgetServerSidePropsを用いた方法ではSSRを成功させている人もいるので、少し古い実装になってしまいますが、そちらでの実装も考えております。もしくはNext13で追加され@benjuwanさんが提案してくれたサーバーコンポーネントからの実装も検討中です。
    ひとまず今回はSSR化で疲弊してしまったところがあるので、また時間をおいて再挑戦をしようと思っています。

  13. ではこのスレッドはクローズ願います。

  14. @sawata0324

    Questioner

    クローズいたしました。

Your answer might help someone💌