Astro Middlewareとは?使い方・実行タイミング・認証・SSR/SSGの違いまで徹底解説

Astro
記事内に広告が含まれています。

「Astro Middlewareって何?」「src/middleware.tsはどう書けばいいの?」「SSRでは動くのにSSGでは動かないのはなぜ?」と疑問に感じていませんか。Astro Middlewareは認証やリダイレクト、ヘッダーの追加、アクセス制御など、リクエストごとの共通処理を実装できる便利な機能です。しかし、実行タイミングやSSR・SSGでの違い、defineMiddleware()context.localsの使い方など、理解しておきたいポイントが多く、初めて触れる方は混乱しやすい部分でもあります。

この記事では、Astro Middlewareの基本から実践的な実装方法まで、サンプルコードを交えながら分かりやすく解説します。認証やURLごとのアクセス制御、レスポンスヘッダーの設定、複数Middlewareを連結するsequence()の使い方まで網羅しているので、Astroで安全性と保守性の高いWebアプリを開発したい方はぜひ参考にしてください。

この記事を読んでわかること

  • Astro Middlewareの役割とできること
  • src/middleware.tsの作成方法と基本的な書き方
  • defineMiddleware()onRequest()MiddlewareResponseHandlerの使い方
  • SSR・SSG・API Routesでの実行タイミングと違い
  • CookieやJWTを利用した認証・アクセス制御の実装方法
  • context.localsやレスポンスヘッダーを活用した実践テクニック
  • sequence()を使って複数のMiddlewareを効率よく管理する方法

Astro Middlewareとは?基本知識とライフサイクル

AstroでWebアプリケーションやWebサイトの規模が大きくなると、ページ遷移時やAPIリクエストのたびに発生する「共通処理」の置き場所に悩む場面が増えてきます。例えば、特定のページグループに対するアクセス制限や、リクエストごとのログ収集、Cookieを用いたセッション管理などです。

これらを各ページ(.astro ファイル)やAPIエンドポイントの内部に個別に記述すると、コードの重複が発生し、将来的なメンテナンスコストが跳ね上がります。こうした課題をスマートに解決するために用意されている仕組みが、Astroの「Middleware(ミドルウェア)」です。

Astro Middlewareとは何か

Astro Middlewareは、ユーザーから送られてきたリクエスト(Request)を受信してから、最終的なレスポンス(Response)をブラウザへ返すまでの間に割り込んで実行される共通処理の仕組みです。

各リクエストがターゲットとなるページやAPI Routesに到達する前に、リクエスト情報の検証、書き換え、あるいはレスポンスの生成自体をインターセプト(遮断・横取り)して肩代わりすることができます。

Express.jsやKoa、Next.jsなどのWebフレームワークを触ったことがあるエンジニアなら、リクエストとレスポンスの間に処理を挟む構造そのものはイメージしやすいはずです。Astro Middlewareは、Astro独自の強力な context オブジェクトを介して、CookieやURL、後述する context.locals といったサーバーサイドの情報を極めて直感的に扱える点が特徴になっています。

ミドルウェア
Astroでミドルウェアを使う方法を学びます。

Astro Middlewareで何ができるのか

ミドルウェアの最大の強みは、個々のページコンポーネント側のロジックを汚すことなく、アプリケーション全体の横断的な関心事(Cross-Cutting Concerns)を1箇所に集約できる点にあります。

主に以下のようなユースケースでその真価を発揮します。

用途具体的な処理内容
アクセス制御・認証ログインセッションやJWT(JSON Web Token)の検証、未認証ユーザーのリダイレクト、ロール(権限)ベースの閲覧制限
セキュリティ・ヘッダーセキュリティ向上のためのレスポンスヘッダー(CSP: Content Security Policy、HSTSなど)の自動一括注入
リクエストの書き換え特定の条件下でのURLリライト、クエリパラメータの調整、カスタムヘッダーの追加
多言語化(i18n)サポートユーザーのブラウザ言語やCookie、パスプレフィックスから最適な言語を判定し、適切なページへリダイレクトまたは言語状態を保持
パフォーマンス・ログ分析リクエストの処理時間(計測)の測定、アクセスログの自動記録、エラーのトラッキング

リクエストとレスポンスのライフサイクル

ミドルウェアの基本的な処理の流れは非常にシンプルです。

  1. リクエストの受信: クライアント(ブラウザ)からサーバーに向けてリクエストが送信されます。
  2. ミドルウェアの起動: ページやAPI Routesの処理が走る前に、src/middleware.ts に定義された onRequest 関数が最優先で呼び出されます。
  3. 境界の制御(next()): * 後続へ進める場合: ミドルウェア内で await next() を呼び出すことで、ターゲットであるページコンポーネントやAPI Routesへと処理を安全にフォワード(移譲)します。
    • 途中で遮断する場合: next() を呼び出さずに、その場で new Response("Unauthorized", { status: 401 }) などを返して処理を終了(アーリーリターン)させることができます。

この「リクエストを途中で受け止め、必要に応じて処理を中断できる」という性質こそが、Webアプリケーションの入り口を守り、効率化するための最も堅牢なアプローチとなります。

◆◇◆ 【衝撃価格】VPS512MBプラン!1時間1.3円【ConoHa】 ◆◇◆

ミドルウェアの基本実装と配置ルール

Astro Middlewareの基本概念を理解したところで、実際にプロジェクトへ導入する手順と、最もシンプルな最小構成の実装方法を見ていきましょう。

Astroでは、設定ファイル(astro.config.mjs)などにミドルウェアの有効化を記述する必要はありません。特定の場所に特定の名前でファイルを配置するだけで、Astroが自動的にそれを検知して処理パイプラインに組み込みます。

ファイルの配置ルール

ミドルウェアを機能させるには、プロジェクトのソースディレクトリ(通常は src)の直下に、middleware.ts(または JavaScriptを使用している場合は middleware.js)という名前のファイルを配置します。

my-astro-project/
├── src/
│   ├── pages/
│   │   ├── index.astro
│   │   └── about.astro
│   ├── middleware.ts  <-- ★必ず「src」直下にこの名前で配置する
│   └── env.d.ts
├── package.json
└── astro.config.mjs

⚠️ 注意点

src/pages/middleware.tssrc/components/middleware.ts のように、他のディレクトリ配下に置いても動作しません。必ず src/middleware.ts に配置してください。

最小構成の実装コード

最もシンプルに、すべてのリクエストをそのまま後続の処理(ページやAPI)へパススルーするミドルウェアのコードは以下のようになります。

// src/middleware.ts
import { defineMiddleware } from "astro:middleware";

export const onRequest = defineMiddleware(async (context, next) => {
  console.log(`[Request] ${context.request.method} - ${context.url.pathname}`);

  // 後続のレンダリングやAPI処理を呼び出し、そのレスポンスを受け取る
  const response = await next();

  return response;
});

コードの解説

  1. defineMiddleware 関数の利用: Astroが提供するヘルパー関数です。これを使用することで、引数の contextnext、および関数の戻り値に対して、TypeScriptの型定義が自動的に正しく適用されます。
  2. onRequest のエクスポート: Astroは middleware.ts から名前付きエクスポートされた onRequest という名前の関数を探索して実行します。この名前は固定であり、変更することはできません。
  3. context オブジェクト: リクエスト情報(request)、現在のURL(url)、Cookie(cookies)、および後述するローカル変数(locals)など、サーバーサイドで必要なすべてのコンテキスト情報が詰まったオブジェクトです。
  4. next() 関数: ミドルウェアの処理を完了し、次のミドルウェア(複数連結している場合)や、ターゲットとなるページ/APIルートのレンダリング処理へ制御を移すための関数です。非同期で動作するため、必ず await next() として呼び出します。

リクエストのインターセプト(割り込み・遮断)

ミドルウェアの真価は、特定の条件に基づいてリクエストを途中で遮断し、独自のレスポンスを返せる点にあります。

例えば、特定のパス(/secret)へのアクセスを検知した際に、特定のクエリパラメータがなければその場で「403 Forbidden」を返す実装は以下のようになります。

// src/middleware.ts
import { defineMiddleware } from "astro:middleware";

export const onRequest = defineMiddleware(async (context, next) => {
  const url = new URL(context.request.url);

  // /secret から始まるパスへのアクセスを監視
  if (url.pathname.startsWith("/secret")) {
    const token = url.searchParams.get("token");

    // 適切なトークンがない場合は、ページ処理を呼び出さずにその場でレスポンスを返す
    if (token !== "super-secret-key") {
      return new Response("Access Denied: Invalid Token", { status: 403 });
    }
  }

  // 条件をクリアしていれば通常通りページを表示
  return await next();
});

このように、ターゲットとなる .astro テンプレートファイルの処理が一切走る前にリクエストをフィルタリングできるため、無駄なサーバーリソースの消費や情報漏洩を未然に防ぐことができます。

◆◇◆ 【衝撃価格】VPS512MBプラン!1時間1.3円【ConoHa】 ◆◇◆

【実用例】実務で使えるユースケース

ここからは、実際のプロジェクトや本番運用のWebサイトで、そのまま導入・活用できる実用的なミドルウェアの実装例を3つ紹介します。

  1. Cookieを使用したトークン認証とアクセス制限
  2. セキュリティヘッダー(CSP)の自動注入
  3. 不要なリクエストの除外(アセットバイパス)

Cookieを使用したトークン認証とアクセス制限

管理画面やユーザー専用のダッシュボード(例:/dashboard 配下のページ)に対して、特定のログインCookie(JWTやセッショントークン)がなければ自動的にログイン画面へリダイレクトさせる仕組みです。

// src/middleware.ts
import { defineMiddleware } from "astro:middleware";

export const onRequest = defineMiddleware(async (context, next) => {
  const url = new URL(context.request.url);

  // ダッシュボード(/dashboard)配下のページのみに制限を適用
  if (url.pathname.startsWith("/dashboard")) {
    // Cookieからセッショントークンを取得
    const sessionToken = context.cookies.get("session_id")?.value;

    // トークンが存在しない、または無効な場合はログイン画面へリダイレクト
    if (!sessionToken || !isValidToken(sessionToken)) {
      // ログイン後の戻り先URLをクエリとして保持させつつリダイレクト
      return context.redirect(`/login?redirectTo=${encodeURIComponent(url.pathname)}`, 302);
    }

    // トークンが有効な場合、認証済みユーザー情報を後続の処理に渡す(詳細はセクション5で解説)
    context.locals.user = {
      id: "usr_999",
      name: "山田 太郎",
      role: "admin"
    };
  }

  return await next();
});

// 簡易的なトークン検証用ダミー関数
function isValidToken(token: string): boolean{
  // 本番環境ではJWTのデコードやセッションDB・キャッシュとの照合を行います
  return token === "valid-secret-token";
}

この実装を行うことで、/dashboard 内にどれだけ多くのページ(/dashboard/settings/dashboard/analytics など)を作成しても、それぞれに認証ロジックを書く必要がなくなります。

セキュリティヘッダー(CSP: Content Security Policy)の注入

クロスサイトスクリプティング(XSS)などの脆弱性を防ぐためのセキュリティヘッダーを、すべてのページレスポンスへ自動的かつ一元的に付与する実装です。

ミドルウェアは await next() を実行することで、後続の処理(ページレンダリング)が生成したレスポンスオブジェクトを受け取ることができます。これを加工してからクライアントに返すアプローチを取ります。

// src/middleware.ts
import { defineMiddleware } from "astro:middleware";

export const onRequest = defineMiddleware(async (context, next) => {
  // 1. ページやAPIの通常レンダリングを行い、レスポンスを取得
  const response = await next();

  // 2. セキュリティヘッダーをレスポンスに注入する
  // 読み込みを特定の信頼されたオリジンのみに制限する設定
  response.headers.set(
    "Content-Security-Policy",
    "default-src 'self'; script-src 'self' <https://trusted-scripts.com>; style-src 'self' 'unsafe-inline';"
  );
  response.headers.set("X-Frame-Options", "DENY");
  response.headers.set("X-Content-Type-Options", "nosniff");
  response.headers.set("Referrer-Policy", "strict-origin-when-cross-origin");

  // 3. ヘッダーが追加されたレスポンスをクライアントへ返す
  return response;
});

ページを個別に作成する際、開発者がヘッダーの付与漏れを心配する必要がなくなるため、サイト全体のセキュリティレベルを均一に保てます。

静的アセットへのリクエストの除外(アセットバイパス)

Astro Middlewareは、HTMLページの読み込みだけでなく、画像ファイル、CSS、JavaScript、アイコン(favicon.ico)といったすべての静的ファイルへのリクエストに対しても毎回実行されます。

認証チェックやDB照合、重いロギングなどの処理がそれら全てのアセットリクエストに対して動作すると、サイト全体のパフォーマンスが大幅に低下します。そのため、アセットへのアクセスは処理の最序盤で即座にバイパス(パススルー)するのが鉄則です。

// src/middleware.ts
import { defineMiddleware } from "astro:middleware";

export const onRequest = defineMiddleware(async (context, next) => {
  const url = new URL(context.request.url);

  // 以下のパスや拡張子に対するリクエストは、余計な処理をせず即座にパススルーする
  const isAsset =
    url.pathname.startsWith("/_astro") || // Astroが生成するビルド後のアセット
    url.pathname.startsWith("/api") ||    // APIエンドポイント(必要に応じて)
    /\.(png|jpe?g|gif|svg|webp|ico|css|js|woff2?)$/i.test(url.pathname); // 静的ファイル拡張子

  if (isAsset) {
    return await next(); // 後続処理を呼ぶだけで、ミドルウェアのメインロジックはスキップ
  }

  // ─── これ以降に、認証やロギングなどの「重い共通処理」を記述する ───
  console.log("アセット以外のアクセスに対してのみこのログが出力されます:", url.pathname);

  return await next();
});

この「アセットのバイパス処理」をミドルウェアの先頭に1枚挟んでおくだけで、開発環境でのリクエストの詰まりや、本番環境での不要なサーバー負荷を劇的に軽減できます。

応用:複数のミドルウェアを連結する(sequence)

Astroで開発を進めていくと、「セキュリティヘッダーの付与」「認証チェック」「アクセスログの記録」といった独立した複数の共通処理をミドルウェアとして実行したくなります。

これらをすべて1つの onRequest 関数の中にダラダラと書き連ねてしまうと、コードの見通しが悪くなり、再利用性も低下します。

こうした事態を避けるために、Astroでは複数のミドルウェアを機能ごとに分割し、それらを直列に連結(チェイニング)して実行するための sequence() という便利なユーティリティを提供しています。

sequence() とは

sequence() は、引数に渡された複数のミドルウェア関数を、記述した順番に上から実行していく仕組みです。

import { sequence } from "astro:middleware";

export const onRequest = sequence(middlewareA, middlewareB, middlewareC);

このように設定すると、リクエストは middlewareAmiddlewareBmiddlewareC の順番で通過し、最終的なページやAPI Routesに到達します。

オニオンアーキテクチャ(実行順序の仕組み)

sequence() を理解するうえで最も重要なのが、リクエストとレスポンスがミドルウェアを通過する方向です。ミドルウェアの連鎖は、よく「玉ねぎの皮(オニオンアーキテクチャ)」に例えられます。

  • リクエスト(往路): sequence() に登録された「左から右(上から下)」の順番で処理が進みます。
  • ページ処理: すべてのミドルウェアの next() が呼び出された後、実際のページがレンダリングされます。
  • レスポンス(復路): レンダリングが完了したレスポンスは、今度は「右から左(下から上)」、つまり逆順でミドルウェアを遡って戻っていきます。

実行の流れ(イメージ)

この往路と復路の性質があるため、「ログの計測開始は往路の最初に行い、計測結果の出力は復路の最後(=Middleware Aの戻り時)に行う」といった、高度な処理をスマートに実装することができます。

sequence() を使った分割実装例

それでは、実際にミドルウェアを3つの機能(ロガー、認証、セキュリティ)に分割し、sequence() で連結する具体的なコードを見ていきましょう。

それぞれのミドルウェアは、defineMiddleware ヘルパーを用いて個別に定義します。こうすることで、各パーツの型安全性を保ちつつ、単体でのテストや再利用が容易になります。

// src/middleware.ts
import { defineMiddleware, sequence } from "astro:middleware";

// 1. 処理時間を計測・記録するロガー・ミドルウェア
const logger = defineMiddleware(async (context, next) => {
  const startTime = Date.now();
  console.log(`--> [Start] ${context.url.pathname}`);

  const response = await next(); // 後続の処理を実行

  const duration = Date.now() - startTime;
  console.log(`<-- [End] ${context.url.pathname} (${duration}ms)`);

  return response;
});

// 2. 簡易的な認証チェック・ミドルウェア
const authenticator = defineMiddleware(async (context, next) => {
  const url = new URL(context.request.url);

  // 認証が必要なエリアへのアクセス制限
  if (url.pathname.startsWith("/dashboard")) {
    const session = context.cookies.get("session_id")?.value;
    if (!session) {
      console.log("❌ 未認証アクセスのためリダイレクトします");
      return context.redirect("/login", 302);
    }
  }

  return await next();
});

// 3. セキュリティヘッダーを注入するミドルウェア
const securityHeaders = defineMiddleware(async (context, next) => {
  const response = await next(); // ページレンダリング結果を取得

  // レスポンスヘッダーにセキュリティ設定を追加(復路での処理)
  response.headers.set("X-Frame-Options", "DENY");
  response.headers.set("X-Content-Type-Options", "nosniff");

  return response;
});

// 最後に、実行したい順番で sequence に渡して一括エクスポートする
export const onRequest = sequence(logger, authenticator, securityHeaders);

この実装における処理の推移

  1. 往路の開始: リクエストが届くと、まず logger が「開始ログ」を出力します。
  2. logger 内の await next() により、制御が次の authenticator に移ります。
  3. authenticator がパスを判定し、問題なければ await next()securityHeaders に制御を移します。
  4. securityHeadersawait next() を呼び、ここで初めて実際のページ(.astro)のレンダリングが実行され、ベースとなるレスポンスが生成されます。
  5. 復路の開始: 生成されたレスポンスが securityHeaders に戻り、ここで各種ヘッダーが注入されます。
  6. レスポンスがさらに手前の authenticator を通過し(ここでは特に何もしない)、最後に logger に戻ります。
  7. logger で処理時間の「終了ログ」が出力され、最終的なレスポンスがブラウザへ送信されます。

このように sequence() を利用すると、ミドルウェアのロジックを綺麗に分離でき、要件の追加や順序の入れ替え(例:ロギングを一番外側にするか、内側にするか)にも柔軟に対応できるようになります。

国内シェアNo.1のエックスサーバーが提供するVPSサーバー『XServer VPS』

TypeScriptによる型拡張(App.Locals)

Astro Middlewareの非常に強力な機能のひとつに、リクエストの「往路」で取得・生成したデータを context.locals に格納し、後続のページ(.astro ファイル)やAPI Routesへと引き渡す仕組みがあります。

しかし、TypeScriptプロジェクトでこれをそのまま使おうとすると、context.locals の中身がデフォルトでは型定義されておらず、プロパティにアクセスする際にコンパイルエラーになったり、自動補完(IntelliSense)が効かなかったりします。

このセクションでは、context.locals を安全に扱うために不可欠な「TypeScriptの型拡張(アンビエント宣言)」の具体的な手順を解説します。

context.locals とは

context.locals は、同一のリクエストサイクル内(ミドルウェアからターゲットのページがレスポンスを返すまで)だけで共有される、一時的なオブジェクトの保管庫です。

例えば、ミドルウェアでCookie内のセッショントークンを検証し、DBからユーザー情報を取得したとします。そのユーザー情報を context.locals に保存しておくことで、実際の表示を担当する .astro ページ側で再度のDB問い合わせを行うことなく、即座にログインユーザー名などを描画できます。

env.d.ts で型定義を拡張する

Astroプロジェクトのルート(または src ディレクトリ直下)には、通常 env.d.ts という型定義ファイルが用意されています。このファイルの中で、グローバルな名前空間 App の中の Locals インターフェースを拡張(マージ)します。

以下のように、型定義を追加してください。

// src/env.d.ts

/// <reference types="astro/client" />

declare namespace App {
  // Astroが内部で参照する Locals インターフェースを拡張
  interface Locals {
    // 1. 認証済みユーザー情報の型定義
    user?: {
      id: string;
      name: string;
      role: "admin" | "user";
    } | null;

    // 2. リクエストごとの一意なトラッキングIDなど(任意)
    requestId?: string;
  }
}

💡 ポイント

declare namespace App を用いて Locals インターフェースを定義すると、TypeScriptは既存のAstroの型定義とこの記述を自動的に結合(宣言マージ)します。これにより、プロジェクト内のすべてのファイルで locals のプロパティが型安全になります。

ミドルウェア側でのデータ格納の実装

型定義が完了したら、ミドルウェア(src/middleware.ts)の中で実際にデータを格納してみましょう。

// src/middleware.ts
import { defineMiddleware } from "astro:middleware";

export const onRequest = defineMiddleware(async (context, next) => {
  // 型定義のおかげで、エディタ上で .user や .requestId が自動補完されます
  context.locals.requestId = crypto.randomUUID();

  // ログインCookieを検証するダミー処理
  const sessionCookie = context.cookies.get("session_id")?.value;

  if (sessionCookie === "valid-user") {
    context.locals.user = {
      id: "usr_123",
      name: "開発 太郎",
      role: "user",
    };
  } else {
    // 未ログイン時は明示的に null をセット(または undefined)
    context.locals.user = null;
  }

  return await next();
});

Astroページ(.astro)やAPIでのデータ取り出し

ミドルウェアが locals に格納したデータは、各 .astro コンポーネントのフロントマター(コードフェンス)や、APIエンドポイントのハンドラー内で簡単に取り出すことができます。

.astro ファイルでの利用例(src/pages/dashboard.astro

---
// Astro.locals からミドルウェアでセットしたデータを取り出す
// ここでも完全な型安全が保証され、`user.name` などの自動補完が効きます
const { user, requestId } = Astro.locals;

// 未ログインユーザーはダッシュボードを表示させない(二重の防御)
if (!user) {
  return Astro.redirect("/login");
}
---

<layout title="ダッシュボード">
  <main>
    <h1>おかえりなさい、{user.name} さん!</h1>
    <p>あなたの権限: <strong>{user.role}</strong></p>

    <footer class="text-xs text-gray-400">
      Request ID: {requestId}
    </footer>
  </main>
</layout>

APIエンドポイント(src/pages/api/v1/data.ts)での利用例

import type { APIRoute } from "astro";

export const GET: APIRoute = async (context) => {
  // API Routesでも、引数の context.locals から全く同じデータにアクセス可能
  const user = context.locals.user;

  if (!user) {
    return new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401 });
  }

  return new Response(JSON.stringify({
    message: "データ取得成功",
    data: [1, 2, 3],
    requestedBy: user.name
  }));
};

このように、TypeScriptの型拡張を正しく行うことで、データの受け渡しプロセス全体が非常に堅牢になります。タイプミスによる「動かない」トラブルを未然に防ぎ、開発体験(DX)が飛躍的に向上するため、Astroでミドルウェアを使用する際は必ずセットで設定するようにしましょう。

あなたのサイトのURL、そろそろスリムにしませんか?

よくある質問(FAQ)

ミドルウェアの中で「Astro」グローバルオブジェクトは使える?

いいえ、使えません。代わりに引数の context を使用してください。

.astro ファイルのフロントマターでお馴染みの Astro オブジェクト(Astro.cookiesAstro.request など)は、ページコンポーネントのレンダリングコンテキストにバインドされたグローバルオブジェクトです。

ミドルウェアは、レンダリング処理が始まるよりさらに手前(HTTPサーバーレイヤーに近い場所)で動作するため、この Astro グローバルは存在しません。ミドルウェア内でリクエスト情報やCookieを操作する際は、必ず関数の第一引数として渡される context(例:context.requestcontext.cookies)を使用してください。

Requestのボディ(JSONやFormData)を読み取ったら、後続のページでエラーが出るのはなぜ?

Request のボディ(ストリーム)は仕様上「1回しか読み取れない」ためです。ミドルウェアで読み取ったデータを locals に退避させて解決します。

JavaScriptの標準仕様(WHATWG Stream API)に基づき、リクエストオブジェクトの request.json()request.formData() などを一度呼び出すと、そのリクエストのボディストリームは「消費済み(Consumed)」ステータスになります。

そのため、ミドルウェアでボディを解析し、その後 await next() で遷移した先のページやAPI Routesで再度ボディを読み取ろうとすると、ブラウザやサーバーは「すでに消費されています」というエラーを吐いてクラッシュします。

❌ NG:ミドルウェアとページの両方で直接読み取ろうとする

// src/middleware.ts
export const onRequest = defineMiddleware(async (context, next) => {
  if (context.request.method === "POST") {
    const data = await context.request.json(); // ここでストリームを消費
    console.log("Logged body:", data);
  }
  return await next(); // ➔ 遷移先で再度 context.request.json() を呼ぶとエラー!
});

⭕️ OK:読み取った内容を context.locals に格納して引き渡す

一度ミドルウェア側でパースしたデータを locals に格納しておき、後続の処理では request から直接ではなく locals からデータを引き出す設計にします。

// src/middleware.ts
export const onRequest = defineMiddleware(async (context, next) => {
  if (context.request.method === "POST") {
    try {
      const data = await context.request.json();
      context.locals.postBody = data; // localsに安全に退避
    } catch (e) {
      context.locals.postBody = null;
    }
  }
  return await next();
});

ミドルウェアの中でエラー(例外)が発生した場合、どうなりますか?

開発環境ではエラー画面(オーバーレイ)が表示され、本番環境では「500 Internal Server Error」として処理がクラッシュします。必ず try...catch で安全なフォールバックを実装しましょう。

ミドルウェア内で発生した未捕捉(Unhandled)の例外は、アプリケーションサーバー全体のレスポンスを即座にストップさせてしまいます。例えば、JWTのデコード処理や、外部セッションDBへの接続がネットワークエラーなどで一時的に失敗した際、ページ全体が真っ白な500エラー画面になってしまうのは防がなければなりません。

認証や外部API接続などの失敗しうる処理は、必ず try...catch で囲み、エラー時でも「未ログイン(null)の安全な状態」として次の処理へ流す(または特定のログインエラーページへリダイレクトする)設計にしてください。

// src/middleware.ts
export const onRequest = defineMiddleware(async (context, next) => {
  try {
    const user = await fetchUserProfile(context.cookies.get("session")?.value);
    context.locals.user = user;
  } catch (error) {
    console.error("ミドルウェアでエラーが発生しました:", error);
    // エラー時は未ログイン状態としてフォールバックし、後続へ処理を移譲する
    context.locals.user = null;
  }

  return await next();
});

SSG(静的ビルド)環境での挙動はどうなる?ランタイムでも動く?

SSG(output: 'static')環境では、ミドルウェアは「ビルド時(astro build)」にしか動作しません。

Astroの最大の魅力であるSSG(静的サイト生成)ですが、ミドルウェアを組み合わせる際には動作条件に注意が必要です。

Astroが完全に静的なサイトとしてビルドされる場合、ミドルウェアの処理はビルドプロセスの中で全ページ分のHTMLを生成するタイミングで1度だけ実行されます。

そのため、デプロイされた本番環境(CDNや静的ホスティング先)において、ユーザーがページにアクセスしたタイミング(ランタイム)でCookieを読み取ってパーソナライズしたり、動的にリダイレクトしたりすることは不可能です。

ランタイムで毎回リアルタイムにミドルウェアを実行したい場合は、必ず astro.config.mjsoutput: 'server'(SSR) または output: 'hybrid'(ハイブリッド) を設定し、SSRに対応したNode.jsやVercel、Cloudflare Pagesなどのサーバー環境にデプロイする必要があります。

Astro標準のi18n機能と、ミドルウェアによる多言語化は何が違う?

標準i18nは「パスベース(静的プレフィックス)」の制御を行い、ミドルウェアは「ユーザーの動的なコンテキスト(Cookieやヘッダー)」に基づいた高度な制御を行います。

Astroには標準で強力な多言語(i18n)ルーティング機能が組み込まれています。これは /ja/about/en/about といった、URLのパスに基づいて静的に言語を切り替えるのに最適です。基本的にはまずこの標準機能を利用することをお勧めします。

一方で、ミドルウェアによる多言語化が必要になるのは、以下のような「動的な判定」を行いたい場合です。

  • ユーザーが初めてルートURL(/)にアクセスした際、ブラウザの Accept-Language ヘッダーを読み取り、最適な言語のパス(/ja/ など)にサーバーサイドで即座にリダイレクトする。
  • ユーザーが手動で設定した優先言語(Cookieに保存されている情報)を読み込み、URLを書き換えることなくレスポンスを出し分ける。

静的な言語ルーティングはAstroの標準機能に任せ、そこに至るまでの「ユーザーのコンテキスト解析とファーストタッチのリダイレクト」をミドルウェアが担当するという、両者の特性を活かした役割分担が最もスマートなアプローチになります。

まとめ:Astro Middlewareでサーバーサイドを一元管理する

Astro Middlewareは、個々のページやAPI Routesから横断的な処理を分離し、サーバーサイド(またはエッジ環境)で一括制御するための極めて強力な仕組みです。

重要なポイント

  • 配置ルールはシンプル: src/middleware.ts という名前で、必ず src 直下に配置します。
  • 強力な共通処理の集約: 認証制限、セキュリティヘッダーの追加、さらにアセットファイル(/_astro)の効率的なバイパス処理などを1箇所にまとめられます。
  • 複数ミドルウェアの直列連結: sequence() を利用することで、オニオン(玉ねぎ)構造のようにリクエストとレスポンスの往路・復路をコントロールし、コードの責務を綺麗に分離できます。
  • context.locals による型安全なバトン渡し: env.d.tsApp.Locals の型定義を拡張(マージ)することで、ミドルウェアからページコンポーネントへ安全にデータを共有できます。
  • 実務での落とし穴の回避: 「リクエストボディの使い切り制約」や「未捕捉エラーによる500クラッシュ」といった標準の挙動を正しく理解し、堅牢なフォールバックを組み込むことが大切です。

ミドルウェアを導入することで、これまで各 .astro ファイルのフロントマターにコピペしていたコードや、複雑になりがちだった認証ロジックがすっきりと整理されます。

まずは、最もシンプルな「アクセスロガー」や「静的アセットのバイパス」の実装から始めてみてはいかがでしょうか。Astroが持つ「静的プレハブ」のパフォーマンスと、サーバーサイドによる「動的な制御」の両輪を最大限に活かし、スマートで安全なWebサイト・Webアプリケーションを構築してください!

Audibleプレミアムプラン30日間無料体験
タイトルとURLをコピーしました