[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"verticals":3,"quiz-nextjs-weekly-middleware-to-proxy-network-boundary":44,"search-suggestions":60,"quiz-article-nextjs-weekly-middleware-to-proxy-network-boundary":106},[4,20,32],{"id":5,"slug":6,"name":7,"tagline":8,"description":9,"accentFrom":10,"accentTo":11,"icon":12,"defaultLocale":13,"locales":14,"features":16,"position":19},"019fe637-3d33-714b-b57f-23e163ffca0c","dev","Web Development","Read it. Run it. Prove it.","A post a day on modern web development — most with an editable playground and a quiz that explains every answer. Free, no account needed.","violet-500","cyan-400","◇","en",[13,15],"fa",{"courses":17,"paths":17,"articles":17,"exams":18,"flashcards":18,"packages":17,"community":17,"certificates":17,"teams":17,"commerce":17},true,false,0,{"id":21,"slug":22,"name":23,"tagline":24,"description":25,"accentFrom":26,"accentTo":10,"icon":27,"defaultLocale":13,"locales":28,"features":30,"position":31},"019fe637-3dc2-754c-8657-0f175bfee7c6","lang","Languages","Learn a language the way you learn a codebase.","Grammar explained the way good documentation explains an API — one idea at a time, each with a quiz.","amber-400","⌘",[13,15,29],"es",{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":17,"packages":18,"community":17,"certificates":17,"teams":18,"commerce":18},2,{"id":33,"slug":34,"name":35,"tagline":36,"description":37,"accentFrom":38,"accentTo":39,"icon":40,"defaultLocale":13,"locales":41,"features":42,"position":43},"7b3c16f2-931d-410e-802e-e1fa4edab7de","soft","Soft Skills","The half of the job nobody wrote documentation for.","Weekly, on the parts of working life that decide more than your code does — first weeks, meetings, interviews, promotions, and the people around you. Written from what actually happens, and recorded as a podcast you can listen to on the walk.","emerald-400","teal-300","◉",[13],{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":18,"packages":18,"community":17,"certificates":18,"teams":18,"commerce":18},3,{"id":45,"slug":46,"kind":47,"title":48,"description":49,"config":50,"verticalId":5,"vertical":55,"course":52,"_count":56,"access":57,"attempts":59,"questionCount":51},"01a0b58a-801b-777f-bc56-6d6d6e031b6f","nextjs-weekly-middleware-to-proxy-network-boundary","PRACTICE_QUIZ","Next.js proxy.ts: The Network Boundary","Test yourself on why Next.js 16 renamed middleware.ts to proxy.ts, what runtime it locked in, and how to migrate safely.",{"questionCount":51,"timeLimitSec":52,"shuffleQuestions":18,"shuffleOptions":17,"negativeMarking":19,"passScorePct":53,"maxAttempts":52,"revealAnswers":54,"allowFlagging":18,"allowBacktracking":17},7,null,70,"AFTER_SUBMIT",{"slug":6,"name":7},{"questions":51},{"allowed":17,"reason":58},"FREE",[],[61,65,69,73,77,81,85,89,93,97,100,103],{"slug":62,"name":63,"articles":64},"webdev","Webdev",107,{"slug":66,"name":67,"articles":68},"javascript","Javascript",89,{"slug":70,"name":71,"articles":72},"frontend","Frontend",72,{"slug":74,"name":75,"articles":76},"css","Css",36,{"slug":78,"name":79,"articles":80},"tutorial","Tutorial",35,{"slug":82,"name":83,"articles":84},"typescript","Typescript",15,{"slug":86,"name":87,"articles":88},"react","React",13,{"slug":90,"name":91,"articles":92},"performance","Performance",12,{"slug":94,"name":95,"articles":96},"browser","Browser",9,{"slug":98,"name":99,"articles":96},"node","Node",{"slug":101,"name":102,"articles":51},"html","Html",{"slug":104,"name":105,"articles":51},"accessibility","Accessibility",{"id":107,"slug":46,"title":108,"subtitle":52,"excerpt":109,"coverUrl":110,"locale":13,"readingMinutes":92,"publishedAt":111,"viewCount":112,"likeCount":19,"commentCount":19,"author":113,"vertical":118,"topic":119,"tags":122,"_count":127,"playground":129,"body":131,"bodyMd":420,"seo":421,"translationGroupId":423,"series":424,"podcastUrl":52,"verticalId":5,"thread":442,"assessments":444,"translations":447,"quiz":449},"01a0b58a-7f56-736a-babf-1796c7b70c19","Next.js proxy.ts Explained (with Cheat Sheet)","Next.js 16 renamed middleware.ts to proxy.ts and locked it to the Node.js runtime. Learn the network-boundary model, matcher config, and the migration path.","\u002Fmedia\u002Fcovers\u002Fnextjs-weekly-middleware-to-proxy-network-boundary.png","2026-09-22T19:36:08.416Z",38,{"id":114,"name":115,"username":116,"avatarUrl":52,"headline":117},"019fe637-3c25-7088-9034-39c9f15dc3c8","Parsa Jiravand","parsa","Frontend engineer · building bestpractic",{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":120,"name":121},"nextjs","Nextjs",[123,124,125,126],{"slug":120,"name":121,"color":52},{"slug":66,"name":67,"color":52},{"slug":78,"name":79,"color":52},{"slug":62,"name":63,"color":52},{"assessments":128},1,{"slug":46,"title":130},"Next.js proxy.ts vs middleware.ts — interactive playground",{"blocks":132,"version":128},[133,137,142,145,154,157,160,163,174,177,180,183,189,192,195,200,204,207,210,213,216,219,222,225,230,233,236,239,242,246,249,253,256,259,267,270,277,280,284,287,290,293,297,300,303,306,309,312,315,355,359,362,365,368,371,374,377,384,387,390,393,396,399,402,405,408,411,414],{"id":134,"html":135,"type":136},"b1","\u003Cp>Your team wrote \u003Ccode>middleware.ts\u003C\u002Fcode> carefully — Edge-safe imports only, \u003Ccode>jose\u003C\u002Fcode> instead of \u003Ccode>jsonwebtoken\u003C\u002Fcode>, no direct database calls — because that&#39;s what Edge middleware demanded. Then you upgraded to Next.js 16, skimmed the release notes, and moved on. Nothing broke. Which is exactly the problem: \u003Ccode>middleware.ts\u003C\u002Fcode> still runs, but it&#39;s now the \u003Cem>deprecated\u003C\u002Fem> way to do the one job every non-trivial app needs — checking a request before a single line of your app runs. The framework renamed the file, moved the runtime under it, and left the old name working just long enough for teams to miss the change entirely.\u003C\u002Fp>","paragraph",{"id":138,"html":139,"text":140,"type":141,"level":31},"b2","What you&#39;ll learn","What you'll learn","heading",{"id":143,"html":144,"type":136},"b3","\u003Cp>By the end of this article you&#39;ll be able to:\u003C\u002Fp>",{"id":146,"type":147,"items":148,"ordered":18},"b4","list",[149,150,151,152,153],"Explain what \u003Cstrong>\u003Ccode>proxy.ts\u003C\u002Fcode>\u003C\u002Fstrong> is, why Next.js 16 renamed \u003Ccode>middleware.ts\u003C\u002Fcode> to it, and what actually changed under the hood.","State exactly which runtime \u003Ccode>proxy.ts\u003C\u002Fcode> runs on — and why you can no longer choose.","Migrate an existing \u003Ccode>middleware.ts\u003C\u002Fcode> file with the official codemod, including the config options that renamed alongside it.","Recognize the one capability trade Next.js made, and decide whether it affects your app.","Write a \u003Ccode>proxy.ts\u003C\u002Fcode> that checks auth, sets a header, and rewrites a request — the shape that covers most real uses.",{"id":155,"html":156,"text":156,"type":141,"level":31},"b5","Who this is for",{"id":158,"html":159,"type":136},"b6","\u003Cp>You&#39;ve shipped a Next.js App Router app and have (or have used) a \u003Ccode>middleware.ts\u003C\u002Fcode> file for things like auth checks or redirects. You don&#39;t need prior Edge-runtime experience — this article explains what that runtime was and why it mattered.\u003C\u002Fp>",{"id":161,"html":162,"text":162,"type":141,"level":31},"b7","Table of contents",{"id":164,"type":147,"items":165,"ordered":18},"b8",[166,167,168,169,170,171,172,173],"\u003Ca href=\"#the-problem-a-boundary-with-two-names-and-a-hidden-runtime\">The problem: a boundary with two names and a hidden runtime\u003C\u002Fa>","\u003Ca href=\"#the-mental-model-proxyts-is-the-network-boundary-not-a-request-handler\">The mental model: proxy.ts is the network boundary, not a request handler\u003C\u002Fa>","\u003Ca href=\"#migrating-middlewarets-to-proxyts-step-by-step\">Migrating middleware.ts to proxy.ts, step by step\u003C\u002Fa>","\u003Ca href=\"#edge-cases-and-gotchas\">Edge cases and gotchas\u003C\u002Fa>","\u003Ca href=\"#best-practices\">Best practices\u003C\u002Fa>","\u003Ca href=\"#faq\">FAQ\u003C\u002Fa>","\u003Ca href=\"#cheat-sheet\">Cheat sheet\u003C\u002Fa>","\u003Ca href=\"#key-takeaways\">Key takeaways\u003C\u002Fa>",{"id":175,"html":176,"text":176,"type":141,"level":31},"b9","The problem: a boundary with two names and a hidden runtime",{"id":178,"html":179,"type":136},"b10","\u003Cp>This article is written against \u003Cstrong>Next.js 16.3\u003C\u002Fstrong> (the current Active LTS release, verified against the framework&#39;s own file-convention and upgrade docs, and its GitHub releases, in September 2026). If you&#39;re reading this from a much later version, re-check the docs linked below before trusting a specific detail — that&#39;s the honest habit this series keeps asking of you, and this topic is exactly why.\u003C\u002Fp>",{"id":181,"html":182,"type":136},"b11","\u003Cp>Here&#39;s the wrong-way-first version, because it&#39;s what most teams actually did. A Next.js 15 app has this \u003Ccode>middleware.ts\u003C\u002Fcode>:\u003C\u002Fp>",{"id":184,"code":185,"type":186,"language":187,"highlight":188},"b12","\u002F\u002F middleware.ts — Next.js 15, Edge runtime (the only option)\nimport { NextResponse } from \"next\u002Fserver\";\nimport { jwtVerify } from \"jose\"; \u002F\u002F Edge-safe; jsonwebtoken would not run here\n\nexport async function middleware(request: Request) {\n  const token = request.headers.get(\"cookie\")?.match(\u002Fsession=([^;]+)\u002F)?.[1];\n  if (!token) return NextResponse.redirect(new URL(\"\u002Flogin\", request.url));\n\n  try {\n    await jwtVerify(token, secretKey); \u002F\u002F must be Edge-runtime-compatible\n    return NextResponse.next();\n  } catch {\n    return NextResponse.redirect(new URL(\"\u002Flogin\", request.url));\n  }\n}\n\nexport const config = { matcher: [\"\u002Fdashboard\u002F:path*\"] };","code","ts",[],{"id":190,"html":191,"type":136},"b13","\u003Cp>Every choice in that file — \u003Ccode>jose\u003C\u002Fcode> over \u003Ccode>jsonwebtoken\u003C\u002Fcode>, no direct Postgres client, no \u003Ccode>fs\u003C\u002Fcode> — exists because Edge middleware ran on a restricted, non-Node runtime. That constraint was real and it shaped how an entire generation of Next.js auth code got written.\u003C\u002Fp>",{"id":193,"html":194,"type":136},"b14","\u003Cp>Then Next.js 16 ships, and the docs start talking about \u003Ccode>proxy.ts\u003C\u002Fcode> instead. The team upgrades. \u003Ccode>middleware.ts\u003C\u002Fcode> still runs — Next.js kept it working on purpose — so nothing visibly breaks, and the rename gets filed under &quot;not our problem yet.&quot; Two things go quietly wrong from there:\u003C\u002Fp>",{"id":196,"type":147,"items":197,"ordered":17},"b15",[198,199],"New code in the same repo starts appearing as \u003Ccode>proxy.ts\u003C\u002Fcode> (copied from a blog post, a teammate&#39;s other project, or the docs), and now the app has both a \u003Ccode>middleware.ts\u003C\u002Fcode> and a mental model split between two names for the same job.","Someone &quot;helpfully&quot; migrates the file and copies the runtime opt-in along with it:",{"id":201,"code":202,"type":186,"language":187,"highlight":203},"b16","\u002F\u002F proxy.ts — this line is now meaningless\nexport const runtime = \"edge\"; \u002F\u002F ❌ ignored — proxy always runs on Node.js",[],{"id":205,"html":206,"type":136},"b17","\u003Cp>\u003Ccode>proxy.ts\u003C\u002Fcode> doesn&#39;t fail loudly here — it just runs on the Node.js runtime regardless, because that runtime \u003Cstrong>cannot be configured\u003C\u002Fstrong>. The Edge runtime isn&#39;t an option for \u003Ccode>proxy.ts\u003C\u002Fcode> at all. If your mental model is still &quot;Edge middleware, just renamed,&quot; you&#39;ll misjudge what you can and can&#39;t do inside it.\u003C\u002Fp>",{"id":208,"html":209,"text":209,"type":141,"level":31},"b18","The mental model: proxy.ts is the network boundary, not a request handler",{"id":211,"html":212,"type":136},"b19","\u003Cp>\u003Cstrong>The mental model:\u003C\u002Fstrong> \u003Ccode>proxy.ts\u003C\u002Fcode> is the one file that sits in front of your entire app, on every request that matches its \u003Ccode>matcher\u003C\u002Fcode>, and runs before the App Router resolves a route — before any layout, page, Server Component, or Server Action executes. Next.js 16 renamed it from \u003Ccode>middleware.ts\u003C\u002Fcode> specifically to stop you from thinking of it as a request handler in the Express sense (a function in a chain, alongside your route logic). It&#39;s a \u003Cstrong>network boundary\u003C\u002Fstrong>: the place where you decide whether a request is even allowed to reach the app, and what it&#39;s allowed to carry in with it (a header, a rewritten path, a redirect).\u003C\u002Fp>",{"id":214,"html":215,"type":136},"b20","\u003Cp>The rename came with a runtime decision, not just new vocabulary: \u003Ccode>proxy.ts\u003C\u002Fcode> runs exclusively on the \u003Cstrong>Node.js runtime\u003C\u002Fstrong>. There is no \u003Ccode>export const runtime = &quot;edge&quot;\u003C\u002Fcode> for it — the option doesn&#39;t exist, because a proxy that always runs the same way, in the same environment, is the entire point. \u003Ccode>middleware.ts\u003C\u002Fcode> is still there for teams that specifically need Edge behavior, but it&#39;s documented as deprecated, scheduled for removal in a future major version. You&#39;re not choosing between two files going forward; you&#39;re on a deprecation clock.\u003C\u002Fp>",{"id":217,"html":218,"type":136},"b21","\u003Cp>What that buys you: \u003Ccode>proxy.ts\u003C\u002Fcode> can use anything the Node.js runtime supports — Node&#39;s built-in \u003Ccode>crypto\u003C\u002Fcode>, a real database driver for a session lookup, any npm package that assumes Node — without auditing it for Edge compatibility first. What it costs you: if your app specifically wanted Edge&#39;s global, low-latency execution for this boundary, that option is gone for new code. For the overwhelming majority of auth\u002Fredirect\u002Frewrite logic, that trade is invisible; for a handful of latency-critical, globally-distributed checks, it&#39;s worth knowing about before you commit.\u003C\u002Fp>",{"id":220,"html":221,"text":221,"type":141,"level":31},"b22","Migrating middleware.ts to proxy.ts, step by step",{"id":223,"html":224,"type":136},"b23","\u003Cp>\u003Cstrong>Step 1 — run the codemod, don&#39;t hand-edit.\u003C\u002Fstrong> Next.js ships an automated migration:\u003C\u002Fp>",{"id":226,"code":227,"type":186,"language":228,"highlight":229},"b24","npx @next\u002Fcodemod@canary middleware-to-proxy","bash",[],{"id":231,"html":232,"type":136},"b25","\u003Cp>This renames \u003Ccode>middleware.ts\u003C\u002Fcode> → \u003Ccode>proxy.ts\u003C\u002Fcode>, renames the exported \u003Ccode>middleware\u003C\u002Fcode> function to \u003Ccode>proxy\u003C\u002Fcode>, and updates the config keys that renamed alongside it (for example \u003Ccode>skipMiddlewareUrlNormalize\u003C\u002Fcode> → \u003Ccode>skipProxyUrlNormalize\u003C\u002Fcode>, and \u003Ccode>experimental.middlewareClientMaxBodySize\u003C\u002Fcode> → \u003Ccode>experimental.proxyClientMaxBodySize\u003C\u002Fcode> in \u003Ccode>next.config.js\u003C\u002Fcode>). Run it, then read the diff — a codemod is a strong first draft, not a substitute for review.\u003C\u002Fp>",{"id":234,"html":235,"type":136},"b26","\u003Cp>\u003Cstrong>Key concept:\u003C\u002Fstrong> the codemod changes names, not behavior. Whatever your middleware did, your proxy does identically — the boundary&#39;s job hasn&#39;t moved, only its label and its guaranteed runtime.\u003C\u002Fp>",{"id":237,"html":238,"type":136},"b27","\u003Cp>\u003Cstrong>Step 2 — delete any runtime opt-in.\u003C\u002Fstrong> If your old file had \u003Ccode>export const config = { runtime: &quot;edge&quot; }\u003C\u002Fcode> or similar, remove it. It has no effect on \u003Ccode>proxy.ts\u003C\u002Fcode>, and leaving it in is the kind of thing that confuses the next engineer more than it confuses the framework.\u003C\u002Fp>",{"id":240,"html":241,"type":136},"b28","\u003Cp>\u003Cstrong>Step 3 — keep the matcher, unmodified.\u003C\u002Fstrong> The \u003Ccode>matcher\u003C\u002Fcode> config that scopes which paths trigger the boundary is unchanged:\u003C\u002Fp>",{"id":243,"code":244,"type":186,"language":187,"highlight":245},"b29","\u002F\u002F proxy.ts\nexport const config = {\n  matcher: [\"\u002Fdashboard\u002F:path*\", \"\u002Fapi\u002Fprotected\u002F:path*\"],\n};",[],{"id":247,"html":248,"type":136},"b30","\u003Cp>\u003Cstrong>Step 4 — now you can simplify, if it helps.\u003C\u002Fstrong> Because you&#39;re guaranteed Node.js, you can replace an Edge-safe workaround with the straightforward version, if one exists:\u003C\u002Fp>",{"id":250,"code":251,"type":186,"language":187,"highlight":252},"b31","\u002F\u002F proxy.ts — Next.js 16, Node.js runtime (the only option, and now a guarantee)\nimport { NextResponse } from \"next\u002Fserver\";\nimport { jwtVerify } from \"jose\"; \u002F\u002F still works fine — no need to rip it out\nimport type { NextRequest } from \"next\u002Fserver\";\n\nexport function proxy(request: NextRequest) {\n  const token = request.cookies.get(\"session\")?.value;\n  if (!token) {\n    const loginUrl = new URL(\"\u002Flogin\", request.url);\n    loginUrl.searchParams.set(\"from\", request.nextUrl.pathname);\n    return NextResponse.redirect(loginUrl);\n  }\n  return NextResponse.next();\n}\n\nexport const config = { matcher: [\"\u002Fdashboard\u002F:path*\"] };",[],{"id":254,"html":255,"type":136},"b32","\u003Cp>\u003Cstrong>Key concept:\u003C\u002Fstrong> nothing here \u003Cem>had\u003C\u002Fem> to change — \u003Ccode>jose\u003C\u002Fcode> runs fine on Node.js too. The point isn&#39;t &quot;rewrite everything,&quot; it&#39;s that you&#39;re no longer required to reach for an Edge-safe library when a plain Node one would do, and you won&#39;t hit a surprise if a dependency assumes \u003Ccode>Buffer\u003C\u002Fcode> or \u003Ccode>crypto.createHmac\u003C\u002Fcode> exists.\u003C\u002Fp>",{"id":257,"html":258,"text":258,"type":141,"level":31},"b33","Edge cases and gotchas",{"id":260,"type":147,"items":261,"ordered":18},"b34",[262,263,264,265,266],"\u003Cstrong>\u003Ccode>middleware.ts\u003C\u002Fcode> still works — for now.\u003C\u002Fstrong> Next.js 16 didn&#39;t remove it; it&#39;s deprecated and slated for removal in a future major version. If you need the Edge runtime specifically (for its global execution model), keep using \u003Ccode>middleware.ts\u003C\u002Fcode> with its \u003Ccode>runtime\u003C\u002Fcode> opt-in and track the deprecation notice for when that stops being an option.","\u003Cstrong>The runtime is not configurable, in either direction.\u003C\u002Fstrong> You can&#39;t opt \u003Ccode>proxy.ts\u003C\u002Fcode> into Edge, and there&#39;s no flag to force \u003Ccode>middleware.ts\u003C\u002Fcode> onto Node.js. The two files map to two fixed runtimes; migrating means accepting the new one.","\u003Cstrong>Config keys renamed, not just the file.\u003C\u002Fstrong> If your \u003Ccode>next.config.js\u003C\u002Fcode> sets \u003Ccode>skipMiddlewareUrlNormalize\u003C\u002Fcode> or \u003Ccode>experimental.middlewareClientMaxBodySize\u003C\u002Fcode>, those need the \u003Ccode>proxy\u003C\u002Fcode>-prefixed equivalents after migration — the codemod handles this, a manual rename easily misses it.","\u003Cstrong>This doesn&#39;t remove the Edge runtime from Next.js.\u003C\u002Fstrong> Route Handlers and pages can still opt into the Edge runtime where it&#39;s supported. The one place Edge specifically disappeared is the network-boundary file — don&#39;t over-generalize the change to the rest of the framework.","\u003Cstrong>The rewrite\u002Fredirect logic itself hasn&#39;t changed.\u003C\u002Fstrong> \u003Ccode>NextResponse.next()\u003C\u002Fcode>, \u003Ccode>.redirect()\u003C\u002Fcode>, \u003Ccode>.rewrite()\u003C\u002Fcode>, and reading\u002Fwriting cookies and headers all work the same way in \u003Ccode>proxy.ts\u003C\u002Fcode> as they did in \u003Ccode>middleware.ts\u003C\u002Fcode>. The migration is about the file&#39;s name, its exported function&#39;s name, and its runtime — not its API.",{"id":268,"html":269,"text":269,"type":141,"level":31},"b35","Best practices",{"id":271,"type":147,"items":272,"ordered":18},"b36",[273,274,275,276],"\u003Cstrong>Reach for \u003Ccode>proxy.ts\u003C\u002Fcode> for boundary decisions\u003C\u002Fstrong>, not business logic: auth gating, locale\u002Fregion redirects, A\u002FB routing, header injection, and blocking bad requests before they cost you a route render. If a check needs your app&#39;s Server Components or database models to decide, it usually belongs past the boundary, not inside it.","\u003Cstrong>Run the codemod on every \u003Ccode>middleware.ts\u003C\u002Fcode> you own\u003C\u002Fstrong>, even ones that &quot;still work fine.&quot; The deprecation clock is real, and doing it now — while you can compare the diff against a file you understand — is cheaper than doing it later under a removal deadline.","\u003Cstrong>Keep the \u003Ccode>matcher\u003C\u002Fcode> as narrow as the job needs.\u003C\u002Fstrong> A boundary that runs on every request, including static assets it doesn&#39;t care about, is pure overhead; scope it to the paths that actually need the check.","\u003Cstrong>Don&#39;t move Edge-specific code into \u003Ccode>proxy.ts\u003C\u002Fcode> unexamined.\u003C\u002Fstrong> If a library was chosen specifically for Edge compatibility, it&#39;s fine to leave it — but don&#39;t assume you now need a \u003Cem>different\u003C\u002Fem> library, either. Change what the runtime actually requires you to change, nothing more.",{"id":278,"html":279,"text":279,"type":141,"level":31},"b37","FAQ",{"id":281,"html":282,"text":283,"type":141,"level":43},"b38","Is \u003Ccode>proxy.ts\u003C\u002Fcode> a completely new file, or a rename?","Is proxy.ts a completely new file, or a rename?",{"id":285,"html":286,"type":136},"b39","\u003Cp>It&#39;s a rename with a runtime attached. Same conceptual job as \u003Ccode>middleware.ts\u003C\u002Fcode> — code that runs before the App Router resolves a route — but the exported function is now called \u003Ccode>proxy\u003C\u002Fcode>, the file is \u003Ccode>proxy.ts\u003C\u002Fcode>, and it always runs on the Node.js runtime.\u003C\u002Fp>",{"id":288,"html":289,"text":289,"type":141,"level":43},"b40","Do I have to migrate right now?",{"id":291,"html":292,"type":136},"b41","\u003Cp>No — \u003Ccode>middleware.ts\u003C\u002Fcode> still works in Next.js 16. But it&#39;s documented as deprecated and due for removal in a future major version, so treat this as scheduled work, not optional cleanup.\u003C\u002Fp>",{"id":294,"html":295,"text":296,"type":141,"level":43},"b42","Can I run \u003Ccode>proxy.ts\u003C\u002Fcode> on the Edge runtime if I really want to?","Can I run proxy.ts on the Edge runtime if I really want to?",{"id":298,"html":299,"type":136},"b43","\u003Cp>No. The runtime for \u003Ccode>proxy.ts\u003C\u002Fcode> is fixed to Node.js and isn&#39;t configurable. If your use case specifically needs Edge, that&#39;s what \u003Ccode>middleware.ts\u003C\u002Fcode> remains for, while it&#39;s still available.\u003C\u002Fp>",{"id":301,"html":302,"text":302,"type":141,"level":43},"b44","Will the migration change what my auth\u002Fredirect logic does?",{"id":304,"html":305,"type":136},"b45","\u003Cp>It shouldn&#39;t. The codemod renames the file, the function, and the handful of config keys that renamed with it. The request\u002Fresponse API — \u003Ccode>NextResponse.next()\u003C\u002Fcode>, \u003Ccode>.redirect()\u003C\u002Fcode>, \u003Ccode>.rewrite()\u003C\u002Fcode>, cookies, headers — is unchanged.\u003C\u002Fp>",{"id":307,"html":308,"text":308,"type":141,"level":43},"b46","Does this affect Route Handlers or pages that use the Edge runtime?",{"id":310,"html":311,"type":136},"b47","\u003Cp>No. The Edge-runtime removal is specific to the network-boundary file. Route Handlers and pages can still opt into Edge where Next.js supports it there.\u003C\u002Fp>",{"id":313,"html":314,"text":314,"type":141,"level":31},"b48","Cheat sheet",{"id":316,"head":317,"rows":321,"type":354},"b49",[318,319,320],"Task","Next.js 16 way","Notes",[322,326,330,334,338,342,346,350],[323,324,325],"File name","\u003Ccode>proxy.ts\u003C\u002Fcode> (was \u003Ccode>middleware.ts\u003C\u002Fcode>)","Old name still works, deprecated",[327,328,329],"Exported function","\u003Ccode>export function proxy(...)\u003C\u002Fcode>","Was \u003Ccode>export function middleware(...)\u003C\u002Fcode>",[331,332,333],"Runtime","Node.js only, not configurable","No Edge option for \u003Ccode>proxy.ts\u003C\u002Fcode>",[335,336,337],"Scope which paths run it","\u003Ccode>export const config = { matcher: [...] }\u003C\u002Fcode>","Unchanged from \u003Ccode>middleware.ts\u003C\u002Fcode>",[339,340,341],"Migrate automatically","\u003Ccode>npx @next\u002Fcodemod@canary middleware-to-proxy\u003C\u002Fcode>","Renames file, function, and config keys",[343,344,345],"Renamed config keys","\u003Ccode>skipMiddlewareUrlNormalize\u003C\u002Fcode> → \u003Ccode>skipProxyUrlNormalize\u003C\u002Fcode>; \u003Ccode>experimental.middlewareClientMaxBodySize\u003C\u002Fcode> → \u003Ccode>experimental.proxyClientMaxBodySize\u003C\u002Fcode>","Codemod handles these",[347,348,349],"Need Edge runtime specifically","Keep \u003Ccode>middleware.ts\u003C\u002Fcode> for now","Tracked for future removal",[351,352,353],"Redirect \u002F rewrite \u002F headers API","\u003Ccode>NextResponse.next()\u003C\u002Fcode> \u002F \u003Ccode>.redirect()\u003C\u002Fcode> \u002F \u003Ccode>.rewrite()\u003C\u002Fcode>","Identical to \u003Ccode>middleware.ts\u003C\u002Fcode>","table",{"id":356,"code":357,"type":186,"language":187,"highlight":358},"b50","\u002F\u002F The canonical proxy.ts shape: check, then let through or redirect\nimport { NextResponse } from \"next\u002Fserver\";\nimport type { NextRequest } from \"next\u002Fserver\";\n\nexport function proxy(request: NextRequest) {\n  const isAllowed = \u002F* your boundary check — auth, locale, A\u002FB, etc. *\u002F true;\n  if (!isAllowed) return NextResponse.redirect(new URL(\"\u002Flogin\", request.url));\n  return NextResponse.next();\n}\n\nexport const config = { matcher: [\"\u002Fdashboard\u002F:path*\"] };",[],{"id":360,"html":361,"type":136},"b51","\u003C!-- playground:start -->",{"id":363,"html":364,"text":364,"type":141,"level":31},"b52","🎮 Try it yourself",{"id":366,"html":367,"type":136},"b53","\u003Cp>\u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnextjs-weekly-middleware-to-proxy-network-boundary\u002Fplayground\">▶️ Open the interactive playground →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":369,"html":370,"type":136},"b54","\u003Cp>\u003Cem>Runs right in your browser — poke at it and watch the concept react live.\u003C\u002Fem>\u003C\u002Fp>",{"id":372,"html":373,"type":136},"b55","\u003C!-- playground:end -->",{"id":375,"html":376,"text":376,"type":141,"level":31},"b56","Key takeaways",{"id":378,"type":147,"items":379,"ordered":18},"b57",[380,381,382,383],"\u003Ccode>proxy.ts\u003C\u002Fcode> is Next.js 16&#39;s rename of \u003Ccode>middleware.ts\u003C\u002Fcode> — same job (the network boundary in front of your app), new name, and a fixed Node.js runtime that can&#39;t be configured.","\u003Ccode>middleware.ts\u003C\u002Fcode> still runs today, but it&#39;s deprecated; the official codemod (\u003Ccode>npx @next\u002Fcodemod@canary middleware-to-proxy\u003C\u002Fcode>) migrates the file, the function name, and the config keys together.","The Edge runtime isn&#39;t gone from Next.js — it&#39;s gone specifically from this one boundary file, so don&#39;t over-apply the change to Route Handlers or pages.","Because the boundary now runs on Node.js unconditionally, you can use ordinary Node-only libraries there without an Edge-compatibility audit — but you don&#39;t have to change code that already worked.",{"id":385,"html":386,"type":136},"b58","\u003Cp>This series has already covered two things \u003Ccode>proxy.ts\u003C\u002Fcode> sits in front of: the request eventually reaches \u003Ca href=\"https:\u002F\u002Fdev.to\u002Fparsajiravand\u002Fnextjs-server-actions-mutations-security-cheat-sheet-b63\">Server Actions and the mutation flow they run\u003C\u002Fa>, and whatever renders downstream is shaped by \u003Ca href=\"https:\u002F\u002Fdev.to\u002Fparsajiravand\u002Fnextjs-cache-components-explained-with-cheat-sheet-55ob\">Cache Components and what streams versus what&#39;s cached\u003C\u002Fa>. Neither is required reading here, but both make more sense once you know what already ran before them.\u003C\u002Fp>",{"id":388,"html":389,"type":136},"b59","\u003C!-- quiz:start -->",{"id":391,"html":392,"text":392,"type":141,"level":31},"b60","🧠 Test yourself",{"id":394,"html":395,"type":136},"b61","\u003Cp>Think it clicked? \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnextjs-weekly-middleware-to-proxy-network-boundary\u002Fquiz\">Take the 7-question quiz →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":397,"html":398,"type":136},"b62","\u003Cp>\u003Cem>Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.\u003C\u002Fem>\u003C\u002Fp>",{"id":400,"html":401,"type":136},"b63","\u003C!-- quiz:end -->",{"id":403,"html":404,"type":136},"b64","\u003Cp>Your \u003Ccode>middleware.ts\u003C\u002Fcode> still works today — but it&#39;s running on borrowed time and an assumption about the Edge runtime that no longer holds for new code. Migrate it this week, while the diff is small and the reasoning is fresh, rather than in a rush when the removal notice finally lands. What&#39;s the messiest thing your boundary file currently does — and would you trust it to run on Node.js without a second look?\u003C\u002Fp>",{"id":406,"type":407},"b65","divider",{"id":409,"html":410,"type":136},"b66","\u003Cp>🚀 \u003Cstrong>Want more like this?\u003C\u002Fstrong> Every guide, playground, and quiz lives on \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002F\">bestpractic.org\u003C\u002Fa>\u003C\u002Fstrong> — open it and \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002F\">sign up free\u003C\u002Fa>\u003C\u002Fstrong> so the next one finds you.\u003C\u002Fp>",{"id":412,"html":413,"type":136},"b67","\u003Cp>\u003Cem>Thanks for reading! Let&#39;s stay connected:\u003C\u002Fem>\u003C\u002Fp>",{"id":415,"type":147,"items":416,"ordered":18},"b68",[417,418,419],"⭐ \u003Cstrong>GitHub\u003C\u002Fstrong> — follow me and star the projects: \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fparsajiravand\">github.com\u002Fparsajiravand\u003C\u002Fa>","💬 \u003Cstrong>Discord\u003C\u002Fstrong> — join the frontend best-practices community: \u003Ca href=\"https:\u002F\u002Fdiscord.gg\u002Fd9KRhuAwQ\">discord.gg\u002Fd9KRhuAwQ\u003C\u002Fa>","📸 \u003Cstrong>Instagram\u003C\u002Fstrong> — frontend best practices, daily: \u003Ca href=\"https:\u002F\u002Fwww.instagram.com\u002Fbestpractice___\u002F\">@bestpractice___\u003C\u002Fa>","Your team wrote `middleware.ts` carefully — Edge-safe imports only, `jose` instead of `jsonwebtoken`, no direct database calls — because that's what Edge middleware demanded. Then you upgraded to Next.js 16, skimmed the release notes, and moved on. Nothing broke. Which is exactly the problem: `middleware.ts` still runs, but it's now the *deprecated* way to do the one job every non-trivial app needs — checking a request before a single line of your app runs. The framework renamed the file, moved the runtime under it, and left the old name working just long enough for teams to miss the change entirely.\n\n## What you'll learn\n\nBy the end of this article you'll be able to:\n\n- Explain what **`proxy.ts`** is, why Next.js 16 renamed `middleware.ts` to it, and what actually changed under the hood.\n- State exactly which runtime `proxy.ts` runs on — and why you can no longer choose.\n- Migrate an existing `middleware.ts` file with the official codemod, including the config options that renamed alongside it.\n- Recognize the one capability trade Next.js made, and decide whether it affects your app.\n- Write a `proxy.ts` that checks auth, sets a header, and rewrites a request — the shape that covers most real uses.\n\n## Who this is for\n\nYou've shipped a Next.js App Router app and have (or have used) a `middleware.ts` file for things like auth checks or redirects. You don't need prior Edge-runtime experience — this article explains what that runtime was and why it mattered.\n\n## Table of contents\n\n- [The problem: a boundary with two names and a hidden runtime](#the-problem-a-boundary-with-two-names-and-a-hidden-runtime)\n- [The mental model: proxy.ts is the network boundary, not a request handler](#the-mental-model-proxyts-is-the-network-boundary-not-a-request-handler)\n- [Migrating middleware.ts to proxy.ts, step by step](#migrating-middlewarets-to-proxyts-step-by-step)\n- [Edge cases and gotchas](#edge-cases-and-gotchas)\n- [Best practices](#best-practices)\n- [FAQ](#faq)\n- [Cheat sheet](#cheat-sheet)\n- [Key takeaways](#key-takeaways)\n\n## The problem: a boundary with two names and a hidden runtime\n\nThis article is written against **Next.js 16.3** (the current Active LTS release, verified against the framework's own file-convention and upgrade docs, and its GitHub releases, in September 2026). If you're reading this from a much later version, re-check the docs linked below before trusting a specific detail — that's the honest habit this series keeps asking of you, and this topic is exactly why.\n\nHere's the wrong-way-first version, because it's what most teams actually did. A Next.js 15 app has this `middleware.ts`:\n\n```ts\n\u002F\u002F middleware.ts — Next.js 15, Edge runtime (the only option)\nimport { NextResponse } from \"next\u002Fserver\";\nimport { jwtVerify } from \"jose\"; \u002F\u002F Edge-safe; jsonwebtoken would not run here\n\nexport async function middleware(request: Request) {\n  const token = request.headers.get(\"cookie\")?.match(\u002Fsession=([^;]+)\u002F)?.[1];\n  if (!token) return NextResponse.redirect(new URL(\"\u002Flogin\", request.url));\n\n  try {\n    await jwtVerify(token, secretKey); \u002F\u002F must be Edge-runtime-compatible\n    return NextResponse.next();\n  } catch {\n    return NextResponse.redirect(new URL(\"\u002Flogin\", request.url));\n  }\n}\n\nexport const config = { matcher: [\"\u002Fdashboard\u002F:path*\"] };\n```\n\nEvery choice in that file — `jose` over `jsonwebtoken`, no direct Postgres client, no `fs` — exists because Edge middleware ran on a restricted, non-Node runtime. That constraint was real and it shaped how an entire generation of Next.js auth code got written.\n\nThen Next.js 16 ships, and the docs start talking about `proxy.ts` instead. The team upgrades. `middleware.ts` still runs — Next.js kept it working on purpose — so nothing visibly breaks, and the rename gets filed under \"not our problem yet.\" Two things go quietly wrong from there:\n\n1. New code in the same repo starts appearing as `proxy.ts` (copied from a blog post, a teammate's other project, or the docs), and now the app has both a `middleware.ts` and a mental model split between two names for the same job.\n2. Someone \"helpfully\" migrates the file and copies the runtime opt-in along with it:\n\n```ts\n\u002F\u002F proxy.ts — this line is now meaningless\nexport const runtime = \"edge\"; \u002F\u002F ❌ ignored — proxy always runs on Node.js\n```\n\n`proxy.ts` doesn't fail loudly here — it just runs on the Node.js runtime regardless, because that runtime **cannot be configured**. The Edge runtime isn't an option for `proxy.ts` at all. If your mental model is still \"Edge middleware, just renamed,\" you'll misjudge what you can and can't do inside it.\n\n## The mental model: proxy.ts is the network boundary, not a request handler\n\n**The mental model:** `proxy.ts` is the one file that sits in front of your entire app, on every request that matches its `matcher`, and runs before the App Router resolves a route — before any layout, page, Server Component, or Server Action executes. Next.js 16 renamed it from `middleware.ts` specifically to stop you from thinking of it as a request handler in the Express sense (a function in a chain, alongside your route logic). It's a **network boundary**: the place where you decide whether a request is even allowed to reach the app, and what it's allowed to carry in with it (a header, a rewritten path, a redirect).\n\nThe rename came with a runtime decision, not just new vocabulary: `proxy.ts` runs exclusively on the **Node.js runtime**. There is no `export const runtime = \"edge\"` for it — the option doesn't exist, because a proxy that always runs the same way, in the same environment, is the entire point. `middleware.ts` is still there for teams that specifically need Edge behavior, but it's documented as deprecated, scheduled for removal in a future major version. You're not choosing between two files going forward; you're on a deprecation clock.\n\nWhat that buys you: `proxy.ts` can use anything the Node.js runtime supports — Node's built-in `crypto`, a real database driver for a session lookup, any npm package that assumes Node — without auditing it for Edge compatibility first. What it costs you: if your app specifically wanted Edge's global, low-latency execution for this boundary, that option is gone for new code. For the overwhelming majority of auth\u002Fredirect\u002Frewrite logic, that trade is invisible; for a handful of latency-critical, globally-distributed checks, it's worth knowing about before you commit.\n\n## Migrating middleware.ts to proxy.ts, step by step\n\n**Step 1 — run the codemod, don't hand-edit.** Next.js ships an automated migration:\n\n```bash\nnpx @next\u002Fcodemod@canary middleware-to-proxy\n```\n\nThis renames `middleware.ts` → `proxy.ts`, renames the exported `middleware` function to `proxy`, and updates the config keys that renamed alongside it (for example `skipMiddlewareUrlNormalize` → `skipProxyUrlNormalize`, and `experimental.middlewareClientMaxBodySize` → `experimental.proxyClientMaxBodySize` in `next.config.js`). Run it, then read the diff — a codemod is a strong first draft, not a substitute for review.\n\n**Key concept:** the codemod changes names, not behavior. Whatever your middleware did, your proxy does identically — the boundary's job hasn't moved, only its label and its guaranteed runtime.\n\n**Step 2 — delete any runtime opt-in.** If your old file had `export const config = { runtime: \"edge\" }` or similar, remove it. It has no effect on `proxy.ts`, and leaving it in is the kind of thing that confuses the next engineer more than it confuses the framework.\n\n**Step 3 — keep the matcher, unmodified.** The `matcher` config that scopes which paths trigger the boundary is unchanged:\n\n```ts\n\u002F\u002F proxy.ts\nexport const config = {\n  matcher: [\"\u002Fdashboard\u002F:path*\", \"\u002Fapi\u002Fprotected\u002F:path*\"],\n};\n```\n\n**Step 4 — now you can simplify, if it helps.** Because you're guaranteed Node.js, you can replace an Edge-safe workaround with the straightforward version, if one exists:\n\n```ts\n\u002F\u002F proxy.ts — Next.js 16, Node.js runtime (the only option, and now a guarantee)\nimport { NextResponse } from \"next\u002Fserver\";\nimport { jwtVerify } from \"jose\"; \u002F\u002F still works fine — no need to rip it out\nimport type { NextRequest } from \"next\u002Fserver\";\n\nexport function proxy(request: NextRequest) {\n  const token = request.cookies.get(\"session\")?.value;\n  if (!token) {\n    const loginUrl = new URL(\"\u002Flogin\", request.url);\n    loginUrl.searchParams.set(\"from\", request.nextUrl.pathname);\n    return NextResponse.redirect(loginUrl);\n  }\n  return NextResponse.next();\n}\n\nexport const config = { matcher: [\"\u002Fdashboard\u002F:path*\"] };\n```\n\n**Key concept:** nothing here *had* to change — `jose` runs fine on Node.js too. The point isn't \"rewrite everything,\" it's that you're no longer required to reach for an Edge-safe library when a plain Node one would do, and you won't hit a surprise if a dependency assumes `Buffer` or `crypto.createHmac` exists.\n\n## Edge cases and gotchas\n\n- **`middleware.ts` still works — for now.** Next.js 16 didn't remove it; it's deprecated and slated for removal in a future major version. If you need the Edge runtime specifically (for its global execution model), keep using `middleware.ts` with its `runtime` opt-in and track the deprecation notice for when that stops being an option.\n- **The runtime is not configurable, in either direction.** You can't opt `proxy.ts` into Edge, and there's no flag to force `middleware.ts` onto Node.js. The two files map to two fixed runtimes; migrating means accepting the new one.\n- **Config keys renamed, not just the file.** If your `next.config.js` sets `skipMiddlewareUrlNormalize` or `experimental.middlewareClientMaxBodySize`, those need the `proxy`-prefixed equivalents after migration — the codemod handles this, a manual rename easily misses it.\n- **This doesn't remove the Edge runtime from Next.js.** Route Handlers and pages can still opt into the Edge runtime where it's supported. The one place Edge specifically disappeared is the network-boundary file — don't over-generalize the change to the rest of the framework.\n- **The rewrite\u002Fredirect logic itself hasn't changed.** `NextResponse.next()`, `.redirect()`, `.rewrite()`, and reading\u002Fwriting cookies and headers all work the same way in `proxy.ts` as they did in `middleware.ts`. The migration is about the file's name, its exported function's name, and its runtime — not its API.\n\n## Best practices\n\n- **Reach for `proxy.ts` for boundary decisions**, not business logic: auth gating, locale\u002Fregion redirects, A\u002FB routing, header injection, and blocking bad requests before they cost you a route render. If a check needs your app's Server Components or database models to decide, it usually belongs past the boundary, not inside it.\n- **Run the codemod on every `middleware.ts` you own**, even ones that \"still work fine.\" The deprecation clock is real, and doing it now — while you can compare the diff against a file you understand — is cheaper than doing it later under a removal deadline.\n- **Keep the `matcher` as narrow as the job needs.** A boundary that runs on every request, including static assets it doesn't care about, is pure overhead; scope it to the paths that actually need the check.\n- **Don't move Edge-specific code into `proxy.ts` unexamined.** If a library was chosen specifically for Edge compatibility, it's fine to leave it — but don't assume you now need a *different* library, either. Change what the runtime actually requires you to change, nothing more.\n\n## FAQ\n\n### Is `proxy.ts` a completely new file, or a rename?\n\nIt's a rename with a runtime attached. Same conceptual job as `middleware.ts` — code that runs before the App Router resolves a route — but the exported function is now called `proxy`, the file is `proxy.ts`, and it always runs on the Node.js runtime.\n\n### Do I have to migrate right now?\n\nNo — `middleware.ts` still works in Next.js 16. But it's documented as deprecated and due for removal in a future major version, so treat this as scheduled work, not optional cleanup.\n\n### Can I run `proxy.ts` on the Edge runtime if I really want to?\n\nNo. The runtime for `proxy.ts` is fixed to Node.js and isn't configurable. If your use case specifically needs Edge, that's what `middleware.ts` remains for, while it's still available.\n\n### Will the migration change what my auth\u002Fredirect logic does?\n\nIt shouldn't. The codemod renames the file, the function, and the handful of config keys that renamed with it. The request\u002Fresponse API — `NextResponse.next()`, `.redirect()`, `.rewrite()`, cookies, headers — is unchanged.\n\n### Does this affect Route Handlers or pages that use the Edge runtime?\n\nNo. The Edge-runtime removal is specific to the network-boundary file. Route Handlers and pages can still opt into Edge where Next.js supports it there.\n\n## Cheat sheet\n\n| Task | Next.js 16 way | Notes |\n| --- | --- | --- |\n| File name | `proxy.ts` (was `middleware.ts`) | Old name still works, deprecated |\n| Exported function | `export function proxy(...)` | Was `export function middleware(...)` |\n| Runtime | Node.js only, not configurable | No Edge option for `proxy.ts` |\n| Scope which paths run it | `export const config = { matcher: [...] }` | Unchanged from `middleware.ts` |\n| Migrate automatically | `npx @next\u002Fcodemod@canary middleware-to-proxy` | Renames file, function, and config keys |\n| Renamed config keys | `skipMiddlewareUrlNormalize` → `skipProxyUrlNormalize`; `experimental.middlewareClientMaxBodySize` → `experimental.proxyClientMaxBodySize` | Codemod handles these |\n| Need Edge runtime specifically | Keep `middleware.ts` for now | Tracked for future removal |\n| Redirect \u002F rewrite \u002F headers API | `NextResponse.next()` \u002F `.redirect()` \u002F `.rewrite()` | Identical to `middleware.ts` |\n\n```ts\n\u002F\u002F The canonical proxy.ts shape: check, then let through or redirect\nimport { NextResponse } from \"next\u002Fserver\";\nimport type { NextRequest } from \"next\u002Fserver\";\n\nexport function proxy(request: NextRequest) {\n  const isAllowed = \u002F* your boundary check — auth, locale, A\u002FB, etc. *\u002F true;\n  if (!isAllowed) return NextResponse.redirect(new URL(\"\u002Flogin\", request.url));\n  return NextResponse.next();\n}\n\nexport const config = { matcher: [\"\u002Fdashboard\u002F:path*\"] };\n```\n\n\u003C!-- playground:start -->\n\n## 🎮 Try it yourself\n\n**[▶️ Open the interactive playground →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnextjs-weekly-middleware-to-proxy-network-boundary\u002Fplayground)**\n\n_Runs right in your browser — poke at it and watch the concept react live._\n\n\u003C!-- playground:end -->\n\n## Key takeaways\n\n- `proxy.ts` is Next.js 16's rename of `middleware.ts` — same job (the network boundary in front of your app), new name, and a fixed Node.js runtime that can't be configured.\n- `middleware.ts` still runs today, but it's deprecated; the official codemod (`npx @next\u002Fcodemod@canary middleware-to-proxy`) migrates the file, the function name, and the config keys together.\n- The Edge runtime isn't gone from Next.js — it's gone specifically from this one boundary file, so don't over-apply the change to Route Handlers or pages.\n- Because the boundary now runs on Node.js unconditionally, you can use ordinary Node-only libraries there without an Edge-compatibility audit — but you don't have to change code that already worked.\n\nThis series has already covered two things `proxy.ts` sits in front of: the request eventually reaches [Server Actions and the mutation flow they run](https:\u002F\u002Fdev.to\u002Fparsajiravand\u002Fnextjs-server-actions-mutations-security-cheat-sheet-b63), and whatever renders downstream is shaped by [Cache Components and what streams versus what's cached](https:\u002F\u002Fdev.to\u002Fparsajiravand\u002Fnextjs-cache-components-explained-with-cheat-sheet-55ob). Neither is required reading here, but both make more sense once you know what already ran before them.\n\n\u003C!-- quiz:start -->\n\n## 🧠 Test yourself\n\nThink it clicked? **[Take the 7-question quiz →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnextjs-weekly-middleware-to-proxy-network-boundary\u002Fquiz)**\n\n_Instant feedback, a hint on every question, and an explanation for each answer — right or wrong._\n\n\u003C!-- quiz:end -->\n\nYour `middleware.ts` still works today — but it's running on borrowed time and an assumption about the Edge runtime that no longer holds for new code. Migrate it this week, while the diff is small and the reasoning is fresh, rather than in a rush when the removal notice finally lands. What's the messiest thing your boundary file currently does — and would you trust it to run on Node.js without a second look?\n\n---\n\n🚀 **Want more like this?** Every guide, playground, and quiz lives on **[bestpractic.org](https:\u002F\u002Fbestpractic.org\u002F)** — open it and **[sign up free](https:\u002F\u002Fbestpractic.org\u002F)** so the next one finds you.\n\n*Thanks for reading! Let's stay connected:*\n\n- ⭐ **GitHub** — follow me and star the projects: [github.com\u002Fparsajiravand](https:\u002F\u002Fgithub.com\u002Fparsajiravand)\n- 💬 **Discord** — join the frontend best-practices community: [discord.gg\u002Fd9KRhuAwQ](https:\u002F\u002Fdiscord.gg\u002Fd9KRhuAwQ)\n- 📸 **Instagram** — frontend best practices, daily: [@bestpractice___](https:\u002F\u002Fwww.instagram.com\u002Fbestpractice___\u002F)",{"title":108,"canonical":422,"description":109},"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnextjs-weekly-middleware-to-proxy-network-boundary","01a0b58a-7f56-736a-babf-191c06435602",{"name":425,"part":426,"total":426,"items":427},"Next.js Deep Dive",4,[428,433,437,441],{"slug":429,"title":430,"publishedAt":431,"readingMinutes":432},"nextjs-weekly-cache-components-explained","Next.js Cache Components Explained (with Cheat Sheet)","2026-09-01T11:24:15.087Z",14,{"slug":434,"title":435,"publishedAt":436,"readingMinutes":84},"nextjs-weekly-server-actions-mutations-security","Next.js Server Actions: Mutations & Security (Cheat Sheet)","2026-09-08T10:57:00.975Z",{"slug":438,"title":439,"publishedAt":440,"readingMinutes":432},"nextjs-weekly-parallel-intercepting-routes-modals","Next.js Parallel & Intercepting Routes: Modals Done Right","2026-09-15T17:50:47.218Z",{"slug":46,"title":108,"publishedAt":111,"readingMinutes":92},{"id":443,"locked":18},"01a0b58a-7ff8-755d-9ee8-12e9c618f6c0",[445],{"id":45,"slug":46,"title":48,"_count":446},{"questions":51},[448],{"locale":13,"slug":46},{"id":45,"slug":46,"title":48,"_count":450,"questionCount":51},{"questions":51}]