前提知識
ミドルウェアとは?初心者向けガイド
ミドルウェアの基本、SaaSで使われる理由、Next.jsのi18nとrequest IDを扱うミドルウェアの仕組み、そして安全なカスタマイズ方法を解説します。
一言でいうと
ミドルウェアは、リクエストと最終的なハンドラの間で動くコードです。ページやAPIが動く前に、検査・リダイレクト・ヘッダー追加ができます。
Middleware 101(初心者視点)
リクエストを道路を走る車だとすると、ミドルウェアは検問所です。すべてのリクエストが通過し、そこで内容を確認したり判断したり、必要ならレスポンスを変えたり(早めに止めたり)できます。
Webアプリでの代表的な用途:
- 認証ゲート: サインイン済みか確認し、未ログインならリダイレクト。
- 国際化(i18n): URLや設定に応じて適切なロケールへ誘導。
- ロギングとトレーシング:
request_idを付与してログを相関。 - フィーチャーフラグ / A/Bテスト: 早い段階でユーザーを振り分け。
- セキュリティとレート制限: 悪質なトラフィックを遮断、ヘッダーを強制。
Next.jsでは middleware.ts という特別なファイルに書き、エッジで(非常に早く)実行されます。リダイレクト/リライト、ヘッダーの読み書き、レスポンスのショートサーキットを、ハンドラの前に行えます。
どんなときに役立つか
ミドルウェアは横断的な関心ごと(多くのルートに共通する処理)を集約できます。結果として:
- ルールの一元化: authやロケールの抜け漏れが減る。
- 観測性の統一: すべてのリクエストに
request_idを付けられる。 - 早い判断: エッジでリダイレクトや拒否を行い、サーバー負荷を減らす。
- ハンドラがすっきり: ビジネスロジックに集中できる。
SaaSで特に効くケース:
/:locale/...を維持したい多言語アプリ。- リクエスト単位の相関IDが必要なログ/トレース。
- 有料機能や管理エリアのゲート(Cookieは慎重に扱う)。
- A/B、地域ルーティング、ヘッダー正規化。
現在のミドルウェアの役割
src/middleware.ts に、next-intl のi18nルーティングと軽量なrequest ID付与を組み合わせた安全なデフォルトを用意しています。
主な挙動:
- ロケールルーティング:
next-intlに委譲し、src/i18n/routing.tsとsrc/i18n/locale.tsを参照します。/en/...や/fr/...を一貫させ、必要なら自動検出も可能。 - Request ID: すべてのレスポンスに
x-request-idを付与。すでにあれば再利用し、なければUUIDを生成。 - 保守的な matcher: ローカライズされたページと一般ルートのみ対象。
api、_next、アセット、adminは除外。
現在のコード(要点のみ):
// src/middleware.ts
import createMiddleware from 'next-intl/middleware';
import { NextRequest } from 'next/server';
import { routing } from '@/i18n/routing';
const intlMiddleware = createMiddleware(routing);
export default function middleware(request: NextRequest) {
const existing = request.headers.get('x-request-id');
const requestId = existing || crypto.randomUUID();
const res = intlMiddleware(request);
// 可視化のみ。リクエストのcookie/headersは変更しない。
res.headers.set('x-request-id', requestId);
return res;
}
export const config = {
matcher: [
'/',
'/(en|en-US|zh|zh-CN|zh-TW|zh-HK|zh-MO|ja|ko|ru|fr|de|ar|es|it)/:path*',
'/((?!api|_next|_vercel|admin|.*\\..*).*)',
],
};ロケール設定:
// src/i18n/routing.ts
import { defaultLocale, localeDetection, localePrefix, locales } from './locale';
import { defineRouting } from 'next-intl/routing';
export const routing = defineRouting({ locales, defaultLocale, localePrefix, localeDetection });ロケール定義(追加/削除する場所):
// src/i18n/locale.ts
export const locales = ['en', 'zh', 'es', 'fr', 'ja'];
export const defaultLocale = 'en';
export const localePrefix = 'always';
export const localeDetection = process.env.NEXT_PUBLIC_LOCALE_DETECTION === 'true';注意点:
- Better Auth を安定させるため、ミドルウェアでリクエストのCookieやヘッダーは変更しません。レスポンスヘッダーのみ付与します。
- matcher は
api、_next、アセット、adminを除外しています。必要なら調整してください。
安全にカスタマイズする方法
まず、その処理がミドルウェア向きか(横断的で早期判断が必要か)を確認しましょう。適切なら次のパターンが使えます。
カスタムヘッダーの追加
export default function middleware(request: NextRequest) {
const res = intlMiddleware(request);
res.headers.set('x-feature-flag', 'on');
return res;
}リダイレクト/リライト
import { NextResponse } from 'next/server';
export default function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
if (pathname === '/old') {
return NextResponse.redirect(new URL('/new', request.url));
}
return intlMiddleware(request);
}matcher で対象を調整
export const config = {
matcher: [
'/',
// admin を対象に戻すには negative lookahead から外すか
// 明示的にマッチさせる:
'/admin/:path*',
'/(en|fr|ja|zh|es)/:path*',
'/((?!api|_next|_vercel|.*\\..*).*)',
],
};request ID をハンドラで使う
// Node のルートハンドラで
import { headers } from 'next/headers';
export async function GET() {
const h = headers();
const request_id = h.get('x-request-id');
// ログやDBトレースに付与
return new Response('ok');
}国際化の調整
NEXT_PUBLIC_LOCALE_DETECTIONをtrueにして自動検出を有効化。src/i18n/locale.tsでロケールの追加/削除。- 新しいロケール追加時は
content/docs、messages/、src/i18nを同期。
認証などのセンシティブな処理
- ミドルウェアでの認証ゲートはCookieの変更を避け、リダイレクトや読み取りのみで行う。
- 保護APIはハンドラ側でも必ず検証する(ミドルウェアは補助でしかない)。
ミドルウェアを使わない方が良い場面
- ルート固有のロジックで、横断的ではない。
- 重い非同期処理(DBアクセスなど)。
- 大きなリクエストボディの変換(ミドルウェアでは読めない)。
ミドルウェアは小さく、速く、集中させるのが基本です。複雑化したら、サービス関数に移してハンドラから呼び出しましょう。
TL;DR
- ミドルウェアは早く・中央で動くので、i18n、ヘッダー、リダイレクト、軽いガードに最適。
- デフォルトのミドルウェアはロケールルーティングと
x-request-idを提供。 config.matcher、レスポンスヘッダー、安全なリダイレクトでカスタマイズ。認証Cookieは触らない。- 小さく、速く、ロケール間で一貫させる。