言語を切り替える
テーマを切り替える

Next.js Server Components データ取得完全ガイド:fetch、DB直接取得、ベストプラクティス

Next.js App Router で初めてコンポーネントを書いた時、私はエディタの前でしばらくフリーズしました。

async function Page() {
  const data = await fetch('...')
  return <div>{data}</div>
}

「えっ、これだけ? いきなり await?」

useEffectuseState を使わず、競合状態(Race Conditions)も気にしなくていいの?

正直、最初は戸惑いました。React のクライアントサイドの流儀に慣れすぎていて、「コンポーネントが非同期関数になる」というのが、まるでルール違反のように感じられたのです。さらに悩ましいのが、「結局 fetch API を使うべきなのか、それとも直接データベースを叩くべきなのか?」という問題。fetch だと API 呼び出しが1層増えるし、DB 直接だとクライアントにキーが漏れないか不安になる…。

もしあなたも同じような疑問を持っているなら、この記事が答えになります。Next.js Server Components でのデータ取得の正解——いつ fetch を使い、いつ DB を叩くのか、async コンポーネントの正しい書き方、キャッシュ制御、そして避けるべき落とし穴について解説します。

Server Components データ取得の基礎

なぜ Server Components は直接 await できるのか?

答えはシンプルです:これらはブラウザではなく、サーバー上で実行されるからです。

当たり前に聞こえるかもしれませんが、この違いが決定てきです。従来の React コンポーネントはブラウザでレンダリングされるため、データベースやファイルシステムに直接アクセスできませんでした。しかし Server Components はサーバー上で実行されるため、以前は API ルートでしかできなかったことが可能になります:

  • データベースへの直接接続(Prisma, Drizzle, 生SQLなど)
  • ファイルシステムの読み込み(Markdown ファイルなど)
  • 内部サービスの呼び出し(CORS の心配なし)
  • 環境変数や秘密鍵へのアクセス(クライアントに漏洩しない)

したがって、以下のコードは完全に安全です:

// app/posts/page.tsx
import { db } from '@/lib/db'

async function PostsPage() {
  // DB を直接叩く。秘密鍵はブラウザには送信されない
  const posts = await db.post.findMany()

  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

export default PostsPage

ポイント

  1. コンポーネントを async function として宣言する。
  2. データベースクエリを直接 await できる。
  3. React Hooks(useState, useEffect)は使えない。
  4. デフォルトではサーバーでレンダリングされ、クライアントには生成された HTML だけが届く。

3つの主要なデータ取得方法

Server Components では、主に3つの選択肢があります:

1. fetch API

外部 API や独自の Route Handler を呼び出す最も馴染みのある方法:

async function Page() {
  const res = await fetch('https://api.example.com/data')
  const data = await res.json()
  return <div>{data.title}</div>
}

2. データベース直接クエリ

ORM や DB クライアントを使って直接取得:

import { db } from '@/lib/db'

async function Page() {
  const data = await db.posts.findFirst()
  return <div>{data.title}</div>
}

3. Server Actions

主にデータの変更(フォーム送信、削除など)に使われますが、データの取得も可能です:

async function createPost(formData: FormData) {
  'use server'
  const title = formData.get('title')
  await db.post.create({ data: { title } })
}

では、どれを使うべきなのでしょうか?

fetch vs データベースクエリ —— 選択の基準

これは私が App Router を使い始めた時に最も悩んだ点です。結論から言うと、判断基準は明確です。

5秒で決まるデシジョンツリー

以下の3つの質問に答えてください:

  1. これは Server Component ですか? → YES なら次へ。NO(Client Component)なら質問3へ。
  2. データはどこにありますか?
    • 自分のデータベース → データベース直接クエリ
    • 外部 API → fetch API
  3. Client Component からデータを取得する必要がありますか? → Route Handler (API) を作って、そこから fetch する

シンプルですね。

なぜ「直接クエリ」を優先するのか?

Next.js 公式も推奨していますが、Server Component 内では API ルートを経由せず、直接 DB を叩くべきです。

理由は合理的です:

1. HTTP ラウンドトリップの節約

比較してみましょう:

// ❌ 遠回り:Server Component → API Route → Database
async function Page() {
  const res = await fetch('http://localhost:3000/api/posts')  // 内部的な HTTP 通信が発生
  const posts = await res.json()
  return <PostList posts={posts} />
}

// ✅ 直行:Server Component → Database
async function Page() {
  const posts = await db.post.findMany()  // 直接取得
  return <PostList posts={posts} />
}

後者の方がレイヤーが1つ少なく、応答が高速です。「微々たる差では?」と思うかもしれませんが、ネットワークのオーバーヘッドやシリアライズ/デシリアライズのコストがなくなるため、ページロードが 100-200ms 速くなることもあります。

2. 優れた型安全性

TypeScript + Prisma/Drizzle を使用している場合、直接クエリなら完全な型推論が得られます:

// エディタの補完が完璧に効く
const post = await db.post.findFirst({
  include: { author: true, comments: true }
})

// post.author.name ← 型定義あり
// post.comments[0].content ← 型定義あり

fetch の場合、戻り値の型を手動で定義したり as アサーションを使う必要があり、ミスの原因になります。

3. コードが簡潔

API ルートのファイルを作る必要がなく、HTTP ステータスコードやエラーレスポンスの処理も不要です。

いつ API/fetch を使うべきか?

もちろん、API ルートが不要になるわけではありません。以下のようなケースでは必須です:

ケース1:Client Component でデータが必要な場合

ブラウザで動くコンポーネントは DB に直接アクセスできないため、API エンドポイントが必要です:

// app/api/posts/route.ts
export async function GET() {
  return Response.json(await db.post.findMany())
}

// components/client-posts.tsx
'use client'
export function ClientPosts() {
  const [posts, setPosts] = useState([])
  useEffect(() => {
    fetch('/api/posts').then(res => res.json()).then(setPosts)
  }, [])
  // ...
}

ケース2:外部に API を公開する場合

モバイルアプリやサードパーティ向けにデータを提供する場合。

ケース3:サードパーティサービスの利用

GitHub API や OpenAI API などは fetch で叩くしかありません。

async function Page() {
  const res = await fetch('https://api.github.com/users/vercel')
  // ...
}

async/await コンポーネントの書き方とパターン

並列取得 vs 直列取得:パフォーマンスの要

ここにはよくある落とし穴があります。

以下のコードはどちらが速いでしょうか?

// ❌ 直列(Waterfall):遅い
async function Page() {
  const user = await db.user.findFirst()      // 200ms 待機
  const posts = await db.post.findMany()      // さらに 150ms 待機
  const comments = await db.comment.findMany()  // さらに 100ms 待機
  // 合計 450ms
  return <Dashboard user={user} posts={posts} comments={comments} />
}

// ✅ 並列:速い
async function Page() {
  const [user, posts, comments] = await Promise.all([
    db.user.findFirst(),       // 同時発火
    db.post.findMany(),        // 同時発火
    db.comment.findMany(),     // 同時発火
  ])
  // 合計 200ms(最も遅い処理の時間)
  return <Dashboard user={user} posts={posts} comments={comments} />
}

データ間に依存関係がないなら、必ず Promise.all で並列化しましょう。2倍以上の差が出ることもあります。

Suspense 境界:ローディング体験の制御

「サーバーでデータを待っていたら、画面が白くならない?」

その通りです。だからこそ Next.js には loading.jsSuspense があります。

方法1:loading.js

ルートディレクトリに置くだけで自動的に適用されます:

// app/posts/loading.tsx
export default function Loading() {
  return <div>Loading posts...</div>
}

方法2:手動 Suspense(推奨)

よりきめ細かく制御したい場合、遅いコンポーネントだけをラップします:

import { Suspense } from 'react'

async function SlowComponent() {
  const data = await slowQuery()  // 3秒かかる
  return <div>{data}</div>
}

export default function Page() {
  return (
    <div>
      <h1>My Page</h1>
      <FastComponent />  {/* すぐ表示される */}
      <Suspense fallback={<div>Loading stats...</div>}>
        <SlowComponent />  {/* ここだけローディング表示 */}
      </Suspense>
    </div>
  )
}

よくある間違い:Suspense の配置

// ❌ 間違い:async コンポーネント内で Suspense を使っても無意味
async function Page() {
  return (
    <Suspense>
      {await slowQuery()}  {/* ここで既に待機しているため Suspense は効かない */}
    </Suspense>
  )
}

// ✅ 正解:async コンポーネントを外からラップする
export default function Layout() {
  return (
    <Suspense>
      <SlowPage />
    </Suspense>
  )
}

リクエストの自動重複排除 (Request Memoization)

React は fetch リクエストを自動的にメモ化(重複排除)します。

async function Header() {
  const user = await getUser() // リクエスト1
  return <div>{user.name}</div>
}

async function Sidebar() {
  const user = await getUser() // リクエスト2(キャッシュから返却され、実際には飛ばない)
  return <div>{user.role}</div>
}

同じレンダリングパス内であれば、同じ URL への fetch は一度しか実行されません。これにより、Props のバケツリレーを避けて、必要な場所でデータを取得するパターンが可能になります。

注意fetch 以外の関数(DBクエリなど)には自動適用されません。React の cache 関数を使って手動でメモ化する必要があります。

import { cache } from 'react'
import { db } from '@/lib/db'

export const getUser = cache(async (id: string) => {
  return db.user.findUnique({ where: { id } })
})

キャッシュ戦略:Next.js 15 の変更点

Next.js 15 でキャッシュのデフォルト挙動が大きく変わりました。

Next.js 14 までfetch はデフォルトで force-cache(永久キャッシュ)。
Next.js 15 からfetch はデフォルトで no-store(キャッシュなし)。

「データが更新されない!」という苦情が多かったため、より直感的な挙動(常に最新)に変更されました。

3つのキャッシュ戦略

1. 完全キャッシュ(静的コンテンツ)

async function BlogPost({ slug }) {
  const post = await fetch(`https://api.example.com/posts/${slug}`, {
    cache: 'force-cache'  // ビルド時に取得され、永続キャッシュ
  })
  // ...
}

2. キャッシュなし(リアルタイムデータ)

Next.js 15 のデフォルトですが、明示的に書くなら:

async function StockPrice() {
  const price = await fetch('...', {
    cache: 'no-store'
  })
  // ...
}

3. ISR(定期的再検証)

async function ProductList() {
  const products = await fetch('...', {
    next: { revalidate: 60 }  // 60秒間はキャッシュ、その後裏で更新
  })
  // ...
}

データの更新(Revalidation)

キャッシュを手動でパージしたい場合(例:CMS で記事を更新した時など):

  1. revalidatePath: 特定のパスのキャッシュをクリア

    revalidatePath('/blog/[slug]')
  2. revalidateTag: 特定のタグが付いたキャッシュを一括クリア

    // データ取得時
    fetch(url, { next: { tags: ['posts'] } })
    
    // Server Action で
    revalidateTag('posts')

エラー処理の実践

方法1:try/catch

最も基本的ですが、コンポーネントレベルで制御できます。

async function Page() {
  try {
    const res = await fetch('...')
    if (!res.ok) throw new Error('Fetch failed')
    // ...
  } catch (error) {
    return <div>データの取得に失敗しました</div>
  }
}

方法2:error.js

ルート単位でエラーを捕捉するバリアです。

// app/dashboard/error.tsx
'use client' // エラー境界は Client Component である必要がある

export default function Error({ error, reset }: { error: Error, reset: () => void }) {
  return (
    <div>
      <h2>エラーが発生しました</h2>
      <button onClick={() => reset()}>再試行</button>
    </div>
  )
}

重要テクニック:redirect の罠

redirect 関数は内部的に特別なエラーを投げることで動作します。try/catch ブロック内で使うと、そのエラーがキャッチされてしまい、リダイレクトが失敗します。

// ❌ 失敗:redirect がキャッチされる
try {
  const user = await getUser()
  if (!user) redirect('/login')
} catch (e) {
  // redirect のエラーがここに来てしまう
}

// ✅ 成功:redirect はブロック外で
let user
try {
  user = await getUser()
} catch (e) {
  // ログ出力など
}
if (!user) redirect('/login')

まとめ:シンプルに考えよう

Next.js App Router のデータ取得は、一見複雑に見えるかもしれませんが、以下の原則に従えばシンプルです:

  1. Server Component なら DB を直接叩く。API ルートを作らない。
  2. Client Component で必要なら API ルートを作る
  3. 独立したデータ取得は Promise.all で並列化する
  4. Next.js 15 では fetch はデフォルトでキャッシュされない。必要なら設定する。
  5. エラー処理とサスペンスを活用してユーザー体験を守る

クライアントサイドでの複雑な状態管理から解放され、サーバーでシンプルにデータを取得する快感を、ぜひ体験してください。

Next.js Server Component データ取得フロー

コンポーネント内での非同期データ取得からエラー処理までの実装手順

⏱️ Estimated time: 15 min

  1. 1

    Step1: コンポーネントの定義

    コンポーネントを async function として定義します。

    async function Page({ params }:Props) {
    // ...
    }
  2. 2

    Step2: データの取得

    DB クライアントを使って直接データを取得します。依存関係のない複数のデータは Promise.all で並列化します。

    const [user, posts] = await Promise.all([
    db.user.findFirst(),
    db.post.findMany()
    ])
  3. 3

    Step3: ローディング処理

    遅い処理がある場合は、Suspense コンポーネントでラップし、fallback UI を提供します。

    <Suspense fallback={<LoadingSkeleton />}>
    <SlowComponent />
    </Suspense>
  4. 4

    Step4: エラー処理

    try/catch ブロックを使用するか、error.tsx ファイルを作成してエラー時の UI を定義します。

FAQ

Server Component で API Route を呼ぶのは悪いことですか?
はい、推奨されません。自身の API Route をサーバーから呼ぶと、不要な HTTP 通信が発生し、パフォーマンスが低下します。API Route 内のロジック(コントローラー関数など)を直接インポートして呼び出すか、DB を直接クエリしてください。
fetch を使わない場合、キャッシュはどうすればいいですか?
React の cache 関数(unstable_cache)を使用して、DB クエリの結果などをキャッシュできます。また、Next.js 自体の fetch キャッシュ機能の代わりに、データ取得関数の戻り値をキャッシュするパターンが推奨されています。
Server Actions はデータ取得に使えますか?
可能ですが、Server Actions は本来 POST リクエスト(データの変更)を意図しています。データ取得(GET)には Server Component 内での直接実行または API Route が適しています。
クライアントコンポーネントで async/await は使えますか?
いいえ、現在の React ではクライアントコンポーネント自体を async にすることはできません(一部の実験的機能を除く)。クライアント側では useEffect 内で fetch するか、SWR / React Query などのライブラリを使用してください。

4 min read · 公開日: 2025年12月19日 · 更新日: 2026年1月22日

コメント

GitHubアカウントでログインしてコメントできます

関連記事