Next.js でサイトを運用していると、「古い URL を新しい URL へ飛ばしたい」「見た目の URL は変えずに裏側で別のパスを表示したい」といった要望が出てきます。こうした URL の制御は、next.config.js の redirects() と rewrites() で宣言的に設定できます。この記事では、この2つの書き方と決定的な違い、source / destination / permanent の意味、:path* などのパスマッチ、外部 URL へのプロキシ、そして App Router の redirect() や middleware との使い分けまでを、動くコードとあわせて解説します。
目次
redirects と rewrites でできること
redirects() は、あるパスへのアクセスを別の URL へ転送(リダイレクト)する設定です。ブラウザのアドレスバーの URL が実際に書き換わり、HTTP のリダイレクトステータス(301 や 308 など)が返ります。ページを移動したときに古い URL を生かしたい場合や、URL 構造を変更したときの引っ越しに使います。
一方 rewrites() は、ブラウザから見える URL はそのままに、内部的に別のパスの内容を表示する設定です。URL を隠す(マスキングする)ような働きをするため、リバースプロキシのように外部 API や別サービスの内容を自サイトの URL のもとで配信したいときに向いています。どちらも next.config.js に非同期関数として定義します。
redirects() の基本的な書き方
まずはリダイレクトの最小構成です。next.config.js の設定オブジェクトに async な redirects 関数を追加し、リダイレクトのルールを配列で返します。各ルールは source(リクエスト元のパス)、destination(転送先)、permanent(恒久的かどうか)の3つを持ちます。
/** @type {import('next').NextConfig} */
const nextConfig = {
async redirects() {
return [
{
source: '/old-blog', // 転送元のパス
destination: '/blog', // 転送先のパス
permanent: true, // 恒久的なリダイレクト(308)
},
];
},
};
module.exports = nextConfig;
この設定で /old-blog にアクセスすると、ブラウザは /blog へ転送され、アドレスバーの URL も /blog に変わります。permanent を true にすると 308 Permanent Redirect、false にすると 307 Temporary Redirect が返ります。307 / 308 は元のリクエストの HTTP メソッドとボディを維持するステータスで、Next.js はこちらを既定で使います。恒久的な引っ越しなら true、一時的な転送なら false を選びます。
redirects と rewrites の違い
2つはどちらも「あるパスへのアクセスを別の場所に結びつける」点は同じですが、ブラウザに見える URL が変わるかどうかが決定的に違います。リダイレクトは URL が変わり HTTP ステータスが返るのに対し、リライトは URL がそのままで中身だけ差し替わります。次の表で整理します。
| 観点 | redirects(リダイレクト) | rewrites(リライト) |
|---|---|---|
| ブラウザの URL | 転送先に変わる | 変わらない(元の URL のまま) |
| HTTP ステータス | 307 / 308(や 301 / 302) | 200(通常のレスポンス) |
| 仕組み | ブラウザに転送先を指示する | サーバー内部で別パスの内容を返す(マスキング) |
| 主な用途 | URL の移転・引っ越し | 外部 API のプロキシ、URL の隠蔽 |
たとえば /dashboard を /app/dashboard にリダイレクトするとアドレスバーは /app/dashboard になりますが、リライトなら利用者にはずっと /dashboard が見え続け、実際には /app/dashboard の内容が表示されます。「利用者に見せたい URL」と「実際に処理したい場所」を分けたいときにリライトが役立ちます。
source と destination のパスマッチ
source と destination は固定パスだけでなく、パラメータやワイルドカードを使ってまとめて指定できます。パスの一部を :name の形で受け取り、それを destination 側で再利用できます。さらに :path* のように * を付けると、それ以降のすべてのセグメントにマッチします。
/** @type {import('next').NextConfig} */
const nextConfig = {
async redirects() {
return [
{
// :slug で1セグメントを受け取り、転送先で再利用する
source: '/news/:slug',
destination: '/blog/:slug',
permanent: true,
},
{
// :path* で /docs 以下のすべてのパスをまとめて転送する
source: '/docs/:path*',
destination: '/help/:path*',
permanent: true,
},
];
},
};
module.exports = nextConfig;
1つ目のルールでは /news/hello が /blog/hello に転送されます。:slug がマッチした部分(hello)が destination の :slug にそのまま埋め込まれる仕組みです。2つ目の :path* は0個以上のセグメントにマッチするワイルドカードで、/docs/a/b/c のような深い階層もまとめて /help/a/b/c へ転送できます。パラメータ名(slug や path)は自由に付けられますが、source と destination で名前をそろえる必要があります。
rewrites() で外部 URL をプロキシする
リライトは redirects() とよく似た書き方ですが、permanent は不要で、代わりに destination に外部の URL(絶対 URL)を指定できるのが特徴です。これを使うと、自サイトの URL の裏で別のドメインのコンテンツや API を配信できます。よくあるのは、フロントと同じオリジンに API を見せかけて CORS を避けるパターンです。
/** @type {import('next').NextConfig} */
const nextConfig = {
async rewrites() {
return [
{
// /api/ 以下へのアクセスを外部のバックエンドへ中継する
source: '/api/:path*',
destination: 'https://api.example.com/:path*',
},
];
},
};
module.exports = nextConfig;
この設定では、ブラウザが /api/users にリクエストすると、URL は /api/users のまま、実際には https://api.example.com/users の内容が返ります。利用者やフロントエンドのコードからは同一オリジンへのアクセスに見えるため、ブラウザの同一オリジンポリシーに引っかからずにバックエンドと通信できます。destination にホスト名付きの絶対 URL を書けるのはリライトならではで、リダイレクトと違ってブラウザに転送先を教えないため、外部のドメインを隠したまま配信できます。
なお rewrites() は配列だけでなく、beforeFiles / afterFiles / fallback というキーを持つオブジェクトを返して、ファイルシステム(ページや public の静的ファイル)との適用順序を細かく制御することもできます。単純な配列を返した場合は afterFiles と同じ扱いになり、既存のページに一致しなかったリクエストだけがリライトされます。
特定の条件のときだけマッチさせる
source のパスに加えて、ヘッダー・クッキー・クエリの条件でマッチを絞り込むこともできます。has(その条件があるときにマッチ)と missing(その条件がないときにマッチ)を使います。たとえば、特定のクッキーを持たない利用者だけをログインページへ飛ばす、といった制御ができます。
/** @type {import('next').NextConfig} */
const nextConfig = {
async redirects() {
return [
{
source: '/dashboard',
// session クッキーが「無い」ときだけマッチさせる
missing: [
{
type: 'cookie',
key: 'session',
},
],
destination: '/login',
permanent: false,
},
];
},
};
module.exports = nextConfig;
type には 'header' / 'cookie' / 'query' / 'host' が指定でき、value を付ければ値まで含めた一致条件にできます。ただし、こうしたログイン状態による分岐は毎リクエストの動的な判定になりがちで、後述するように next.config.js はビルド時の静的な設定である点に注意が必要です。複雑な条件分岐は middleware のほうが向く場合もあります。
redirect() 関数や middleware との使い分け
Next.js には URL を制御する方法が複数あり、混同しやすいところです。next.config.js の redirects() / rewrites() のほかに、App Router のサーバーコンポーネントやサーバーアクションで呼ぶ redirect() 関数、そしてリクエストのたびに実行される middleware.ts があります。それぞれ実行タイミングと得意分野が違います。
| 手段 | 実行タイミング | 向いている用途 |
|---|---|---|
next.config.js の redirects / rewrites | ビルド時に確定した静的なルール | URL 構造の移転、パスパターンでの一括転送、外部プロキシ |
redirect() 関数 | サーバーコンポーネント・アクションの処理中 | データ取得や認証結果を見てから条件付きで転送する |
| middleware | 各リクエストの前段(エッジ) | 全リクエスト共通の判定、複雑な条件やロジックを伴う制御 |
使い分けの目安はシンプルです。あらかじめ決まった固定的な URL ルールなら next.config.js が最も手軽で高速です。データベースの状態や認証結果を見て動的に判断したいときは redirect() 関数、すべてのリクエストに共通してロジックを挟みたいときは middleware を選びます。next.config.js の設定はビルド時に固定されるため、実行時に変化する条件で細かく分岐したい場合には向きません。
設定を変えても反映されないとき
next.config.js の redirects() / rewrites() を編集したのに動作が変わらない、という場面はよくあります。原因の多くは開発サーバーの再起動漏れです。
next.config.js はサーバー起動時に読み込まれる
next.config.js はページのコードと違い、サーバーの起動時に一度だけ読み込まれる設定ファイルです。ホットリロードの対象外なので、redirects() や rewrites() を書き換えたら next dev をいったん止めて起動し直す必要があります。本番でも同様で、設定を変えたら再ビルド・再デプロイをしないと反映されません。ファイル名は next.config.js のほか、ESM の next.config.mjs や TypeScript の next.config.ts でも構いませんが、いずれもプロジェクトのルートに置きます。
source は必ずスラッシュ始まりのパスにする
source は /old-path のように必ず先頭スラッシュのパスで書きます。redirects() の permanent を書き忘れるとエラーになるので、恒久か一時かを必ず明示します。また、:path* のワイルドカードを使うときは source と destination でパラメータ名をそろえないと、転送先にうまく引き継がれません。設定を書き換えたら、実際にブラウザで対象 URL にアクセスし、意図した URL とステータスになっているかを確認しましょう。
まとめ
next.config.js の redirects() と rewrites() は、どちらも async 関数としてルールの配列を返す形で書きます。redirects() はブラウザの URL を実際に転送し(permanent: true で 308、false で 307)、URL 構造の引っ越しに使います。rewrites() は URL を変えずに内部的に別パスや外部 URL の内容を表示するマスキングで、API のプロキシなどに向きます。source / destination では :slug や :path* のパターンでまとめて指定でき、外部の絶対 URL をリライト先にすることもできます。動的な条件で分岐したいときは App Router の redirect() 関数や middleware を使い分け、設定を変えたらサーバーの再起動を忘れないようにしましょう。