Next.js でのデータ取得と API
ブログをApp Routerに移行した後、トムは新たな問題に直面しました。ページによってデータの更新頻度が大きく異なっていたのです。記事は月に1回、商品の価格は1日に数回、ユーザーのアバターはログインするたびに変わる可能性がありました。彼は、各データタイプの特性に応じて異なるデータ取得戦略を選択しつつ、フロントエンド向けに統一されたAPIインターフェースを提供する必要がありました。データ取得のためのNext.jsのサーバーコンポーネントとルートハンドラーは、まさにこれらの課題を解決するために彼が必要としていたものでした。
1. 学習内容
- サーバーコンポーネントにおける
fetchデータの直接キャッシュに関する 3 つの戦略(force-cache / no-store / revalidate) - ルートハンドラーは、RESTful APIのエンドポイント(GET/POST/PUT/DELETE)を作成します。
- ISRのインクリメンタル静的生成の仕組みと設定方法
- ミドルウェアにおけるリクエストのインターセプトとルートガード
- サーバー側およびクライアント側の環境変数に関するセキュリティアクセスポリシー
- オンデマンド・キャッシュ再検証のためのオンデマンド ISR の実装(revalidateTag / revalidatePath)
- エッジランタイムにおけるミドルウェアの機能の限界とパフォーマンスに関する考慮事項
2. 概念図
トムは、Next.jsにおけるデータ取得の全プロセスを概説しました。ページリクエストが届くと、Next.jsはデータ取得戦略に基づいて、データをキャッシュするか、再検証するか、あるいはAPIにルーティングするかを判断します。このプロセスを理解することで、彼はデータの種類に応じて適切なアプローチを選択できるようになります。
フローチャート内の決定ノード「{Caching Strategy}」は、このセクションの中核をなすものであり、データ取得のパフォーマンスとリアルタイム性能を決定づける。force-cacheは最速ですが、リアルタイムではありません。no-storeは最もリアルタイム性が高いですが、パフォーマンスは最も低くなります。revalidateとtagsは、この2つの要素の間で柔軟なトレードオフを実現します。ルートハンドラとミドルウェアは、それぞれAPIへのアクセス処理とリクエストのインターセプトを行う補助的なメカニズムとして機能します。
flowchart TD
A[Page Request] --> B{Server Component}
B --> C[fetch Data]
C --> D{Caching Policy}
D -->|force-cache| E[Read Cache<br/>Default SSG]
D -->|no-store| F[Request every time<br/>Real-time SSR]
D -->|revalidate: N| G[Cache N seconds<br/>ISR Pattern]
D -->|{next: {tags: [...]}}| H[Cache by Tag<br/>On-Demand ISR]
E --> I[Back HTML]
F --> I
G --> I
H --> I
I --> J[Route Handler<br/>/api/*]
J --> K[Database/External API]
I --> L{To be verified?}
L -->|is | M[Middleware<br/>Auth/Redirect]
L -->|No| N[Direct Rendering]
3. 実際の事例
ブログをApp Routerに移行した後、トムはデータ取得に関して新たな課題に直面しました。以前はgetStaticPropsとgetServerSidePropsを使用していましたが、これらは使い慣れていたものの、柔軟性に欠けていました。例えば、記事一覧は1時間に1回更新する必要がありましたが、人気記事が編集された場合は即座に更新する必要がありました。しかし、getStaticPropsではページごとにこの処理を行うことができませんでした。
App Router は、まったく新しいデータ取得のパラダイムを導入しています。fetch はサーバーコンポーネント内で直接使用され、next.revalidate および next.tags はキャッシュ戦略を精密に制御するために使用されます。また、Tom はルートハンドラーを使用して、コメントセクションのフロントエンドコンポーネントが呼び出せる API エンドポイントを公開し、ミドルウェアを用いてダッシュボードのルートを保護しています。以下は、彼がブログ用に設計したデータ取得アーキテクチャです。
彼はまず、ブログ上のすべてのデータソースの特徴を分析しました。具体的には、記事コンテンツ(1時間ごとに更新され、キャッシュ可能)、コメント(リアルタイムで更新され、即座に表示する必要がある)、統計情報(アクセスごとに変動する)、製品情報(1日に数回更新され、更新後できるだけ早く表示する必要がある)です。その後、データタイプごとに異なるフェッチ戦略を選択しました。この分析を通じて、彼はNext.jsのデータフェッチ設計の素晴らしさを実感しました。それは、画一的な「SSGかSSRか」というアプローチではなく、ページ単位、リクエスト単位、さらにはデータタグ単位で精密な制御を可能にするものだったのです。
(1) サーバーコンポーネント:データ取得
await fetchはサーバーコンポーネント内で直接使用できます。これはApp Routerの最も強力な機能の一つです。useEffectも、SWRやReact Queryも、追加のクライアントサイド状態管理ライブラリも必要ありません。コンポーネント関数内で直接データをリクエストするだけで、サーバーサイドレンダリングが完了すると、そのデータはHTMLとともにブラウザに送信されます。
fetch の 2 番目のパラメータには、next の設定を含むオブジェクトを指定します。これはキャッシュの動作を制御するために使用されます。{ cache: 'force-cache' } は SSG と同等であり、ビルド時にデータが 1 回だけ取得されます。{ cache: 'no-store' }はSSRに相当し、リクエストごとにデータが新たに取得されます;{ next: { revalidate: 60 } }はISRに相当し、60秒間のキャッシュ後にデータが再検証されます。
さらに高度な活用例として、「オンデマンド ISR」があります。next: { tags: ['posts'] } を使用してデータにタグを付け、その後、ルートハンドラーまたは管理ページで revalidateTag('posts') を呼び出すことで、手動で更新をトリガーします。これにより、再検証期間が終了するのを待つことなく、編集直後に記事を再生成することができます。
3つのキャッシュ戦略に関する決定表
| 戦略 | 設定の取得 | 動作 | ユースケース |
|---|---|---|---|
| 静的 SSG | cache: 'force-cache' |
ビルド時に一度取得され、その後はすべて CDN 経由で配信 | 記事コンテンツ、About ページ |
| リアルタイムSSR | cache: 'no-store' |
リクエストごとに更新 | ユーザーデータ、リアルタイムダッシュボード |
| 増分ISR | next: { revalidate: 60 } |
キャッシュに保存後60秒後に再検証 | 製品一覧、価格ページ |
| オンデマンド更新 | next: { tags: ['x'] } |
キャッシュ + revalidateTag() を指定して更新を実行 |
CMSコンテンツの編集後、即座に更新 |
トムが戦略を選択する際の原則:決定は、データ変更の頻度とリアルタイム性の要件に基づいて行われる。記事の内容(変更頻度が低い)にはSSGを、コメント数(変更頻度は高いが多少の遅延は許容できる)には30秒ごとに更新されるISRを、ユーザーのアバター(即時更新が必要)にはSSRを採用している。これらの戦略を適切に組み合わせることで、パフォーマンスとリアルタイム応答性の最適なバランスを見出すことができる。
(1) ▶ サンプル:サーバーコンポーネントの 3 つのキャッシュ戦略
// app/posts/page.tsx - Strategy 1:force-cache(Default SSG)
// Data is retrieved only once during build time.,All subsequent requests go through CDN cache
async function PostsPage() {
const posts = await fetch('https://api.example.com/posts', {
cache: 'force-cache' // equivalent to SSG,Default behavior
}).then(r => r.json())
return (
<div>
<h1>List of Articles</h1>
{posts.map((post: any) => (
<article key={post.id} style={{ marginBottom: 16 }}>
<h2>{post.title}</h2>
<p>{post.body.slice(0, 100)}...</p>
</article>
))}
</div>
)
}
export default PostsPage
// app/dashboard/stats/page.tsx - Strategy 2:no-store(Real-time SSR)
// Every request starts from API Get the latest data
export const dynamic = 'force-dynamic'
async function StatsPage() {
const stats = await fetch('https://api.example.com/dashboard/stats', {
cache: 'no-store' // Request the latest data every time
}).then(r => r.json())
return (
<div>
<h1>Real-Time Statistics</h1>
<p>Users Online:{stats.onlineUsers}</p>
<p>Today PV:{stats.pageViews}</p>
<p>API Number of calls:{stats.apiCalls}</p>
</div>
)
}
export default StatsPage
// app/products/page.tsx - Strategy 3:revalidate(ISR Incremental Update)
// Cache 60s, first visit after 60s triggers a regenerate
async function ProductsPage() {
const products = await fetch('https://api.example.com/products', {
next: { revalidate: 60 } // ISR:60 Please try again in a few seconds.
}).then(r => r.json())
return (
<div>
<h1>Product List</h1>
{products.map((p: any) => (
<div key={p.id} style={{ border: '1px solid #ddd', padding: 12, marginBottom: 8, borderRadius: 8 }}>
<h3>{p.name}</h3>
<p>Price:${p.price}</p>
<p>Inventory:{p.stock > 0 ? 'In stock' : 'Out of stock'}</p>
</div>
))}
</div>
)
}
export default ProductsPage
// app/api/revalidate/route.ts - On-Demand ISR Refresh on Demand
// Call this after the administrator edits the article API Refresh the cache now
import { revalidateTag } from 'next/cache'
export async function POST(request: Request) {
const body = await request.json()
const { tag } = body
// Verification secret Preventing Abuse
const secret = request.headers.get('x-revalidate-secret')
if (secret !== process.env.REVALIDATION_SECRET) {
return Response.json({ error: 'Unauthorized' }, { status: 401 })
}
revalidateTag(tag) // Refresh the cache by tag
return Response.json({ revalidated: true, tag })
}
(2) ルートハンドラ API によるルーティング
ルートハンドラーとは、App Router で API エンドポイントを作成するために使用されるメソッドのことです。app/api/ ディレクトリに route.ts という名前のファイルを作成し、HTTP メソッドに対応する名前(GET、POST、PUT、DELETE、PATCH)の関数をエクスポートします。ルートハンドラーは、サーバーコンポーネントのすべての機能をサポートしています。データベースへのアクセス、環境変数の使用、キャッシュポリシーの制御が可能です。
トムは、前回のブログ記事で紹介したExpressのバックエンドをRoute Handlerに置き換えました。記事のCRUD操作、ユーザー認証、コメントシステムはすべて、Route Handlerを通じてAPIとして公開されています。On-Demand ISRと組み合わせることで、記事の編集直後にキャッシュが更新されるため、ユーザーは再検証期間が終了するのを待つ必要がありません。
ルートハンドラは動的ルーティングもサポートしています。app/api/products/[id]/route.ts では、パスパラメータは params パラメータを介して取得され、これは RESTful API の設計に適しています。また、各ルートハンドラは独自のキャッシュ戦略を設定することも可能です。例えば、GET リクエストに対して { next: { revalidate: 60 } } を設定することで、API レベルのキャッシュを実現できます。
コアルートハンドラの使用に関するクイックリファレンス
| ファイルパス | HTTPメソッド | URLエンドポイント | 目的 |
|---|---|---|---|
app/api/posts/route.ts |
GET | /api/posts |
記事一覧を取得 |
app/api/posts/route.ts |
投稿 | /api/posts |
新規投稿 |
app/api/posts/[id]/route.ts |
GET | /api/posts/1 |
1つの記事を取得 |
app/api/posts/[id]/route.ts |
PUT | /api/posts/1 |
記事を更新 |
app/api/posts/[id]/route.ts |
削除 | /api/posts/1 |
投稿を削除 |
app/api/auth/login/route.ts |
投稿 | /api/auth/login |
ユーザーログイン |
app/api/revalidate/route.ts |
投稿 | /api/revalidate |
ISRキャッシュの更新 |
(2) ▶ サンプル:ブログ用 CRUD API の実装
// app/api/posts/route.ts - List of Articles API(GET)and creating articles API(POST)
import { revalidateTag } from 'next/cache'
// Simulated Database
const posts = [
{ id: 1, title: 'Next.js Getting Started', body: 'This article introduces Next.js Basic Usage...', published: true },
{ id: 2, title: 'React 19 New Features', body: 'React 19 What changes has this brought about?...', published: true },
]
export async function GET() {
// Return only published posts
const published = posts.filter(p => p.published)
return Response.json(published)
}
export async function POST(request: Request) {
try {
const body = await request.json()
const newPost = {
id: posts.length + 1,
title: body.title,
body: body.body,
published: body.published ?? false,
createdAt: new Date().toISOString()
}
posts.push(newPost)
// Refresh the article list ISR cache
revalidateTag('posts')
return Response.json(newPost, { status: 201 })
} catch (error) {
return Response.json({ error: 'Invalid request body' }, { status: 400 })
}
}
// app/api/posts/[id]/route.ts - Operations on a Single Article(GET / PUT / DELETE)
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
const post = posts.find(p => p.id === Number(params.id))
if (!post) {
return Response.json({ error: 'Post not found' }, { status: 404 })
}
return Response.json(post)
}
export async function PUT(
request: Request,
{ params }: { params: { id: string } }
) {
const body = await request.json()
const index = posts.findIndex(p => p.id === Number(params.id))
if (index === -1) {
return Response.json({ error: 'Post not found' }, { status: 404 })
}
posts[index] = { ...posts[index], ...body, id: Number(params.id) }
revalidateTag('posts')
return Response.json(posts[index])
}
export async function DELETE(
request: Request,
{ params }: { params: { id: string } }
) {
const index = posts.findIndex(p => p.id === Number(params.id))
if (index === -1) {
return Response.json({ error: 'Post not found' }, { status: 404 })
}
posts.splice(index, 1)
revalidateTag('posts')
return Response.json({ message: 'Deleted' })
}
(3) ミドルウェアとリクエストのインターセプト
ミドルウェアは、Next.js の強力なリクエストインターセプト機能です。各リクエストがページに到達する前に実行され、リダイレクト、認証、国際化ルーティング、A/Bテストなどの処理に利用できます。ミドルウェアは Edge Runtime 上で実行され、レイテンシが極めて低い(ミリ秒オーダー)のが特徴です。
トムはミドルウェアを使用して、3つの機能を実装しました。1つ目は、ログインしていないユーザーがダッシュボードにアクセスした際に、ログインページへリダイレクトすること。2つ目は、ユーザーのブラウザの言語設定に基づいて、適切な言語版のページへ自動的にリダイレクトすること。3つ目は、クローラーがAPIルートにアクセスできないようにブロックすることです。
ミドルウェアは、matcherの設定を使用して、インターセプトが必要なルーティングを照合し、それによって不要な実行オーバーヘッドを回避します。なお、ミドルウェアはreq.bodyを読み取ったり、Node.jsのネイティブAPIを使用したりすることはできません。これは、エッジ環境で実行されるためです。
(3) ▶ サンプル:完全なミドルウェア認証システム
// middleware.ts - Project Root Directory,and app/ Peer
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl
// === Features1:Route Guard ===
// Routes Protected from Unauthenticated Users → Redirect to the login page
const token = request.cookies.get('session_token')?.value
const protectedPaths = ['/dashboard', '/admin', '/profile']
if (!token && protectedPaths.some(path => pathname.startsWith(path))) {
const loginUrl = new URL('/login', request.url)
loginUrl.searchParams.set('redirect', pathname)
return NextResponse.redirect(loginUrl)
}
// === Features2:Logged-in users are redirected to the login page → Redirect to the Dashboard ===
if (token && pathname.startsWith('/login')) {
return NextResponse.redirect(new URL('/dashboard', request.url))
}
// === Features3:International Routing ===
// According to Accept-Language Automatically Redirect to Language Version
const supportedLocales = ['zh', 'en', 'ja', 'pt']
const defaultLocale = 'zh'
// Inspection URL Is the language prefix included?
const pathnameHasLocale = supportedLocales.some(
locale => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
)
if (!pathnameHasLocale) {
const acceptLanguage = request.headers.get('accept-language') || ''
const preferredLocale = supportedLocales.find(locale =>
acceptLanguage.startsWith(locale)
) || defaultLocale
return NextResponse.redirect(new URL(`/${preferredLocale}${pathname}`, request.url))
}
// === Features4:Set Response Headers ===
const response = NextResponse.next()
response.headers.set('X-Frame-Options', 'DENY')
response.headers.set('X-Content-Type-Options', 'nosniff')
return response
}
// Layout middleware Match Path(Required,Otherwise, all routes will be triggered.)
export const config = {
matcher: [
// Match all routes that need to be protected
'/dashboard/:path*',
'/admin/:path*',
'/profile/:path*',
'/login',
// Matching Internationalized Routes
'/((?!api|_next/static|_next/image|favicon.ico).*)',
]
}
ミドルウェア設定ガイド
| 設定オプション | 説明 | 例 |
|---|---|---|
matcher |
ミドルウェアの実行が必要なルート | ['/dashboard/:path*', '/login'] |
request.nextUrl |
現在のリクエストの URL オブジェクト | パス名や検索パラメータを取得するために使用されます |
request.cookies |
クッキーの閲覧・操作 | request.cookies.get('token') |
NextResponse.redirect() |
指定されたURLへリダイレクト | ログイン認証に使用 |
NextResponse.next() |
通常通りリクエストの処理を続行 | 有効なリクエストを許可 |
response.headers.set() |
レスポンスヘッダーの設定 | セキュリティ関連のレスポンスヘッダー |
ミドルウェアの実行順序と留意点
ミドルウェアは一致したリクエストごとに実行されるため、パフォーマンスが極めて重要です。Edge Runtime はマイクロ秒単位で実行されるように設計されているため、その内部でデータベースクエリや複雑な計算を実行すべきではありません。複数のミドルウェアコンポーネントの実行順序は、matcher で設定された順序によって決定されます。あるミドルウェアコンポーネントが redirect() または rewrite() を返した場合、それ以降のミドルウェアコンポーネントは実行されません。
重要な注意点:ミドルウェア内の環境変数は、NEXT_PUBLIC_ というプレフィックスを付けて公開する必要があるのでしょうか?いいえ、ミドルウェアはサーバー環境で実行されるため、すべての環境変数に直接アクセスできます。ただし、ミドルウェアのコンパイル時に process.env は実際の値に置き換えられるため、実行時に環境変数を動的に読み取ることはできない点に留意してください。
4. 環境変数のセキュリティポリシー
また、トムはルートハンドラーやミドルウェアを使用する際に、環境変数への安全なアクセスという重大な問題に直面しました。Next.js では、環境変数へのアクセスルールはプレフィックスによって決まりますが、これはデータの取得や API 開発において非常に重要です。
環境変数へのアクセスに関するルール
| 接頭辞 | アクセス可能な場所 | 説明 |
|---|---|---|
| プレフィックスなし | サーバーコンポーネント、ルートハンドラー、ミドルウェア | サーバー側でのみ利用可能。ブラウザには公開されない |
NEXT_PUBLIC_ |
すべての場所(ブラウザを含む) | はコンパイルされてJSにバンドルされるため、機密情報を含めないでください |
NEXT_PRIVATE_ |
サーバー側でのみ利用可能 | プレフィックスのないバージョンと同じ動作をする新しい明示的なマーカー |
トムの経験則:データベースの接続文字列、APIのシークレットキー、JWTキーなどの機密情報は、プレフィックスを付けずに、サーバーコンポーネントやルートハンドラー内でのみ使用してください。Google AnalyticsのIDや公開APIのURLなど、フロントエンドで使用する必要がある変数には、NEXT_PUBLIC_というプレフィックスを付けてください。
(1) ▶ サンプル:ISR 増分静的再生成 — ブログ記事ページ
// app/blog/[slug]/page.tsx - ISR: Regenerate every 60s
interface BlogPost {
title: string
content: string
author: string
publishedAt: string
}
async function getPost(slug: string): Promise<BlogPost> {
const res = await fetch(`https://api.example.com/posts/${slug}`, {
next: { revalidate: 60 },
})
if (!res.ok) throw new Error('Post not found')
return res.json()
}
export async function generateStaticParams() {
const posts = await fetch('https://api.example.com/posts').then(r => r.json())
return posts.map((post: { slug: string }) => ({ slug: post.slug }))
}
export default async function BlogPostPage({ params }: { params: { slug: string } }) {
const post = await getPost(params.slug)
return (
<article style={{ maxWidth: 700, margin: '0 auto', padding: 24 }}>
<h1>{post.title}</h1>
<p style={{ color: '#999', fontSize: 14 }}>
By {post.author} · {new Date(post.publishedAt).toLocaleDateString()}
</p>
<div style={{ lineHeight: 1.8, marginTop: 16 }}>{post.content}</div>
</article>
)
}
(2) ▶ サンプル:サーバー側の処理—フォームの送信とデータの変更
// app/actions.ts - Server Actions
'use server'
import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'
export async function createPost(formData: FormData) {
const title = formData.get('title') as string
const content = formData.get('content') as string
if (!title || !content) return
await fetch('https://api.example.com/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, content, author: 'Alice' }),
})
revalidatePath('/blog')
redirect('/blog')
}
export async function deletePost(slug: string) {
await fetch(`https://api.example.com/posts/${slug}`, { method: 'DELETE' })
revalidatePath('/blog')
}
// app/blog/new/page.tsx - New Article Form
function NewPostPage() {
return (
<div style={{ maxWidth: 600, margin: '0 auto', padding: 24 }}>
<h1>New Post</h1>
<form action={createPost} style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
<input name="title" placeholder="Title" required style={{ padding: 8, borderRadius: 4 }} />
<textarea name="content" placeholder="Content..." rows={8} required style={{ padding: 8, borderRadius: 4 }} />
<button type="submit" style={{ padding: 10, background: '#1890ff', color: 'white', border: 'none', borderRadius: 4, cursor: 'pointer' }}>
Publish
</button>
</form>
</div>
)
}
export default NewPostPage
❓ よくある質問
fetch と、クライアント側の useEffect の fetch との違いは何ですか?fetch はサーバー上で実行されます。データは HTML としてレンダリングされ、ブラウザに直接送信されるため、ユーザーには読み込み画面が表示されることなく、完成したページが表示されます。一方、useEffectのfetchはクライアント側で実行されます。ユーザーにはまず空白のページまたは読み込み画面が表示され、JavaScriptのダウンロードと実行が完了してから初めてコンテンツがレンダリングされます。サーバーコンポーネントのfetch方式は、ファーストスクリーンの読み込みが速く、SEOにもより適しています。req.bodyの読み取り、Node.jsの組み込みモジュール(fs、path)の使用、データベースへのアクセス、およびfetch以外の外部リクエストの送信。複雑な認証ロジックは、ルートハンドラーで実装することを推奨します。revalidate: 60を設定すると、ページは最大60秒以内に更新されます。オンデマンドISRはイベント駆動型の再読み込みであり、revalidateTag()またはrevalidatePath()を呼び出すとページが即座に更新されるため、コンテンツの変更を直ちに反映させる必要があるシナリオに適しています。これら2つは組み合わせて使用することも可能です。フェイルオーバーとして再検証時間を長く設定しつつ、コンテンツが変更された際にはオンデマンドISRを使用して即座に更新を行うといった使い方ができます。Access-Control-Allow-Origin などのCORSヘッダーを直接設定するだけです。統一されたCORSレスポンスヘッダーオブジェクトを返すユーティリティ関数 corsHeaders() をラップし、Response.json(data, { headers: corsHeaders() }) を使用して各ルートハンドラーに組み込むことができます。フロントエンドとバックエンドが同じドメイン上にある場合(Next.jsのフルスタック展開では通常このケースになります)、CORSを処理する必要はありません。📖 まとめ
- サーバーコンポーネントは
await fetchから直接データを取得し、force-cache(SSG)、no-store(SSR)、next.revalidate(ISR) の 3 つのキャッシュ戦略をサポートしています。 - ルートハンドラは、
app/api/ディレクトリにあるroute.tsファイルで定義されています。エクスポートされた関数名は、HTTP メソッド(GET/POST/PUT/DELETE)に対応しています。 - ルートハンドラーは、動的ルーティング (
[id])、リクエスト本文の解析、レスポンスステータスコードの制御、およびカスタムキャッシュポリシーに対応しています - ISRは、
next.revalidateを通じて定期的な再検証を設定し、next.tagsおよびrevalidateTag()を通じてオンデマンドの更新を実装します。 - ミドルウェアは、リクエストがページに到達する前に実行されます。Edge Runtime 上で動作し、ルートガード、国際化、リダイレクト、セキュリティヘッダーの設定などのシナリオに適しています。
matcher不要なパフォーマンスのオーバーヘッドを避けるため、ミドルウェアの実行範囲を設定してください。特定のルートを正確に一致させるか、除外することができます。- オンデマンド ISR は、編集後すぐにコンテンツを更新する必要があるシナリオに適しています。これは
revalidateTagまたはrevalidatePathによってトリガーされ、スケジュールされた有効期限を待つ必要はありません。 - データ取得戦略の選定原則:データの更新頻度および適時性の要件に基づき、SSG、ISR、SSRを組み合わせたハイブリッドなアプローチを決定する。
middleware.tsの設定は、_next/staticやfavicon.icoなどの静的リソースと一致しないように、正確に行う必要があります。revalidateTagおよびrevalidatePathは、オンデマンド ISR を実装するための 2 つの主要な API であり、ルートハンドラーまたはサーバーアクション内で呼び出されます- このレッスンで取り上げたデータ取得のスキルはすべて、その後のレッスン(CI/CDの導入、コンポーネントライブラリの統合、および総合プロジェクト)の技術的な基礎となります。
📝 練習問題
- プロジェクト内に
app/api/products/route.tsを作成し、サンプル製品 API を実装します。GET メソッドは製品リストを返し、POST メソッドは新しい製品を作成します。app/products/page.tsxでは、サーバーコンポーネントfetchを使用してこのAPIを呼び出し、商品リストを表示します。また、revalidate: 30を設定してISRを実装します。商品データが変更されてから30秒間はページが更新されないことを確認してください。30秒経過後にページを更新すると、新しいデータが表示されます。 /dashboard/*への経路を保護するためにmiddleware.tsを作成します。クッキーにsession_tokenが含まれていない場合は、/loginにリダイレクトします。同時に、ログイン済みのユーザーが/loginにアクセスした際は、自動的に/dashboardにリダイレクトします。matcherを使用して、必要なパスにのみ一致するようにルーティングを厳密に設定し、_next/staticなどのリソースパスでミドルウェアが実行されないようにします。- オンデマンドのISR更新メカニズムを実装する:管理者ページにある「キャッシュの更新」ボタンをクリックして、
POST /api/revalidate(x-revalidate-secretリクエストヘッダーによる認証付き)を呼び出し、revalidateTag('products')を介して商品ページのISRキャッシュを更新する。更新後、ページが直ちに更新されるかどうかを確認し、スケジュールされたISRとオンデマンドのISRの更新速度を比較する。



