Astro 5.1 Middleware でレート制限・キャッシュ制御を実装する【2026年6月最新】
Astro 5.1のMiddleware機能を使ってレート制限とキャッシュ制御を実装する方法を解説。エッジ環境での動的制御とCloudflare KV連携の実践ガイド。
Astro 5.1のMiddleware機能を使うと、エッジ環境でのレート制限やキャッシュ制御を効率的に実装できます。この記事では、2026年6月時点の最新仕様に基づき、実際に動作するコード例とともに実装方法を解説します。
Astro Middlewareの基本構造とレート制限の実装パターン
Astro 5系のMiddlewareはsrc/middleware.tsまたはsrc/middleware.jsに定義し、すべてのリクエストに対して実行されます。レート制限を実装する場合、エッジ環境での永続化ストレージとしてCloudflare KV(Key-Value Store)やDurable Objectsを活用するのが一般的です。
レート制限の基本実装
// src/middleware.ts
import type { MiddlewareHandler } from 'astro';
const RATE_LIMIT_WINDOW = 60; // 60秒
const MAX_REQUESTS = 10; // 最大10リクエスト
export const onRequest: MiddlewareHandler = async (context, next) => {
const { request, locals } = context;
const clientIP = request.headers.get('CF-Connecting-IP') || 'unknown';
// Cloudflare KVを使ったレート制限チェック
const rateLimitKey = `rate_limit:${clientIP}`;
const kvNamespace = (context.locals.runtime as any)?.env?.KV;
if (kvNamespace) {
const currentCount = await kvNamespace.get(rateLimitKey);
const count = currentCount ? parseInt(currentCount) : 0;
if (count >= MAX_REQUESTS) {
return new Response('Too Many Requests', {
status: 429,
headers: {
'Retry-After': RATE_LIMIT_WINDOW.toString(),
'X-RateLimit-Limit': MAX_REQUESTS.toString(),
'X-RateLimit-Remaining': '0'
}
});
}
// カウンタを更新(TTL付き)
await kvNamespace.put(
rateLimitKey,
(count + 1).toString(),
{ expirationTtl: RATE_LIMIT_WINDOW }
);
}
return next();
};
この実装では、クライアントIPアドレスごとに60秒間で最大10リクエストまで許可します。CloudflareのCF-Connecting-IPヘッダーを使うことで、プロキシ経由でも正確なクライアントIPを取得できます。
レート制限フロー図
flowchart TD
A["クライアントリクエスト"] --> B["Middleware実行"]
B --> C{"KVからカウント取得"}
C --> D{"制限超過?"}
D -->|Yes| E["429エラー返却"]
D -->|No| F["カウント+1してKVに保存"]
F --> G["次の処理へ"]
E --> H["クライアントにエラー"]
G --> I["ページレンダリング"]
キャッシュ制御ヘッダーの動的設定
Astro 5.1のMiddlewareでは、リクエストパスやコンテンツタイプに応じてキャッシュ制御ヘッダーを動的に設定できます。静的アセットと動的コンテンツで異なるキャッシュ戦略を適用する実装例を示します。
コンテンツタイプ別のキャッシュ制御
// src/middleware.ts
import type { MiddlewareHandler } from 'astro';
const CACHE_STRATEGIES = {
static: 'public, max-age=31536000, immutable', // 1年(静的アセット)
dynamic: 'public, max-age=3600, stale-while-revalidate=86400', // 1時間(動的ページ)
api: 'private, no-cache, no-store, must-revalidate', // APIエンドポイント
} as const;
export const onRequest: MiddlewareHandler = async (context, next) => {
const { request } = context;
const url = new URL(request.url);
const pathname = url.pathname;
// 静的アセットの判定
const isStaticAsset = /\.(js|css|png|jpg|jpeg|gif|svg|webp|woff2?|ttf|eot)$/i.test(pathname);
const isApiRoute = pathname.startsWith('/api/');
const response = await next();
// キャッシュ制御ヘッダーを設定
const cacheControl = isStaticAsset
? CACHE_STRATEGIES.static
: isApiRoute
? CACHE_STRATEGIES.api
: CACHE_STRATEGIES.dynamic;
response.headers.set('Cache-Control', cacheControl);
// 静的アセットにはETagも追加
if (isStaticAsset) {
const etag = generateETag(pathname); // ETag生成関数(実装例は後述)
response.headers.set('ETag', etag);
}
return response;
};
function generateETag(pathname: string): string {
// ビルド時のハッシュ値を使う簡易実装
const hash = pathname.split('/').pop()?.split('.')[0] || '';
return `"${hash}"`;
}
この実装により、静的アセットは1年間のキャッシュ、動的ページは1時間のキャッシュとstale-while-revalidate戦略、APIエンドポイントはキャッシュ無効化という異なるポリシーを適用できます。
Cloudflare KVとの連携によるキャッシュ永続化
Cloudflare KVを活用すると、エッジ環境でレスポンスをキャッシュし、次回リクエスト時に高速に返却できます。以下は動的ページのレスポンスをKVにキャッシュする実装例です。
KVベースのレスポンスキャッシュ
// src/middleware.ts
import type { MiddlewareHandler } from 'astro';
const CACHE_TTL = 300; // 5分
export const onRequest: MiddlewareHandler = async (context, next) => {
const { request, locals } = context;
const url = new URL(request.url);
const cacheKey = `cache:${url.pathname}${url.search}`;
const kvNamespace = (context.locals.runtime as any)?.env?.KV;
if (kvNamespace && request.method === 'GET') {
// キャッシュから取得を試みる
const cachedResponse = await kvNamespace.get(cacheKey, { type: 'text' });
if (cachedResponse) {
return new Response(cachedResponse, {
status: 200,
headers: {
'Content-Type': 'text/html; charset=utf-8',
'X-Cache': 'HIT',
'Cache-Control': 'public, max-age=300'
}
});
}
}
// キャッシュミスの場合は通常レンダリング
const response = await next();
// レスポンスをキャッシュに保存(GETリクエストのみ)
if (kvNamespace && request.method === 'GET' && response.status === 200) {
const responseText = await response.text();
await kvNamespace.put(cacheKey, responseText, { expirationTtl: CACHE_TTL });
return new Response(responseText, {
status: response.status,
headers: {
...Object.fromEntries(response.headers.entries()),
'X-Cache': 'MISS'
}
});
}
return response;
};
この実装では、GETリクエストのレスポンスを5分間KVにキャッシュし、X-CacheヘッダーでHIT/MISSを確認できます。
キャッシュ戦略フロー図
flowchart LR
A["GETリクエスト"] --> B{"KVにキャッシュ存在?"}
B -->|Yes| C["キャッシュ返却<br/>(X-Cache: HIT)"]
B -->|No| D["ページレンダリング"]
D --> E["レスポンス生成"]
E --> F["KVに保存<br/>(TTL: 5分)"]
F --> G["レスポンス返却<br/>(X-Cache: MISS)"]
レート制限とキャッシュ制御の組み合わせ実装
実際のプロダクション環境では、レート制限とキャッシュ制御を組み合わせて使うことが一般的です。以下は両機能を統合した実装例です。
統合実装例
// src/middleware.ts
import type { MiddlewareHandler } from 'astro';
const RATE_LIMIT_WINDOW = 60;
const MAX_REQUESTS = 20;
const CACHE_TTL = 300;
export const onRequest: MiddlewareHandler = async (context, next) => {
const { request, locals } = context;
const url = new URL(request.url);
const clientIP = request.headers.get('CF-Connecting-IP') || 'unknown';
const kvNamespace = (context.locals.runtime as any)?.env?.KV;
// 1. レート制限チェック(APIルートのみ)
if (url.pathname.startsWith('/api/') && kvNamespace) {
const rateLimitKey = `rate_limit:${clientIP}:${url.pathname}`;
const currentCount = await kvNamespace.get(rateLimitKey);
const count = currentCount ? parseInt(currentCount) : 0;
if (count >= MAX_REQUESTS) {
return new Response(JSON.stringify({ error: 'Too Many Requests' }), {
status: 429,
headers: {
'Content-Type': 'application/json',
'Retry-After': RATE_LIMIT_WINDOW.toString()
}
});
}
await kvNamespace.put(
rateLimitKey,
(count + 1).toString(),
{ expirationTtl: RATE_LIMIT_WINDOW }
);
}
// 2. キャッシュチェック(GETリクエストのみ)
if (request.method === 'GET' && kvNamespace) {
const cacheKey = `cache:${url.pathname}${url.search}`;
const cachedResponse = await kvNamespace.get(cacheKey, { type: 'text' });
if (cachedResponse) {
return new Response(cachedResponse, {
status: 200,
headers: {
'Content-Type': 'text/html; charset=utf-8',
'X-Cache': 'HIT',
'Cache-Control': 'public, max-age=300'
}
});
}
}
// 3. 通常のレンダリング
const response = await next();
// 4. レスポンスをキャッシュ(200 OKの場合のみ)
if (request.method === 'GET' && response.status === 200 && kvNamespace) {
const responseText = await response.text();
const cacheKey = `cache:${url.pathname}${url.search}`;
await kvNamespace.put(cacheKey, responseText, { expirationTtl: CACHE_TTL });
return new Response(responseText, {
status: response.status,
headers: {
...Object.fromEntries(response.headers.entries()),
'X-Cache': 'MISS'
}
});
}
return response;
};
Cloudflare Workers環境でのデプロイ設定
Astro 5.1でCloudflare Workers環境にデプロイする場合、astro.config.mjsで@astrojs/cloudflareアダプターを設定し、KVネームスペースをバインディングする必要があります。
Astro設定ファイル
// astro.config.mjs
import { defineConfig } from 'astro/config';
import cloudflare from '@astrojs/cloudflare';
export default defineConfig({
output: 'server', // SSRモード
adapter: cloudflare({
mode: 'directory', // または 'advanced'
}),
});
wrangler.toml設定
# wrangler.toml
name = "astro-middleware-demo"
compatibility_date = "2026-06-01"
[[kv_namespaces]]
binding = "KV"
id = "your-kv-namespace-id"
preview_id = "your-preview-kv-namespace-id"
デプロイ後は、Cloudflareダッシュボードでリクエストログやキャッシュヒット率を確認できます。
まとめ
Astro 5.1のMiddleware機能を活用することで、エッジ環境での高度なレート制限とキャッシュ制御を実装できます。本記事で紹介した実装パターンのポイントは以下の通りです。
- Cloudflare KVを使ったレート制限: クライアントIPごとに時間ウィンドウ内のリクエスト数を制限し、TTL付きでKVに保存
- コンテンツタイプ別のキャッシュ戦略: 静的アセット・動的ページ・APIエンドポイントで異なるCache-Controlヘッダーを設定
- KVベースのレスポンスキャッシュ: GETリクエストのレスポンスをKVに保存し、次回リクエスト時に高速返却
- レート制限とキャッシュの統合: APIルートはレート制限、通常ページはキャッシュという複合戦略を1つのMiddlewareで実装
これらの実装により、パフォーマンスとセキュリティを両立した高品質なWebサイトを構築できます。