Skip to Content
Edge Runtime

Edge Runtime

next-modular provides a next-modular/edge entry point with stateless handlers that have no Node.js dependencies. Use this when you want your module routes to run on Edge Runtime.

How it works

The default handlers (handleRoute, handleApiRoute, handleMiddleware) rely on a global module registry. Edge Runtime doesn’t guarantee persistent global state across requests, so next-modular provides stateless variants that accept modules directly instead:

Standard (Node.js)Edge-compatible
handleRoutehandleRouteWith
handleApiRoutehandleApiRouteWith
handleMiddlewarehandleMiddlewareWith
handleMetadatahandleMetadataWith

To resolve route metadata on the edge, add a generateMetadata to your catch-all page that calls handleMetadataWith(modules, pathname):

// app/[...module]/page.tsx import { handleMetadataWith } from 'next-modular/edge'; import { modules } from '../../modules.config'; import type { Metadata } from 'next'; export async function generateMetadata({ params }): Promise<Metadata> { const { module } = await params; return (await handleMetadataWith(modules, '/' + module.join('/'))) ?? {}; }

Setting up an edge-compatible app

Create your catch-all routes with export const runtime = 'edge' and import from next-modular/edge:

// app/api/[...module]/route.ts export const runtime = 'edge'; import { handleApiRouteWith } from 'next-modular/edge'; import { modules } from '../../modules.config'; async function handle(req: Request, context: { params: Promise<{ module: string[] }> }) { const params = await context.params; const pathname = '/api/' + params.module.join('/'); return handleApiRouteWith(modules, req, pathname, { params }); } export const GET = handle; export const POST = handle; export const PUT = handle; export const DELETE = handle;
// app/[...module]/page.tsx export const runtime = 'edge'; import { handleRouteWith } from 'next-modular/edge'; import { notFound } from 'next/navigation'; import { modules } from '../../modules.config'; export default async function ModulePage({ params }) { const { module } = await params; const pathname = '/' + module.join('/'); const result = await handleRouteWith(modules, pathname); if (!result) notFound(); const { component: Component, params: routeParams } = result; return <Component params={routeParams} />; }

Note: proxy.ts (middleware) always runs on edge runtime by default in Next.js — no runtime declaration needed there.

// proxy.ts import { NextRequest, NextResponse } from 'next/server'; import { handleMiddlewareWith } from 'next-modular/edge'; import { modules } from './modules.config'; export async function proxy(req: NextRequest) { const result = await handleMiddlewareWith(modules, req); if (result) return result; return NextResponse.next(); }

Writing edge-compatible module handlers

Module handlers are edge-compatible as long as they only use Web APIs:

// ✅ Edge-compatible — uses only Request/Response export async function pingHandler(): Promise<Response> { return Response.json({ pong: true }); } // ✅ Edge-compatible — uses only Headers Web API export async function headersHandler(req: Request): Promise<Response> { const headers: Record<string, string> = {}; req.headers.forEach((value, key) => { headers[key] = value; }); return Response.json(headers); } // ❌ Not edge-compatible — uses Node.js fs export async function contentHandler(req: Request): Promise<Response> { const file = await fs.readFile('./content/hello.mdx', 'utf-8'); // will fail on edge return new Response(file); }

Constraint: all-or-nothing

export const runtime = 'edge' applies to an entire Next.js route file. This means all modules handled by that route must be edge-compatible — no Node.js APIs (fs, path, crypto, etc.). If any module uses Node.js, keep the default Node.js runtime instead.

Last updated on