Back to Blog

Next.js API Routes Not Working With output: export — What to Use Instead

Published: July 29, 2026
Next.js API Routes Not Working With output: export — What to Use Instead

Next.js API routes not working with output: export isn't a bug waiting for a patch — it's a direct, permanent consequence of what static export actually produces. output: 'export' generates a folder of static HTML, CSS, and JavaScript files with no Node.js process behind them at all, and an API route needs exactly that: a server receiving and responding to requests. The frustrating part usually isn't the limitation itself. It's discovering it weeks into a project, after a frontend has grown around the assumption that a webhook handler or a form submission endpoint would just be available when needed.

Short answer: API routes and Route Handlers that read the incoming request fundamentally can't run without a server, and static export doesn't have one — the fix is deciding this upfront, splitting anything that needs server-side logic into a separately deployed backend (NestJS, Express, or similar), and having the statically exported frontend call it over a plain HTTP API with CORS configured correctly.

Black paper being cut by scissors on a red background, representing the frontend and backend splitting into two separately deployed pieces once static export rules out API routes

Why API Routes Are Fundamentally Incompatible With Static Export

Next.js's own static export documentation is direct: Route Handlers that rely on the incoming Request object aren't supported at all under output: 'export', alongside a broader list of server-dependent features. This isn't a missing feature Next.js might add — a static export's entire output is files a plain web server (Nginx, S3, a CDN) can serve without executing any application code. There's no process listening for a POST request, no runtime available to verify a webhook signature or write to a database. The limitation is structural, not a gap in the framework's feature set.

(If you've been searching for a workaround config flag that "enables" API routes under static export — there isn't one, and there won't be one, because the two things are mutually exclusive by definition. A static export is, by design, not a place a server-side handler can run.)

Recognizing This Before It Costs You Weeks

Mozilla's own explainer on CORS is worth reading once, since the browser-enforced restriction it describes is exactly what makes this split feel unfamiliar the first time — a statically exported frontend calling an external backend is a genuine cross-origin request, subject to the same rules as calling any third-party API. The single most valuable thing to get right here is timing: ask, during initial architecture planning, whether any part of the application will ever need to receive a webhook, process an authenticated form submission server-side, or do anything else requiring a server to read an incoming request. If the answer is yes for even one feature, output: 'export' isn't viable for the whole application — and finding that out before writing a line of frontend code is dramatically cheaper than discovering it after the frontend has grown around an assumption that turns out to be false.

The Architecture: A Separately Deployed Backend

The standard fix is treating the statically exported frontend exactly like a single-page application calling an external API it doesn't control — because that's precisely what it becomes:

TypeScript
1// backend/main.ts — a genuinely separate NestJS service, deployed on Railway/Render/a VM
2import { NestFactory } from '@nestjs/core';
3import { AppModule } from './app.module';
4
5async function bootstrap() {
6  const app = await NestFactory.create(AppModule);
7  app.enableCors({
8    origin: ['https://yourapp.com'],
9    credentials: true,
10  });
11  await app.listen(process.env.PORT ?? 3001);
12}
13bootstrap();

The backend handles everything that genuinely needs server-side logic — webhooks, authenticated mutations, anything touching a database directly — deployed on its own, running continuously, exactly the way BullMQ workers and dashboards in this series need to be deployed for the same underlying reason. The Next.js frontend stays statically exported, hosted anywhere that serves static files.

A directional signpost showing multiple city names and distances, representing the architecture decision that needs making early, before a frontend grows around an assumption that turns out to be wrong

Calling the External Backend Correctly: CORS and Environment Variables

Two details make or break this split in practice. First, CORS has to be configured explicitly on the backend for the frontend's actual production domain — a wildcard origin combined with credentials (cookies, auth headers) is rejected outright by browsers, so being specific isn't just good security practice, it's frequently required for the request to succeed at all:

TypeScript
1// backend/main.ts — explicit origins, not a wildcard, especially once credentials are involved
2app.enableCors({
3  origin: ['https://yourapp.com', 'https://staging.yourapp.com'],
4  methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
5  credentials: true,
6});

NestJS's own CORS documentation covers the full set of options enableCors() accepts beyond what's shown here, including per-request dynamic origin validation if a static allowlist isn't flexible enough. Second, the backend's base URL needs to reach the frontend's client-side code, and since a static export has no server to read runtime environment variables from, that URL has to be baked into the build itself:

TypeScript
1// app/api-client.ts — NEXT_PUBLIC_ vars are embedded at build time, not read at runtime
2const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL;
3
4export async function submitForm(data: FormData) {
5  return fetch(`${API_BASE_URL}/forms/submit`, {
6    method: 'POST',
7    credentials: 'include',
8    headers: { 'Content-Type': 'application/json' },
9    body: JSON.stringify(data),
10  });
11}

Changing the backend's URL later means rebuilding and redeploying the static frontend, not just updating an environment variable on a running server — the same build-time-versus-runtime distinction our environment variable management guide covers in more general terms, worth planning around rather than discovering during an incident.

A close-up of Ethernet cables plugged directly into server ports, representing the statically exported frontend calling a separately deployed backend across a genuine network boundary

The Opinion Part

Here's the position worth stating plainly: this split isn't a workaround forced on you by an unfortunate limitation — it's frequently the better architecture regardless of whether static export ever entered the picture. A statically exported frontend and an independently deployed, independently scaled backend can each be optimized for what they're actually good at, deployed on the hosting platform that fits them best, and iterated on without one team's frontend release blocking another's backend deploy. The genuine cost isn't the architecture itself — it's making this decision reactively, after weeks of building around an assumption, instead of proactively, as the first question asked before any code gets written.

Conclusion

Next.js API routes not working under output: 'export' isn't a bug to patch — it's static export doing exactly what it's designed to do, and the fix is architectural, not a configuration flag. Ask upfront whether anything in the app needs server-side request handling, split that logic into a genuinely separate backend service if it does, and get CORS and the build-time environment variable pattern right so the static frontend can actually reach it in production.

If dynamic APIs like cookies() or headers() are the specific thing breaking your static export build rather than a full API route, our dedicated guide on that broader error covers the full unsupported-features list and how to audit for it.

Decide the split early, wire up the CORS and environment variables correctly, and let the frontend and backend each be exactly what they were always going to need to be.

Frequently Asked Questions

Because API routes and Route Handlers that read the incoming Request object need a running Node.js server to receive and respond to requests, and output: export produces only static HTML, CSS, and JS files with no server process behind them at all. This isn't a missing feature that might get added — it's a direct consequence of what static export fundamentally is, a folder of files a plain web server can serve without running any application code.

Ask upfront whether any part of the application needs to receive a webhook, handle a form submission server-side, verify a payment signature, or do anything else that requires reading an incoming request on the server. If the answer is yes for even one feature, output: export isn't viable for the whole app, and that's worth knowing during architecture planning, not three sprints into building a frontend that assumes API routes will be available.

A separate backend service — NestJS, Express, or similar — deployed on its own (Railway, Render, a small VM), handling everything that needs server-side logic: webhooks, authenticated mutations, anything reading request data. The statically exported Next.js frontend calls that backend over a plain HTTP API, the same way any single-page application would call an external API it doesn't control.

On the backend, enable CORS explicitly for the frontend's actual production domain rather than a wildcard, especially if requests include credentials like cookies or auth headers. In NestJS, app.enableCors({ origin: ['https://yourapp.com'], credentials: true }) is the standard pattern — an overly permissive wildcard origin combined with credentials is rejected by browsers outright, so being explicit isn't just safer, it's often required for the request to work at all.

Environment variables, specifically ones prefixed with NEXT_PUBLIC_ so they're baked into the static build output at build time. Since a static export has no server to read runtime environment variables from, anything the client-side code needs — including the backend's base URL — has to be embedded during the build itself, which means changing the backend URL requires a rebuild, not just an environment variable update on a running server.

Portrait of Umar Farooq

About Umar Farooq

Umar Farooq is the founder and lead engineer of Codify SaaS. He builds B2B SaaS products and web applications on modern TypeScript stacks and enterprise Java, and writes code-first guides drawn from real production work — the schema decisions, the migrations that almost went wrong, and the performance fixes that actually moved the numbers. When he recommends an approach, he shows the code and explains the trade-offs.

Read full bio