Create Branch

Next.js app/apiの使い方

Published on 2024年11月15日

Next.js

はじめに

外部連携のAPIと独自実装のAPIの実装の違い・管理方法について思考の整理をするためにこの記事を書きました。

結論

  • 外部APIを連携する場合、app/apiを介さずに下記のディレクトリ構成の場合 、feature配下にAPI実行するためのメソッドを定義、Hooks配下で呼び出し、page.tsxから参照して実行する。
  • 独自のAPIを実装する場合、app/api配下にAPIを処理内容を実装して、上記の流れとあとは同様

ディクトリ構成

├── 📂 apps
│   ├── 📂 api
│         └── 📂 Blog
│             └── 📄 route.ts
│   ├── 📄 page.tsx
│   └── 📂 blog
│       └── 📂 [id]
│           └── 📄 page.tsx
│
├── 📂 hooks     // featuresで定義されているAPIを参照してhooksで実行できるようように定義
│   └── 📂 blog
│       └── 📄 index.ts
│
├── 📂 features  // 外部連携しているAPIを定義
│   └── 📂 blog
│       └── 📄 index.ts
│
└── 📂 utils    // microCMSのClientを定義
    └── 📂 microcms
        └── 📄 client.ts

app/apiの認識

  • 独自のAPIを実装する場合、app/app配下にAPIを実装して、「API実装手順 - 詳細」の流れで実装

API実装手順 - 詳細

  1. app/api/blog/routes.ts
    • 独自のエンドポイントを実装したい時、上記のように任意のエンドポイント作成。
import { NextResponse, NextRequest } from 'next/server'

// リクエストカウントを保持するための変数
// Note: このアプローチはサーバーの再起動時にリセットされます
let responseCount = 0

// カウンターをインクリメントするための関数
const incrementCounter = () => {
    responseCount += 1
    return responseCount
}

export async function GET(request: NextRequest) {
    try {
        // リクエストカウントをインクリメント
        const currentCount = incrementCounter()

        return NextResponse.json({
            success: true,
            count: currentCount,
            message: 'Request processed successfully',
            timestamp: new Date().toISOString()
        })
    } catch (error) {
        // エラーハンドリング
        console.error('Error processing request:', error)
        return NextResponse.json(
            {
                success: false,
                error: 'Internal server error',
                message: error instanceof Error ? error.message : 'Unknown error occurred'
            },
            { status: 500 }
        )
    }
}

  1. features/blog/index.ts
  • 1で実装したエンドポイントをfeatures/blogで実行する処理を定義
    ※エンドポイントをfeatures/blog任意のディレクトリでOK、本サンプルではすでに’実装されている箇所で呼び出しているだけです。
import { client } from '@/utils/microCms/client';
import { Blog } from '@/types/blog';
import { GET as GetBLogsAPITest } from '@/app/api/blog/route';

const getBlogDetail = async (id: string) => {
    try {
        const data = await client.get({ endpoint: `blogs/${id}` });
        return data;
    } catch (error) {
        console.error('Error fetching blog:', error);
        throw error;
    }
};

const getBlogs = async () => {
    try {
        const data = await client.get({ endpoint: "blogs" });          
        return data.contents;
    } catch (error) {
        console.error('Error fetching blogs:', error);
        throw error;
    }
};


const getApiTest = async () => {
    try {
        const blogs = await GetBLogsAPITest('test' as any); // APIを呼び出し
        console
        const data = await blogs.json(); // レスポンスをJSONとして解析
        // console.log(data);
        return data.message;
    } catch (error) {
        console.error('Error fetching blogs:', error);
    }
};
    export { getBlogDetail, getBlogs, getApiTest };

  1. hooks配下でカスタムフックを作成
// hooks/blog/index.ts

import { useEffect, useState } from 'react';
import { getBlogs, getBlogDetail, getApiTest } from '@/features/blog';

export function useBlogs() {
    const [blogArticles, setBlogArticles] = useState([]);
    const [loading, setLoading] = useState(true);
    
    async function fetchBlogs() {
        try {
            const blogs = await getBlogs();
            setBlogArticles(blogs);
        } catch (error) {
            console.error('Error fetching blogs:', error);
        } finally {
            setLoading(false);
        }
    }
    
    useEffect(() => {
        fetchBlogs();
    }, []);
    
    return { blogArticles, loading };
}

export function useBlogDetail(id: string | string[]) {
    const [blog, setBlog] = useState(null);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState(null);
    
    async function fetchBlogDetail() {
        try {
            const data = await getBlogDetail(id as string);
            setBlog(data);
        } catch (error: any) {
            setError(error.message);
        } finally {
            setLoading(false);
        }
    }
    
    useEffect(() => {
        if (id) {
            fetchBlogDetail();
        }
    }, [id]);
    
    return { blog, loading, error };
}   


const useGetAPITest = async () => {
    try {
        const blogs = await getApiTest('test' as any); // APIを呼び出し
        console
        const data = await blogs.json(); // レスポンスをJSONとして解析
        // console.log(data);
        return data.message;
    } catch (error) {
        console.error('Error fetching blogs:', error);
    }
};

export { useBlogs, useBlogDetail, useGetAPITest };

  1. page.tsxで任意のカスタムHooksを参照して実行させる
'use client';

import { Header } from "@/components/organisms/header";
import { PageTitle } from "@/components/atoms/pageTittle";
import { CardList } from "@/components/organisms/cardList";
import { useBlogs, useGetApiTest } from "@/hooks/blog";
import { useEffect } from "react";

export default function Home() {
  const { blogArticles, loading } = useBlogs();
  const { APIResponse, GetAPILoading } = useGetApiTest();
  
  // データ取得の状態を監視
  useEffect(() => {
    if (loading || GetAPILoading) {
      console.log('データを取得中...');
      return;
    }

    if (blogArticles) {
      console.log('ブログ記事の取得完了:', blogArticles);
    }
    
    if (APIResponse) {
      console.log('API レスポンス:', APIResponse);
    }
  }, [loading, GetAPILoading, blogArticles, APIResponse]);
  
  return (
    <div className={styles.base}>
      <Header />
      <main className="flex flex-col gap-8 row-start-2 items-center w-11/12 max-w-6xl">
        <div className="md:flex flex-row gap-4 justify-between w-full">
          <PageTitle title="Blog" />
        </div>
        {loading ? (
          <p className="text-2xl font-semibold text-gray-500">Loading...</p>
        ) : blogArticles.length === 0 ? (
          <p className="text-2xl font-semibold text-gray-500">現在、記事が投稿されていません。</p>
        ) : (
          <CardList blogs={blogArticles} />
        )}
      </main>
    </div>
  );
}
hirotobeat