Next.js Edge vs Node.js: Middleware, Cookies, and Set-Cookie Best Practices
In Next.js 13/14 (App Router), the runtime environment defines what APIs you can use and how certain behaviors differ. While building unified login flows and tracking systems, I’ve run into several recurring issues around cookies and Set-Cookie
. This post summarizes key points:
- Middleware runs on Edge Runtime, while pages/layouts/route handlers run on Node.js Runtime.
- How to access cookies in both environments, and how Node.js can read upstream and server
Set-Cookie
. - Why you must use
response.headers.append
(notset
) when writing multipleSet-Cookie
values. - What to watch out for when parsing
Set-Cookie
.
1) Execution Environments
middleware.ts
/middleware.js
→ Runs on Edge Runtime.
You getNextRequest
andNextResponse
, use Web Standard APIs (Headers, Request, Response), but no Node.js built-ins.app/**
pages, layouts, route handlers (e.g.app/api/**/route.ts
) → Run in Node.js Runtime by default.
You can usenext/headers
(cookies()
,headers()
), Node’sfetch
behavior (can readset-cookie
), and Node features.You can override runtime for specific routes with:
1
export const runtime = 'edge';
2) Reading Cookies in Edge vs Node.js
In Middleware (Edge)
1 | // middleware.ts |
In Node.js Runtime (pages/server actions/route handlers)
1 | import { cookies, headers } from 'next/headers'; |
Reading upstream Set-Cookie
in Node.js
Unlike browsers, Node.js fetch
can access set-cookie
from upstream responses:
1 | const upstream = await fetch('https://auth.example.com/login', { |
Reading server Set-Cookie
via next/headers
In Node.js runtime, headers()
also exposes response-bound headers.
That means you can read cookies that your server is about to set with headers().get('set-cookie')
.
1 | // app/api/demo/route.ts |
Output:
1 | { |
👉 Key point:
headers().get('cookie')
→ gives you cookies from the client request.headers().get('set-cookie')
→ gives you cookies that the server response is about to send (only in Node.js runtime).
3) Writing Set-Cookie
: Use append
, not set
HTTP allows multiple Set-Cookie
headers. Using headers.set('set-cookie', ...)
overwrites previous cookies. Always use append
.
1 | // ❌ Overwrites |
💡 Tip: Prefer
response.cookies.set()
where available, as it handles multiple cookies automatically.
1 | import { NextResponse } from 'next/server'; |
4) Parsing Set-Cookie
: Common Pitfalls
The tricky part is that:
- A response may include multiple
Set-Cookie
headers. - Inside a cookie, the
Expires
attribute itself contains a comma (e.g.,Fri, 01 Jan 2038 00:00:00 GMT
). - So you cannot just split on commas.
Recommendations
- Don’t
split(',')
blindly. You’ll break cookies withExpires
. - Use a library like
set-cookie-parser
:splitCookiesString()
splits multiple cookies safely.parseString()
parses each into structured objects.
- Attribute rules:
Max-Age
overridesExpires
.SameSite=None
requiresSecure
.- Be mindful of defaults for
Path
andDomain
. - Encode values properly if they contain special characters.
- Forwarding cookies:
- When proxying upstream logins, forward all
Set-Cookie
headers withappend
. - If you need to rewrite cookies, parse and reconstruct carefully.
- When proxying upstream logins, forward all
Example: Robust parsing & forwarding (Node.js)
1 | import setCookieParser from 'set-cookie-parser'; |
Takeaways
- middleware = Edge, pages/layouts/handlers = Node.js by default.
- Reading cookies works in both; reading
Set-Cookie
works in Node.js via bothfetch
andheaders().get('set-cookie')
. - Writing cookies: use
cookies.set()
orheaders.append
. - Parsing
Set-Cookie
: never split on commas, use a parser, and handle attributes properly.
Following these patterns avoids the classic “Why did my cookie disappear?” or “Why does login only work on one device?” headaches.