お好きな金額でサポートしていただけると嬉しいです!
(50円〜10,000円[税込]まで)
作成日時:2025年8月22日 23:41
更新日時:2025年8月23日 12:15
Next.js
Next.js 15.5でmiddlewareにNode.jsランタイム(runtime: 'nodejs'
)が安定版となりました。
これにより従来Edge Runtimeでは扱いづらかったNode.js固有API(fs, crypto, 既存の認証/DB/計測SDKなど)がミドルウェア層で直接利用可能となります。
そもそもの前提部分も含めてまとめてみました。
Edge RuntimeはWeb標準APIのみをサポートする軽量なJavaScript実行環境です。V8エンジンベースですが、Node.js APIは意図的に除外されています。
Edge Runtimeには、Node.jsネイティブモジュールの非対応という課題がありました。
これはNext.jsというフレームワークの大きなウィークポイントだったところです。
// ❌ Edge Runtimeで利用不可
const fs = require('fs'); // ファイルシステム
const crypto = require('crypto'); // Node.js暗号化
const buffer = require('buffer'); // Buffer操作
const stream = require('stream'); // ストリーム処理
const path = require('path'); // パス操作
const os = require('os'); // OS情報
const child_process = require('child_process'); // プロセス実行
このようなデメリットが、Node.js Middlewareの安定化によって解決する、ということです。
課題 | 従来 | 15.5以降 |
---|---|---|
JWT/署名/HMAC検証 | API Route内で実施 | URL入口(middleware)で遮断 |
既存SDK(fs/crypto依存) | Edgeでは再実装/諦め | そのまま利用 |
監査/計測 | 後段でのみ | 早期記録+無効トラフィック抑制 |
一方で、Edge Runtimeの「超低レイテンシ」という強みは依然として有効です。
どこまで作りこむのかという方針にもよると思いますが、「限定パスのみNode.js Middleware / それ以外はEdge」のハイブリッド構成で併用するということもできます。
ただし、middleware.ts
は1枚しか置けないので、以下のような棲み分けで併用することになります。
export const runtime = 'edge'
を利用Request
├─ /api/auth/* → Node.js middleware 通過 → (認証OK) → API
├─ /dashboard/* → Node.js middleware 通過 → ページ/handler
├─ /api/geo → (matcher外) → Edge runtime handler (runtime='edge')
├─ /blog/* → (matcher外) → Edge ページ
└─ /api/report → (matcher外) → Node.js Route (runtime='nodejs')
観点 | Edge Runtime | Node.js Runtime |
---|---|---|
レイテンシ | 最小 | 増える可能性あり |
利用可能API | Web/標準限定 | Node.jsコア & 大半のnpm |
適性 | 軽量リダイレクト, A/B, Geo | 認証, 暗号, 既存SDK, ロギング |
制約 | ネイティブモジュール不可 | 速度/メモリ若干コスト |
将来性 | 継続(高速層) | 15.5安定・17でデフォルト検討中 |
import { NextRequest, NextResponse } from 'next/server';
export const config = {
runtime: 'nodejs', // 安定版として利用可能に
matcher: ['/api/auth/:path*', '/dashboard/:path*']
};
export function middleware(request: NextRequest) {
// 完全なNode.js APIとnpmパッケージへのアクセス
const fs = require('fs');
const crypto = require('crypto');
// 認証ロジック
const token = request.headers.get('authorization');
if (!isValidToken(token)) {
return NextResponse.redirect(new URL('/login', request.url));
}
return NextResponse.next();
}
function isValidToken(token: string | null): boolean {
if (!token) return false;
const crypto = require('crypto');
return crypto.timingSafeEqual(
Buffer.from(token),
Buffer.from(process.env.EXPECTED_TOKEN || '')
);
}
import { NextRequest, NextResponse } from 'next/server';
export const config = {
runtime: 'nodejs',
matcher: [
'/api/auth/:path*',
'/dashboard/:path*',
'/((?!_next/static|_next/image|favicon.ico).*)', // 静的ファイル除外
]
};
export async function middleware(request: NextRequest) {
// /api/auth/* → JWT検証
if (request.nextUrl.pathname.startsWith('/api/auth')) {
const jwt = require('jsonwebtoken');
const token = request.headers.get('authorization')?.replace('Bearer ', '');
try {
const decoded = jwt.verify(token || '', process.env.JWT_SECRET || '');
const response = NextResponse.next();
response.headers.set('x-user-id', decoded.sub || '');
return response;
} catch (error) {
return NextResponse.redirect(new URL('/login', request.url));
}
}
// /dashboard/* → HMAC署名検証
if (request.nextUrl.pathname.startsWith('/dashboard')) {
const crypto = require('crypto');
const sessionId = request.cookies.get('session-id')?.value;
const providedSignature = request.cookies.get('session-signature')?.value;
if (!sessionId || !providedSignature) {
return NextResponse.redirect(new URL('/login', request.url));
}
const hmac = crypto.createHmac('sha256', process.env.SESSION_SECRET);
hmac.update(sessionId);
const expectedSignature = hmac.digest('hex');
if (!crypto.timingSafeEqual(
Buffer.from(expectedSignature),
Buffer.from(providedSignature)
)) {
return NextResponse.redirect(new URL('/login', request.url));
}
}
return NextResponse.next();
}
// app/api/geo/route.ts
export const runtime = 'edge';
export async function GET() {
return new Response(
JSON.stringify({
region: 'JP',
variant: Math.random() < 0.5 ? 'A' : 'B',
runtime: 'edge',
timestamp: Date.now()
}),
{
status: 200,
headers: { 'Content-Type': 'application/json' }
}
);
}
// app/blog/[id]/page.tsx
export const runtime = 'edge';
async function fetchPost(id: string) {
return {
id,
title: `記事 ${id}`,
content: 'Edge Runtime配信コンテンツ',
createdAt: new Date().toISOString()
};
}
export default async function BlogPage({
params
}: {
params: { id: string }
}) {
const post = await fetchPost(params.id);
return (
<main>
<h1>{post.title}</h1>
<time>{post.createdAt}</time>
<article>{post.content}</article>
</main>
);
}
// app/api/report/route.ts
export const runtime = 'nodejs';
import crypto from 'crypto';
import fs from 'fs';
import path from 'path';
export async function POST(request: Request) {
const body = await request.json().catch(() => ({}));
// Node.js固有API使用
const digest = crypto.createHash('sha256')
.update(JSON.stringify(body))
.digest('hex');
// ログファイル出力
const logPath = path.join(process.cwd(), 'logs', 'reports.log');
fs.appendFileSync(logPath, `${Date.now()}: ${digest}\n`);
return new Response(
JSON.stringify({ digest, runtime: 'nodejs' }),
{
status: 200,
headers: { 'Content-Type': 'application/json' }
}
);
}
export function middleware(request: NextRequest) {
const fs = require('fs');
const path = require('path');
// 非同期でログ記録(パフォーマンス重視)
setImmediate(() => {
const logEntry = {
timestamp: new Date().toISOString(),
method: request.method,
url: request.url,
userAgent: request.headers.get('user-agent'),
ip: request.ip
};
const logPath = path.join(process.cwd(), 'logs', 'access.log');
fs.appendFileSync(logPath, JSON.stringify(logEntry) + '\n');
});
return NextResponse.next();
}
// TTLキャッシュで過剰なDBアクセス防止
const userCache = new Map();
export function middleware(request: NextRequest) {
const userId = request.headers.get('x-user-id');
if (!userId) return NextResponse.next();
// TTLキャッシュで過剰なDBアクセスを防止
if (!userCache.has(userId) ||
Date.now() - userCache.get(userId).timestamp > 300000) { // 5分TTL
// 軽量なユーザー属性のみ取得
const userInfo = getUserInfo(userId);
userCache.set(userId, {
data: userInfo,
timestamp: Date.now()
});
}
const response = NextResponse.next();
response.headers.set('x-user-role', userCache.get(userId).data.role);
return response;
}
function getUserInfo(userId: string) {
// 簡単なDBクエリ(実装例)
return { role: 'user', permissions: ['read'] };
}
export const config = {
runtime: 'nodejs',
matcher: [
'/api/auth/:path*', // 認証が必要なAPIのみ
'/dashboard/:path*', // ダッシュボード領域のみ
]
};
export function middleware(request: NextRequest) {
// 条件分岐で必要な時のみrequire
if (request.url.includes('/api/crypto')) {
const crypto = require('crypto'); // 遅延ロード
// 暗号処理
}
return NextResponse.next();
}
export function middleware(request: NextRequest) {
// 軽い判定を先に
if (!request.headers.get('authorization')) {
return NextResponse.redirect(new URL('/login', request.url));
}
// 重い処理は必要な場合のみ
const heavyValidation = performHeavyValidation(request);
if (!heavyValidation) {
return NextResponse.redirect(new URL('/unauthorized', request.url));
}
return NextResponse.next();
}
function performHeavyValidation(request: NextRequest): boolean {
// 重い検証ロジック
return true;
}
# 現在のパッケージでEdge非対応APIを確認
npm ls --depth=0 | grep -E "(fs|crypto|buffer|stream)"
// 既存のmiddleware.tsを確認
export const config = {
matcher: ['/api/:path*'] // 全APIを対象
};
// Node.js移行版(段階的導入)
export const config = {
runtime: 'nodejs',
matcher: ['/api/auth/:path*'] // 認証APIのみNode.jsに移行
};
// middleware.ts - Node.js Runtime(限定パス)
export const config = {
runtime: 'nodejs',
matcher: ['/api/auth/:path*', '/dashboard/:path*']
};
// app/api/geo/route.ts - Edge Runtime
export const runtime = 'edge';
export async function GET() {
// 軽量処理
}
// app/blog/[id]/page.tsx - Edge Page
export const runtime = 'edge';
export default function BlogPage() {
// 閲覧ページ
}
export function middleware(request: NextRequest) {
const startTime = performance.now();
// 既存の処理
const result = existingLogic(request);
const endTime = performance.now();
console.log(`Processing time: ${endTime - startTime}ms`);
return result;
}
function existingLogic(request: NextRequest) {
// 実装ロジック
return NextResponse.next();
}
export function middleware(request: NextRequest) {
// 早期return
if (!request.headers.get('authorization')) {
return NextResponse.redirect(new URL('/login', request.url));
}
// 遅延require
if (request.url.includes('/api/crypto')) {
const crypto = require('crypto');
// 必要時のみロード
}
return NextResponse.next();
}
Next.js 17でのデフォルト化を見据え、今のうちに設計指針/ガイドラインを社内共有し、標準化によるチーム開発効率向上と円滑な移行を準備しておくことが大切になるかと思います。