[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"verticals":3,"article-nuxt-weekly-nitro-server-routes":44,"search-suggestions":431,"related-nuxt-weekly-nitro-server-routes":473,"code:ts:true:126ogs4":535,"code:ts:true:1glohjr":536,"code:ts:true:15tr16f":537,"comments-01a0ec72-faae-767b-a924-5ebb5891d4cd":538},[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,"title":47,"subtitle":48,"excerpt":49,"coverUrl":50,"locale":13,"readingMinutes":51,"publishedAt":52,"viewCount":53,"likeCount":19,"commentCount":19,"author":54,"vertical":59,"topic":60,"tags":63,"_count":74,"playground":76,"body":78,"bodyMd":397,"seo":398,"translationGroupId":400,"series":401,"podcastUrl":48,"verticalId":5,"thread":419,"assessments":421,"translations":427,"quiz":429},"01a0ec72-faae-767b-a924-5ebb5891d4cd","nuxt-weekly-nitro-server-routes","Nuxt Server Routes Explained: How Nitro Builds Your API",null,"Nuxt server routes turn server\u002Fapi into a real backend via Nitro and h3. Learn routing rules, defineEventHandler, middleware order, and the useState trap.","\u002Fmedia\u002Fcovers\u002Fnuxt-weekly-nitro-server-routes.png",13,"2026-09-29T12:10:53.560Z",66,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},"019fe637-3c25-7088-9034-39c9f15dc3c8","Parsa Jiravand","parsa","Frontend engineer · building bestpractic",{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},"nuxt","Nuxt",[64,65,68,71],{"slug":61,"name":62,"color":48},{"slug":66,"name":67,"color":48},"backend","Backend",{"slug":69,"name":70,"color":48},"javascript","Javascript",{"slug":72,"name":73,"color":48},"tutorial","Tutorial",{"assessments":75},1,{"slug":46,"title":77},"Nuxt server routes — router &amp; middleware-order playground",{"blocks":79,"version":75},[80,84,87,92,95,104,107,110,113,124,128,131,137,140,143,146,149,152,155,158,161,164,167,170,178,181,184,188,191,195,198,202,205,209,212,215,218,221,224,227,230,238,241,245,248,252,255,258,261,265,268,272,275,278,318,321,324,327,330,333,336,343,346,349,352,355,358,361,364,367,370,373,379,382,385,388,391],{"id":81,"html":82,"type":83},"b1","\u003Cp>Open any Nuxt project and there&#39;s a good chance a \u003Ccode>server\u002Fapi\u003C\u002Fcode> folder is already sitting in it — a \u003Ccode>hello.ts\u003C\u002Fcode> here, a \u003Ccode>login.post.ts\u003C\u002Fcode> there. Ask most people what it is and the answer is usually &quot;the API routes.&quot; Ask what actually runs those files, in what order, and whether they can use the same \u003Ccode>useState\u003C\u002Fcode> or \u003Ccode>useRoute\u003C\u002Fcode> composables as the rest of the app, and the answers get much shakier. That gap is where the interesting bugs live: a middleware that silently runs before the one it&#39;s supposed to follow, an event handler that dies with \u003Ccode>useState is not defined\u003C\u002Fcode> for no obvious reason, a \u003Ccode>readBody()\u003C\u002Fcode> that comes back empty.\u003C\u002Fp>","paragraph",{"id":85,"html":86,"type":83},"b2","\u003Cp>This article is written against \u003Cstrong>Nuxt 4.x\u003C\u002Fstrong> (verified against the v4.5 release line, August 2026 — Nuxt 3 reached end-of-life on July 31, 2026). The directory names below assume the \u003Ccode>server\u002F\u003C\u002Fcode> layout, which — unlike \u003Ccode>pages\u002F\u003C\u002Fcode>, \u003Ccode>components\u002F\u003C\u002Fcode>, and the rest of your app code — stays at the project root in both Nuxt 3 and Nuxt 4&#39;s new \u003Ccode>app\u002F\u003C\u002Fcode>-nested structure. If you&#39;ve read \u003Ca href=\"https:\u002F\u002Fdev.to\u002Fparsajiravand\u002Fnuxt-hydration-mismatch-why-it-happens-and-how-to-fix-it-5b7i\">the hydration mismatch episode\u003C\u002Fa> or \u003Ca href=\"https:\u002F\u002Fdev.to\u002Fparsajiravand\u002Fnuxt-usestate-vs-ref-why-server-state-leaks-across-users-47n1\">the \u003Ccode>useState\u003C\u002Fcode> vs \u003Ccode>ref\u003C\u002Fcode> episode\u003C\u002Fa>, this one picks up the other half of &quot;what runs where&quot;: not the Vue app rendering twice, but the completely separate server world sitting next to it.\u003C\u002Fp>",{"id":88,"html":89,"text":90,"type":91,"level":31},"b3","What you&#39;ll learn","What you'll learn","heading",{"id":93,"html":94,"type":83},"b4","\u003Cp>By the end of this article you&#39;ll be able to:\u003C\u002Fp>",{"id":96,"type":97,"items":98,"ordered":18},"b5","list",[99,100,101,102,103],"Explain what Nitro and \u003Ccode>server\u002Fapi\u003C\u002Fcode>\u002F\u003Ccode>server\u002Froutes\u003C\u002Fcode>\u002F\u003Ccode>server\u002Fmiddleware\u003C\u002Fcode> actually are, and how a filename becomes a route","Read and write request data correctly with \u003Ccode>getQuery\u003C\u002Fcode>, \u003Ccode>getRouterParam\u003C\u002Fcode>, and \u003Ccode>readBody\u003C\u002Fcode>","Predict the real execution order of your server middleware — not the order you assume","Explain why Vue composables like \u003Ccode>useState\u003C\u002Fcode> don&#39;t work inside a server route, and what to use instead","Know when calling your own API route with \u003Ccode>useFetch\u003C\u002Fcode> during SSR involves the network at all",{"id":105,"html":106,"text":106,"type":91,"level":31},"b6","Who this is for",{"id":108,"html":109,"type":83},"b7","\u003Cp>You&#39;ve built Nuxt pages and components, and you&#39;ve probably already dropped a file into \u003Ccode>server\u002Fapi\u003C\u002Fcode> and had it work. You don&#39;t need prior backend framework experience — this article treats Nitro as its own subject, not &quot;Express with different syntax.&quot;\u003C\u002Fp>",{"id":111,"html":112,"text":112,"type":91,"level":31},"b8","Table of contents",{"id":114,"type":97,"items":115,"ordered":18},"b9",[116,117,118,119,120,121,122,123],"\u003Ca href=\"#the-problem-it-looks-like-the-rest-of-your-app-but-it-isnt\">The problem: it looks like the rest of your app, but it isn&#39;t\u003C\u002Fa>","\u003Ca href=\"#the-mental-model-two-runtimes-one-project\">The mental model: two runtimes, one project\u003C\u002Fa>","\u003Ca href=\"#building-server-routes-stage-by-stage\">Building server routes, stage by stage\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":125,"html":126,"text":127,"type":91,"level":31},"b10","The problem: it looks like the rest of your app, but it isn&#39;t","The problem: it looks like the rest of your app, but it isn't",{"id":129,"html":130,"type":83},"b11","\u003Cp>Say you want a small \u003Ccode>\u002Fapi\u002Fprofile\u003C\u002Fcode> endpoint that returns the current user, and — since you already have a \u003Ccode>useState(&#39;user&#39;)\u003C\u002Fcode> that holds the logged-in user everywhere else in the app — reusing it here feels natural:\u003C\u002Fp>",{"id":132,"code":133,"type":134,"language":135,"highlight":136},"b12","\u002F\u002F server\u002Fapi\u002Fprofile.get.ts — looks reasonable, isn't\nexport default defineEventHandler((event) => {\n  const user = useState('user') \u002F\u002F ❌ throws at runtime\n  return { user: user.value }\n})","code","ts",[],{"id":138,"html":139,"type":83},"b13","\u003Cp>Run it, and instead of JSON you get a 500: \u003Ccode>useState is not defined\u003C\u002Fcode>. It&#39;s the exact composable you use in every \u003Ccode>.vue\u003C\u002Fcode> file, so it reads like a missing import — but it isn&#39;t one. \u003Ccode>server\u002F\u003C\u002Fcode> gets its own auto-imports (h3 helpers, Nitro utilities, \u003Ccode>server\u002Futils\u003C\u002Fcode>), and Vue composables aren&#39;t among them; import it from \u003Ccode>#app\u003C\u002Fcode> explicitly and the build refuses with &quot;Vue app aliases are not allowed in server runtime.&quot; The problem is \u003Cem>where\u003C\u002Fem> it&#39;s being called from. \u003Ccode>useState\u003C\u002Fcode>, \u003Ccode>useRoute\u003C\u002Fcode>, \u003Ccode>useFetch\u003C\u002Fcode> — the whole family of Nuxt composables — depend on there being a current Nuxt application instance to attach to. A \u003Ccode>server\u002Fapi\u003C\u002Fcode> file has no such thing. It&#39;s not part of the Vue app at all; it&#39;s a plain request handler that Nitro invokes directly, with nothing Vue-shaped anywhere near it.\u003C\u002Fp>",{"id":141,"html":142,"type":83},"b14","\u003Cp>This is the trap the &quot;Nuxt is just Vue with routing&quot; mental model sets. \u003Ccode>server\u002Fapi\u003C\u002Fcode> looks like it belongs to the same app as your pages because it lives in the same repo, ships in the same deploy, and even shares the same \u003Ccode>nuxt dev\u003C\u002Fcode> process — but it&#39;s a different runtime with a different lifecycle, and the rules that make composables work don&#39;t apply there.\u003C\u002Fp>",{"id":144,"html":145,"text":145,"type":91,"level":31},"b15","The mental model: two runtimes, one project",{"id":147,"html":148,"type":83},"b16","\u003Cp>A Nuxt project is really two separate request-handling worlds glued together at build time:\u003C\u002Fp>",{"id":150,"html":151,"type":83},"b17","\u003Cp>\u003Cstrong>The Vue\u002Fapp world.\u003C\u002Fstrong> Pages, components, layouts, and composables. Every request for a page spins up a Nuxt application instance (server-side, then again client-side for hydration), and that instance is what \u003Ccode>useState\u003C\u002Fcode>, \u003Ccode>useRoute\u003C\u002Fcode>, and friends attach themselves to. This is the world the last two episodes of this series lived in.\u003C\u002Fp>",{"id":153,"html":154,"type":83},"b18","\u003Cp>\u003Cstrong>The Nitro\u002Fh3 world.\u003C\u002Fstrong> \u003Ccode>server\u002Fapi\u003C\u002Fcode>, \u003Ccode>server\u002Froutes\u003C\u002Fcode>, and \u003Ccode>server\u002Fmiddleware\u003C\u002Fcode>. Nitro is the server engine Nuxt is built on — it&#39;s what starts the process, decides which file handles which URL, and runs each matched file as a plain function that receives an \u003Ccode>H3Event\u003C\u002Fcode> (from \u003Ccode>h3\u003C\u002Fcode>, the tiny HTTP toolkit Nitro is built around) and returns a value. There is no component tree here, no &quot;current instance,&quot; nothing for a Vue composable to hook into. It doesn&#39;t know or care that a Vue app exists elsewhere in the same process.\u003C\u002Fp>",{"id":156,"html":157,"type":83},"b19","\u003Cp>The two worlds do talk to each other, but only across an explicit boundary: a page calls \u003Ccode>useFetch(&#39;\u002Fapi\u002Fprofile&#39;)\u003C\u002Fcode> or \u003Ccode>$fetch(&#39;\u002Fapi\u002Fprofile&#39;)\u003C\u002Fcode>, which sends a request that Nitro routes to your \u003Ccode>server\u002Fapi\u002Fprofile.get.ts\u003C\u002Fcode> handler exactly like it would route a request from curl or a browser tab. The response crosses back as plain, serializable data — never a live object, never a shared reference, never a \u003Ccode>ref\u003C\u002Fcode>. Whatever you build inside a server route has to assume it&#39;s talking to \u003Cem>some\u003C\u002Fem> client, not sharing memory with one.\u003C\u002Fp>",{"id":159,"html":160,"type":83},"b20","\u003Cp>\u003Cstrong>Key concept:\u003C\u002Fstrong> if you can&#39;t point to the \u003Ccode>.vue\u003C\u002Fcode> file or component \u003Ccode>setup()\u003C\u002Fcode> a piece of code runs inside, it isn&#39;t in the Vue world — and a \u003Ccode>server\u002Fapi\u003C\u002Fcode>, \u003Ccode>server\u002Froutes\u003C\u002Fcode>, or \u003Ccode>server\u002Fmiddleware\u003C\u002Fcode> file never is.\u003C\u002Fp>",{"id":162,"html":163,"text":163,"type":91,"level":31},"b21","Building server routes, stage by stage",{"id":165,"html":166,"text":166,"type":91,"level":43},"b22","Stage 1 — filenames are routes",{"id":168,"html":169,"type":83},"b23","\u003Cp>Nitro turns \u003Ccode>server\u002Fapi\u003C\u002Fcode> and \u003Ccode>server\u002Froutes\u003C\u002Fcode> into a router by convention, no manual registration:\u003C\u002Fp>",{"id":171,"type":97,"items":172,"ordered":18},"b24",[173,174,175,176,177],"\u003Ccode>server\u002Fapi\u002Fhello.ts\u003C\u002Fcode> → matches any method at \u003Ccode>\u002Fapi\u002Fhello\u003C\u002Fcode>","\u003Ccode>server\u002Fapi\u002Fhello.get.ts\u003C\u002Fcode> → matches only \u003Ccode>GET \u002Fapi\u002Fhello\u003C\u002Fcode>; \u003Ccode>hello.post.ts\u003C\u002Fcode> only \u003Ccode>POST\u003C\u002Fcode>","\u003Ccode>server\u002Fapi\u002Fusers\u002F[id].ts\u003C\u002Fcode> → dynamic segment, matched with \u003Ccode>getRouterParam(event, &#39;id&#39;)\u003C\u002Fcode>","\u003Ccode>server\u002Fapi\u002Ffiles\u002F[...slug].ts\u003C\u002Fcode> → catch-all, everything after \u003Ccode>\u002Ffiles\u002F\u003C\u002Fcode> lands in \u003Ccode>getRouterParam(event, &#39;slug&#39;)\u003C\u002Fcode> (an \u003Cem>unnamed\u003C\u002Fem> catch-all, \u003Ccode>[...].ts\u003C\u002Fcode>, lands in \u003Ccode>event.context.params._\u003C\u002Fcode> instead)","\u003Ccode>server\u002Froutes\u002Frobots.txt.ts\u003C\u002Fcode> → same rules, but \u003Cstrong>no automatic \u003Ccode>\u002Fapi\u003C\u002Fcode> prefix\u003C\u002Fstrong> — useful for exact, non-API paths like \u003Ccode>robots.txt\u003C\u002Fcode>, \u003Ccode>sitemap.xml\u003C\u002Fcode>, or a webhook URL a third party expects at a fixed path",{"id":179,"html":180,"text":180,"type":91,"level":43},"b25","Stage 2 — reading input, returning output",{"id":182,"html":183,"type":83},"b26","\u003Cp>Every handler is wrapped in \u003Ccode>defineEventHandler\u003C\u002Fcode>, and gets one \u003Ccode>H3Event\u003C\u002Fcode> to work with:\u003C\u002Fp>",{"id":185,"code":186,"type":134,"language":135,"highlight":187},"b27","\u002F\u002F server\u002Fapi\u002Fusers\u002F[id].get.ts\nexport default defineEventHandler(async (event) => {\n  const id = getRouterParam(event, 'id')\n  const { includeOrders } = getQuery(event) \u002F\u002F ?includeOrders=true\n\n  const user = await findUser(id)\n  if (!user) {\n    throw createError({ status: 404, statusText: 'User not found' })\n  }\n\n  return { user, includeOrders: includeOrders === 'true' }\n})",[],{"id":189,"html":190,"type":83},"b28","\u003Cp>Whatever you \u003Ccode>return\u003C\u002Fcode> — an object, an array, a string — gets serialized to the right response automatically (JSON for objects and arrays; strings sent as-is, with a \u003Ccode>text\u002Fhtml\u003C\u002Fcode> content type unless you set one). \u003Ccode>createError\u003C\u002Fcode> is the correct way to fail: it sets the real HTTP status and gives the client a structured error body, instead of a generic 500 from an uncaught throw. For a \u003Ccode>POST\u003C\u002Fcode>\u002F\u003Ccode>PUT\u003C\u002Fcode> body, \u003Ccode>readBody(event)\u003C\u002Fcode> parses it based on the request&#39;s content type — JSON, form-encoded, or plain text.\u003C\u002Fp>",{"id":192,"html":193,"text":194,"type":91,"level":43},"b29","Stage 3 — middleware runs on \u003Cem>everything\u003C\u002Fem>, in an order you don&#39;t choose","Stage 3 — middleware runs on everything, in an order you don't choose",{"id":196,"html":197,"type":83},"b30","\u003Cp>\u003Ccode>server\u002Fmiddleware\u002F*.ts\u003C\u002Fcode> files run before every request Nitro handles — not just \u003Ccode>\u002Fapi\u002F*\u003C\u002Fcode>, but page requests too, since a page request is also something Nitro routes. A middleware doesn&#39;t return a response (to end a request early, throw \u003Ccode>createError\u003C\u002Fcode> instead of returning); it inspects or mutates the request and lets it continue, usually by writing to \u003Ccode>event.context\u003C\u002Fcode> so a later handler can read it:\u003C\u002Fp>",{"id":199,"code":200,"type":134,"language":135,"highlight":201},"b31","\u002F\u002F server\u002Fmiddleware\u002Fauth.ts\nexport default defineEventHandler((event) => {\n  const token = getHeader(event, 'authorization')\n  event.context.user = token ? verifyToken(token) : null\n  \u002F\u002F no return — request continues to the matched route\n})",[],{"id":203,"html":204,"type":83},"b32","\u003Cp>The order these run in is \u003Cstrong>alphabetical by filename, sorted as a string\u003C\u002Fstrong> — not the order you created them in, and not numeric order either. \u003Ccode>&quot;10.rate-limit.ts&quot;\u003C\u002Fcode> sorts \u003Cem>before\u003C\u002Fem> \u003Ccode>&quot;2.legacy.ts&quot;\u003C\u002Fcode>, because string comparison looks at the character \u003Ccode>&#39;1&#39;\u003C\u002Fcode> before it ever gets to \u003Ccode>&#39;2&#39;\u003C\u002Fcode>. If you need explicit ordering, zero-pad: \u003Ccode>01.\u003C\u002Fcode>, \u003Ccode>02.\u003C\u002Fcode>, \u003Ccode>03.\u003C\u002Fcode> — never bare \u003Ccode>1.\u003C\u002Fcode>, \u003Ccode>2.\u003C\u002Fcode>, \u003Ccode>10.\u003C\u002Fcode>.\u003C\u002Fp>",{"id":206,"html":207,"text":208,"type":91,"level":43},"b33","Stage 4 — the SSR bridge, without the network hop you&#39;d expect","Stage 4 — the SSR bridge, without the network hop you'd expect",{"id":210,"html":211,"type":83},"b34","\u003Cp>When a page calls \u003Ccode>useFetch(&#39;\u002Fapi\u002Fprofile&#39;)\u003C\u002Fcode> (or the plain \u003Ccode>$fetch\u003C\u002Fcode> it&#39;s built on) while rendering on the server, Nitro doesn&#39;t open a real HTTP connection to itself. It recognizes the request is for one of its own routes and calls the matching function directly, in-process — this is documented, intentional behavior, not an implementation detail you&#39;re relying on by accident. The same call from the \u003Cem>browser\u003C\u002Fem>, after hydration, does go over real HTTP, because at that point there&#39;s no server process to short-circuit into. \u003Ccode>useFetch\u003C\u002Fcode> also writes the server-side result into the page&#39;s payload, so the client doesn&#39;t refetch it on hydration — the same payload mechanism the hydration-mismatch episode in this series covers in more depth.\u003C\u002Fp>",{"id":213,"html":214,"text":214,"type":91,"level":31},"b35","Edge cases and gotchas",{"id":216,"html":217,"type":83},"b36","\u003Cp>\u003Cstrong>The numeric-prefix sort trap isn&#39;t limited to \u003Ccode>server\u002Fmiddleware\u003C\u002Fcode>.\u003C\u002Fstrong> Global route middleware (files ending \u003Ccode>.global.ts\u003C\u002Fcode>, which run in the Vue\u002Fapp world, not Nitro&#39;s) follow the exact same alphabetical-string rule. If you&#39;ve zero-padded one and not the other, you now have two different, easy-to-miss ordering bugs in the same project.\u003C\u002Fp>",{"id":219,"html":220,"type":83},"b37","\u003Cp>\u003Cstrong>\u003Ccode>event.context.params\u003C\u002Fcode> can be typed as possibly-\u003Ccode>undefined\u003C\u002Fcode>\u003C\u002Fstrong> even on a route where a dynamic segment guarantees it exists, because the type comes from the general Nitro types, not your specific route. Prefer \u003Ccode>getRouterParam(event, &#39;id&#39;)\u003C\u002Fcode> over reaching into \u003Ccode>event.context.params\u003C\u002Fcode> directly — it reads the same value with a cleaner, purpose-built API.\u003C\u002Fp>",{"id":222,"html":223,"type":83},"b38","\u003Cp>\u003Cstrong>\u003Ccode>defineCachedEventHandler\u003C\u002Fcode> and \u003Ccode>readBody\u003C\u002Fcode> don&#39;t currently mix well\u003C\u002Fstrong> — the cached handler&#39;s event type deliberately omits \u003Ccode>body\u003C\u002Fcode>, and the cache key is built from the URL (plus any \u003Ccode>varies\u003C\u002Fcode> headers), never the body — so two different POST bodies would share one cached response. Don&#39;t cache routes whose output depends on the request body.\u003C\u002Fp>",{"id":225,"html":226,"type":83},"b39","\u003Cp>\u003Cstrong>A \u003Ccode>server\u002Fapi\u003C\u002Fcode> route your page never calls directly is still public.\u003C\u002Fstrong> There&#39;s no implicit auth boundary between &quot;routes I use internally&quot; and &quot;routes anyone can hit&quot; — every file under \u003Ccode>server\u002Fapi\u003C\u002Fcode> is a real, reachable HTTP endpoint the moment it ships, whether or not any of your own pages ever call it.\u003C\u002Fp>",{"id":228,"html":229,"text":229,"type":91,"level":31},"b40","Best practices",{"id":231,"type":97,"items":232,"ordered":18},"b41",[233,234,235,236,237],"\u003Cstrong>Never reach for a Vue composable inside a server route.\u003C\u002Fstrong> If server-side logic needs to be shared between multiple handlers, put it in \u003Ccode>server\u002Futils\u002F\u003C\u002Fcode> as a plain function — it&#39;s auto-imported inside \u003Ccode>server\u002F\u003C\u002Fcode>, same as composables are inside \u003Ccode>app\u002F\u003C\u002Fcode>, but it&#39;s just a function, not something tied to a Vue instance.","\u003Cstrong>Zero-pad any filename whose order matters\u003C\u002Fstrong> — \u003Ccode>01.auth.ts\u003C\u002Fcode>, \u003Ccode>02.logging.ts\u003C\u002Fcode> — so a later teammate adding \u003Ccode>03.rate-limit.ts\u003C\u002Fcode> doesn&#39;t silently jump ahead of \u003Ccode>2.something.ts\u003C\u002Fcode> that was never renumbered.","\u003Cstrong>Validate input at the top of the handler\u003C\u002Fstrong>, before touching a database or an external API — \u003Ccode>readValidatedBody\u003C\u002Fcode> with a schema (Zod or otherwise) turns a malformed request into a clean 400 instead of a confusing failure three lines deeper.","\u003Cstrong>Keep secrets out of the public runtime config.\u003C\u002Fstrong> \u003Ccode>nuxt.config\u003C\u002Fcode>&#39;s \u003Ccode>runtimeConfig\u003C\u002Fcode> (server-only) versus \u003Ccode>runtimeConfig.public\u003C\u002Fcode> (shipped to the client bundle) is the one line standing between an API key and every visitor&#39;s browser dev tools — a server route can safely read the private half; in a page component the private keys exist only during the server render and never reach the browser — so never render them or put them in \u003Ccode>useState\u003C\u002Fcode>.","\u003Cstrong>Treat every \u003Ccode>server\u002Fapi\u003C\u002Fcode> file as a public endpoint from the day it&#39;s created\u003C\u002Fstrong>, and add auth\u002Fvalidation before the first real feature depends on it, not after.",{"id":239,"html":240,"text":240,"type":91,"level":31},"b42","FAQ",{"id":242,"html":243,"text":244,"type":91,"level":43},"b43","Can I use \u003Ccode>useState\u003C\u002Fcode> or \u003Ccode>useRoute\u003C\u002Fcode> inside a server route?","Can I use useState or useRoute inside a server route?",{"id":246,"html":247,"type":83},"b44","\u003Cp>No — those composables require a live Nuxt application instance, which only exists in the Vue\u002Fapp world (pages, components, plugins). A \u003Ccode>server\u002Fapi\u003C\u002Fcode>\u002F\u003Ccode>server\u002Froutes\u003C\u002Fcode>\u002F\u003Ccode>server\u002Fmiddleware\u003C\u002Fcode> file runs as a plain Nitro\u002Fh3 handler with no such instance. Share logic through \u003Ccode>server\u002Futils\u002F\u003C\u002Fcode> instead.\u003C\u002Fp>",{"id":249,"html":250,"text":251,"type":91,"level":43},"b45","What&#39;s the actual difference between \u003Ccode>server\u002Fapi\u003C\u002Fcode> and \u003Ccode>server\u002Froutes\u003C\u002Fcode>?","What's the actual difference between server\u002Fapi and server\u002Froutes?",{"id":253,"html":254,"type":83},"b46","\u003Cp>Identical routing rules (filenames, method suffixes, dynamic segments) — the only difference is that \u003Ccode>server\u002Fapi\u003C\u002Fcode> files are automatically prefixed with \u003Ccode>\u002Fapi\u003C\u002Fcode>, and \u003Ccode>server\u002Froutes\u003C\u002Fcode> files are not. Use \u003Ccode>server\u002Froutes\u003C\u002Fcode> for paths that need to be exact, like \u003Ccode>\u002Frobots.txt\u003C\u002Fcode> or a fixed webhook URL.\u003C\u002Fp>",{"id":256,"html":257,"text":257,"type":91,"level":43},"b47","Why does my logging middleware run before my auth middleware, even though I created auth first?",{"id":259,"html":260,"type":83},"b48","\u003Cp>\u003Ccode>server\u002Fmiddleware\u003C\u002Fcode> files run in alphabetical order of their filename, sorted as a string — creation order and file-tree position don&#39;t matter. Rename the files with zero-padded numeric prefixes (\u003Ccode>01.auth.ts\u003C\u002Fcode>, \u003Ccode>02.logging.ts\u003C\u002Fcode>) to force the order you want.\u003C\u002Fp>",{"id":262,"html":263,"text":264,"type":91,"level":43},"b49","Does calling my own \u003Ccode>\u002Fapi\u003C\u002Fcode> route with \u003Ccode>useFetch\u003C\u002Fcode> make a real network request?","Does calling my own \u002Fapi route with useFetch make a real network request?",{"id":266,"html":267,"type":83},"b50","\u003Cp>Only from the browser. During SSR, Nitro recognizes the target is one of its own routes and calls the handler function directly, in the same process — no HTTP round trip. After hydration, the same call from the browser does go over the network like any other request.\u003C\u002Fp>",{"id":269,"html":270,"text":271,"type":91,"level":43},"b51","Can a dynamic route segment ever be \u003Ccode>undefined\u003C\u002Fcode> at runtime?","Can a dynamic route segment ever be undefined at runtime?",{"id":273,"html":274,"type":83},"b52","\u003Cp>Not for a segment your filename guarantees — \u003Ccode>[id].ts\u003C\u002Fcode> will always have an \u003Ccode>id\u003C\u002Fcode> on a matched request, even though its TypeScript type may be looser than that. Use \u003Ccode>getRouterParam(event, &#39;id&#39;)\u003C\u002Fcode> (still typed \u003Ccode>string | undefined\u003C\u002Fcode>) or \u003Ccode>getValidatedRouterParams\u003C\u002Fcode> with a schema when you want a guaranteed, typed value.\u003C\u002Fp>",{"id":276,"html":277,"text":277,"type":91,"level":31},"b53","Cheat sheet",{"id":279,"head":280,"rows":283,"type":317},"b54",[281,282],"Want to...","Do this",[284,287,290,293,296,299,302,305,308,311,314],[285,286],"Match any method at \u003Ccode>\u002Fapi\u002Fx\u003C\u002Fcode>","\u003Ccode>server\u002Fapi\u002Fx.ts\u003C\u002Fcode>",[288,289],"Match only \u003Ccode>GET\u003C\u002Fcode>\u002F\u003Ccode>POST\u003C\u002Fcode>\u002Fetc.","\u003Ccode>server\u002Fapi\u002Fx.get.ts\u003C\u002Fcode> \u002F \u003Ccode>x.post.ts\u003C\u002Fcode>",[291,292],"Match a dynamic segment","\u003Ccode>server\u002Fapi\u002Fx\u002F[id].ts\u003C\u002Fcode> → \u003Ccode>getRouterParam(event, &#39;id&#39;)\u003C\u002Fcode>",[294,295],"Match a catch-all","\u003Ccode>server\u002Fapi\u002Fx\u002F[...slug].ts\u003C\u002Fcode> → \u003Ccode>getRouterParam(event, &#39;slug&#39;)\u003C\u002Fcode>",[297,298],"Serve a path with no \u003Ccode>\u002Fapi\u003C\u002Fcode> prefix","\u003Ccode>server\u002Froutes\u002Frobots.txt.ts\u003C\u002Fcode>",[300,301],"Run code before every request","\u003Ccode>server\u002Fmiddleware\u002FNN.name.ts\u003C\u002Fcode> (zero-padded prefix)",[303,304],"Pass data from middleware to a handler","\u003Ccode>event.context.yourKey = value\u003C\u002Fcode>",[306,307],"Read the query string \u002F body","\u003Ccode>getQuery(event)\u003C\u002Fcode> \u002F \u003Ccode>readBody(event)\u003C\u002Fcode>",[309,310],"Fail with a real HTTP status","\u003Ccode>throw createError({ status, statusText })\u003C\u002Fcode>",[312,313],"Share logic between server routes","a plain function in \u003Ccode>server\u002Futils\u002F\u003C\u002Fcode>",[315,316],"Keep a value out of the client bundle","\u003Ccode>runtimeConfig\u003C\u002Fcode> (not \u003Ccode>.public\u003C\u002Fcode>) in \u003Ccode>nuxt.config\u003C\u002Fcode>","table",{"id":319,"html":320,"type":83},"b55","\u003C!-- playground:start -->",{"id":322,"html":323,"text":323,"type":91,"level":31},"b56","🎮 Try it yourself",{"id":325,"html":326,"type":83},"b57","\u003Cp>\u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnuxt-weekly-nitro-server-routes\u002Fplayground\">▶️ Open the interactive playground →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":328,"html":329,"type":83},"b58","\u003Cp>\u003Cem>Runs right in your browser — poke at it and watch the concept react live.\u003C\u002Fem>\u003C\u002Fp>",{"id":331,"html":332,"type":83},"b59","\u003C!-- playground:end -->",{"id":334,"html":335,"text":335,"type":91,"level":31},"b60","Key takeaways",{"id":337,"type":97,"items":338,"ordered":18},"b61",[339,340,341,342],"\u003Ccode>server\u002Fapi\u003C\u002Fcode>, \u003Ccode>server\u002Froutes\u003C\u002Fcode>, and \u003Ccode>server\u002Fmiddleware\u003C\u002Fcode> run in Nitro — a separate request-handling world from the Vue app your pages render in, with no component instance and no access to composables like \u003Ccode>useState\u003C\u002Fcode> or \u003Ccode>useRoute\u003C\u002Fcode>.","Routing is entirely filename-driven: the path, the HTTP method, dynamic segments, and catch-alls are all decided by how you name the file, not by any registration code.","\u003Ccode>server\u002Fmiddleware\u003C\u002Fcode> order is alphabetical string-sort of the filename, not creation order and not numeric order — zero-pad any prefix that has to hold a specific position.","Calling your own API route with \u003Ccode>useFetch\u003C\u002Fcode> during SSR skips the network and calls the function directly; the same call from the browser after hydration is a real HTTP request.",{"id":344,"html":345,"type":83},"b62","\u003C!-- quiz:start -->",{"id":347,"html":348,"text":348,"type":91,"level":31},"b63","🧠 Test yourself",{"id":350,"html":351,"type":83},"b64","\u003Cp>Think it clicked? \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnuxt-weekly-nitro-server-routes\u002Fquiz\">Take the 8-question quiz →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":353,"html":354,"type":83},"b65","\u003Cp>\u003Cem>Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.\u003C\u002Fem>\u003C\u002Fp>",{"id":356,"html":357,"type":83},"b66","\u003C!-- quiz:end -->",{"id":359,"html":360,"text":360,"type":91,"level":31},"b67","The endpoint that finally made sense",{"id":362,"html":363,"type":83},"b68","\u003Cp>That \u003Ccode>\u002Fapi\u002Fprofile\u003C\u002Fcode> handler from the top of the article has an honest fix now: drop the \u003Ccode>useState\u003C\u002Fcode> call, read the user from \u003Ccode>event.context\u003C\u002Fcode> (set by an auth middleware upstream), and return plain data. Nothing about the fix is exotic — it&#39;s just respecting that the file it lives in was never part of the Vue app to begin with.\u003C\u002Fp>",{"id":365,"html":366,"type":83},"b69","\u003Cp>Next time a server route throws \u003Ccode>useState is not defined\u003C\u002Fcode>, or a middleware runs in an order you didn&#39;t expect, you&#39;ll know exactly which of the two worlds you&#39;re standing in — and that&#39;s most of the debugging done before you&#39;ve even opened the stack trace.\u003C\u002Fp>",{"id":368,"html":369,"type":83},"b70","\u003C!-- related:start -->",{"id":371,"html":372,"text":372,"type":91,"level":31},"b71","📚 Read next",{"id":374,"type":97,"items":375,"ordered":18},"b72",[376,377,378],"\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnuxt-weekly-hydration-mismatch\">Nuxt Hydration Mismatch: Why It Happens and How to Fix It\u003C\u002Fa>","\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnuxt-weekly-useasyncdata-keys-dedupe\">useAsyncData Keys in Nuxt: Caching, Dedupe &amp; the Sharing Bug\u003C\u002Fa>","\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnuxt-weekly-cross-request-state-leak\">Nuxt useState vs ref(): Why Server State Leaks Across Users\u003C\u002Fa>",{"id":380,"html":381,"type":83},"b73","\u003C!-- related:end -->",{"id":383,"type":384},"b74","divider",{"id":386,"html":387,"type":83},"b75","\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":389,"html":390,"type":83},"b76","\u003Cp>\u003Cem>Thanks for reading! Let&#39;s stay connected:\u003C\u002Fem>\u003C\u002Fp>",{"id":392,"type":97,"items":393,"ordered":18},"b77",[394,395,396],"⭐ \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>","Open any Nuxt project and there's a good chance a `server\u002Fapi` folder is already sitting in it — a `hello.ts` here, a `login.post.ts` there. Ask most people what it is and the answer is usually \"the API routes.\" Ask what actually runs those files, in what order, and whether they can use the same `useState` or `useRoute` composables as the rest of the app, and the answers get much shakier. That gap is where the interesting bugs live: a middleware that silently runs before the one it's supposed to follow, an event handler that dies with `useState is not defined` for no obvious reason, a `readBody()` that comes back empty.\n\nThis article is written against **Nuxt 4.x** (verified against the v4.5 release line, August 2026 — Nuxt 3 reached end-of-life on July 31, 2026). The directory names below assume the `server\u002F` layout, which — unlike `pages\u002F`, `components\u002F`, and the rest of your app code — stays at the project root in both Nuxt 3 and Nuxt 4's new `app\u002F`-nested structure. If you've read [the hydration mismatch episode](https:\u002F\u002Fdev.to\u002Fparsajiravand\u002Fnuxt-hydration-mismatch-why-it-happens-and-how-to-fix-it-5b7i) or [the `useState` vs `ref` episode](https:\u002F\u002Fdev.to\u002Fparsajiravand\u002Fnuxt-usestate-vs-ref-why-server-state-leaks-across-users-47n1), this one picks up the other half of \"what runs where\": not the Vue app rendering twice, but the completely separate server world sitting next to it.\n\n## What you'll learn\n\nBy the end of this article you'll be able to:\n\n- Explain what Nitro and `server\u002Fapi`\u002F`server\u002Froutes`\u002F`server\u002Fmiddleware` actually are, and how a filename becomes a route\n- Read and write request data correctly with `getQuery`, `getRouterParam`, and `readBody`\n- Predict the real execution order of your server middleware — not the order you assume\n- Explain why Vue composables like `useState` don't work inside a server route, and what to use instead\n- Know when calling your own API route with `useFetch` during SSR involves the network at all\n\n## Who this is for\n\nYou've built Nuxt pages and components, and you've probably already dropped a file into `server\u002Fapi` and had it work. You don't need prior backend framework experience — this article treats Nitro as its own subject, not \"Express with different syntax.\"\n\n## Table of contents\n\n- [The problem: it looks like the rest of your app, but it isn't](#the-problem-it-looks-like-the-rest-of-your-app-but-it-isnt)\n- [The mental model: two runtimes, one project](#the-mental-model-two-runtimes-one-project)\n- [Building server routes, stage by stage](#building-server-routes-stage-by-stage)\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: it looks like the rest of your app, but it isn't\n\nSay you want a small `\u002Fapi\u002Fprofile` endpoint that returns the current user, and — since you already have a `useState('user')` that holds the logged-in user everywhere else in the app — reusing it here feels natural:\n\n```ts\n\u002F\u002F server\u002Fapi\u002Fprofile.get.ts — looks reasonable, isn't\nexport default defineEventHandler((event) => {\n  const user = useState('user') \u002F\u002F ❌ throws at runtime\n  return { user: user.value }\n})\n```\n\nRun it, and instead of JSON you get a 500: `useState is not defined`. It's the exact composable you use in every `.vue` file, so it reads like a missing import — but it isn't one. `server\u002F` gets its own auto-imports (h3 helpers, Nitro utilities, `server\u002Futils`), and Vue composables aren't among them; import it from `#app` explicitly and the build refuses with \"Vue app aliases are not allowed in server runtime.\" The problem is *where* it's being called from. `useState`, `useRoute`, `useFetch` — the whole family of Nuxt composables — depend on there being a current Nuxt application instance to attach to. A `server\u002Fapi` file has no such thing. It's not part of the Vue app at all; it's a plain request handler that Nitro invokes directly, with nothing Vue-shaped anywhere near it.\n\nThis is the trap the \"Nuxt is just Vue with routing\" mental model sets. `server\u002Fapi` looks like it belongs to the same app as your pages because it lives in the same repo, ships in the same deploy, and even shares the same `nuxt dev` process — but it's a different runtime with a different lifecycle, and the rules that make composables work don't apply there.\n\n## The mental model: two runtimes, one project\n\nA Nuxt project is really two separate request-handling worlds glued together at build time:\n\n**The Vue\u002Fapp world.** Pages, components, layouts, and composables. Every request for a page spins up a Nuxt application instance (server-side, then again client-side for hydration), and that instance is what `useState`, `useRoute`, and friends attach themselves to. This is the world the last two episodes of this series lived in.\n\n**The Nitro\u002Fh3 world.** `server\u002Fapi`, `server\u002Froutes`, and `server\u002Fmiddleware`. Nitro is the server engine Nuxt is built on — it's what starts the process, decides which file handles which URL, and runs each matched file as a plain function that receives an `H3Event` (from `h3`, the tiny HTTP toolkit Nitro is built around) and returns a value. There is no component tree here, no \"current instance,\" nothing for a Vue composable to hook into. It doesn't know or care that a Vue app exists elsewhere in the same process.\n\nThe two worlds do talk to each other, but only across an explicit boundary: a page calls `useFetch('\u002Fapi\u002Fprofile')` or `$fetch('\u002Fapi\u002Fprofile')`, which sends a request that Nitro routes to your `server\u002Fapi\u002Fprofile.get.ts` handler exactly like it would route a request from curl or a browser tab. The response crosses back as plain, serializable data — never a live object, never a shared reference, never a `ref`. Whatever you build inside a server route has to assume it's talking to *some* client, not sharing memory with one.\n\n**Key concept:** if you can't point to the `.vue` file or component `setup()` a piece of code runs inside, it isn't in the Vue world — and a `server\u002Fapi`, `server\u002Froutes`, or `server\u002Fmiddleware` file never is.\n\n## Building server routes, stage by stage\n\n### Stage 1 — filenames are routes\n\nNitro turns `server\u002Fapi` and `server\u002Froutes` into a router by convention, no manual registration:\n\n- `server\u002Fapi\u002Fhello.ts` → matches any method at `\u002Fapi\u002Fhello`\n- `server\u002Fapi\u002Fhello.get.ts` → matches only `GET \u002Fapi\u002Fhello`; `hello.post.ts` only `POST`\n- `server\u002Fapi\u002Fusers\u002F[id].ts` → dynamic segment, matched with `getRouterParam(event, 'id')`\n- `server\u002Fapi\u002Ffiles\u002F[...slug].ts` → catch-all, everything after `\u002Ffiles\u002F` lands in `getRouterParam(event, 'slug')` (an *unnamed* catch-all, `[...].ts`, lands in `event.context.params._` instead)\n- `server\u002Froutes\u002Frobots.txt.ts` → same rules, but **no automatic `\u002Fapi` prefix** — useful for exact, non-API paths like `robots.txt`, `sitemap.xml`, or a webhook URL a third party expects at a fixed path\n\n### Stage 2 — reading input, returning output\n\nEvery handler is wrapped in `defineEventHandler`, and gets one `H3Event` to work with:\n\n```ts\n\u002F\u002F server\u002Fapi\u002Fusers\u002F[id].get.ts\nexport default defineEventHandler(async (event) => {\n  const id = getRouterParam(event, 'id')\n  const { includeOrders } = getQuery(event) \u002F\u002F ?includeOrders=true\n\n  const user = await findUser(id)\n  if (!user) {\n    throw createError({ status: 404, statusText: 'User not found' })\n  }\n\n  return { user, includeOrders: includeOrders === 'true' }\n})\n```\n\nWhatever you `return` — an object, an array, a string — gets serialized to the right response automatically (JSON for objects and arrays; strings sent as-is, with a `text\u002Fhtml` content type unless you set one). `createError` is the correct way to fail: it sets the real HTTP status and gives the client a structured error body, instead of a generic 500 from an uncaught throw. For a `POST`\u002F`PUT` body, `readBody(event)` parses it based on the request's content type — JSON, form-encoded, or plain text.\n\n### Stage 3 — middleware runs on *everything*, in an order you don't choose\n\n`server\u002Fmiddleware\u002F*.ts` files run before every request Nitro handles — not just `\u002Fapi\u002F*`, but page requests too, since a page request is also something Nitro routes. A middleware doesn't return a response (to end a request early, throw `createError` instead of returning); it inspects or mutates the request and lets it continue, usually by writing to `event.context` so a later handler can read it:\n\n```ts\n\u002F\u002F server\u002Fmiddleware\u002Fauth.ts\nexport default defineEventHandler((event) => {\n  const token = getHeader(event, 'authorization')\n  event.context.user = token ? verifyToken(token) : null\n  \u002F\u002F no return — request continues to the matched route\n})\n```\n\nThe order these run in is **alphabetical by filename, sorted as a string** — not the order you created them in, and not numeric order either. `\"10.rate-limit.ts\"` sorts *before* `\"2.legacy.ts\"`, because string comparison looks at the character `'1'` before it ever gets to `'2'`. If you need explicit ordering, zero-pad: `01.`, `02.`, `03.` — never bare `1.`, `2.`, `10.`.\n\n### Stage 4 — the SSR bridge, without the network hop you'd expect\n\nWhen a page calls `useFetch('\u002Fapi\u002Fprofile')` (or the plain `$fetch` it's built on) while rendering on the server, Nitro doesn't open a real HTTP connection to itself. It recognizes the request is for one of its own routes and calls the matching function directly, in-process — this is documented, intentional behavior, not an implementation detail you're relying on by accident. The same call from the *browser*, after hydration, does go over real HTTP, because at that point there's no server process to short-circuit into. `useFetch` also writes the server-side result into the page's payload, so the client doesn't refetch it on hydration — the same payload mechanism the hydration-mismatch episode in this series covers in more depth.\n\n## Edge cases and gotchas\n\n**The numeric-prefix sort trap isn't limited to `server\u002Fmiddleware`.** Global route middleware (files ending `.global.ts`, which run in the Vue\u002Fapp world, not Nitro's) follow the exact same alphabetical-string rule. If you've zero-padded one and not the other, you now have two different, easy-to-miss ordering bugs in the same project.\n\n**`event.context.params` can be typed as possibly-`undefined`** even on a route where a dynamic segment guarantees it exists, because the type comes from the general Nitro types, not your specific route. Prefer `getRouterParam(event, 'id')` over reaching into `event.context.params` directly — it reads the same value with a cleaner, purpose-built API.\n\n**`defineCachedEventHandler` and `readBody` don't currently mix well** — the cached handler's event type deliberately omits `body`, and the cache key is built from the URL (plus any `varies` headers), never the body — so two different POST bodies would share one cached response. Don't cache routes whose output depends on the request body.\n\n**A `server\u002Fapi` route your page never calls directly is still public.** There's no implicit auth boundary between \"routes I use internally\" and \"routes anyone can hit\" — every file under `server\u002Fapi` is a real, reachable HTTP endpoint the moment it ships, whether or not any of your own pages ever call it.\n\n## Best practices\n\n- **Never reach for a Vue composable inside a server route.** If server-side logic needs to be shared between multiple handlers, put it in `server\u002Futils\u002F` as a plain function — it's auto-imported inside `server\u002F`, same as composables are inside `app\u002F`, but it's just a function, not something tied to a Vue instance.\n- **Zero-pad any filename whose order matters** — `01.auth.ts`, `02.logging.ts` — so a later teammate adding `03.rate-limit.ts` doesn't silently jump ahead of `2.something.ts` that was never renumbered.\n- **Validate input at the top of the handler**, before touching a database or an external API — `readValidatedBody` with a schema (Zod or otherwise) turns a malformed request into a clean 400 instead of a confusing failure three lines deeper.\n- **Keep secrets out of the public runtime config.** `nuxt.config`'s `runtimeConfig` (server-only) versus `runtimeConfig.public` (shipped to the client bundle) is the one line standing between an API key and every visitor's browser dev tools — a server route can safely read the private half; in a page component the private keys exist only during the server render and never reach the browser — so never render them or put them in `useState`.\n- **Treat every `server\u002Fapi` file as a public endpoint from the day it's created**, and add auth\u002Fvalidation before the first real feature depends on it, not after.\n\n## FAQ\n\n### Can I use `useState` or `useRoute` inside a server route?\n\nNo — those composables require a live Nuxt application instance, which only exists in the Vue\u002Fapp world (pages, components, plugins). A `server\u002Fapi`\u002F`server\u002Froutes`\u002F`server\u002Fmiddleware` file runs as a plain Nitro\u002Fh3 handler with no such instance. Share logic through `server\u002Futils\u002F` instead.\n\n### What's the actual difference between `server\u002Fapi` and `server\u002Froutes`?\n\nIdentical routing rules (filenames, method suffixes, dynamic segments) — the only difference is that `server\u002Fapi` files are automatically prefixed with `\u002Fapi`, and `server\u002Froutes` files are not. Use `server\u002Froutes` for paths that need to be exact, like `\u002Frobots.txt` or a fixed webhook URL.\n\n### Why does my logging middleware run before my auth middleware, even though I created auth first?\n\n`server\u002Fmiddleware` files run in alphabetical order of their filename, sorted as a string — creation order and file-tree position don't matter. Rename the files with zero-padded numeric prefixes (`01.auth.ts`, `02.logging.ts`) to force the order you want.\n\n### Does calling my own `\u002Fapi` route with `useFetch` make a real network request?\n\nOnly from the browser. During SSR, Nitro recognizes the target is one of its own routes and calls the handler function directly, in the same process — no HTTP round trip. After hydration, the same call from the browser does go over the network like any other request.\n\n### Can a dynamic route segment ever be `undefined` at runtime?\n\nNot for a segment your filename guarantees — `[id].ts` will always have an `id` on a matched request, even though its TypeScript type may be looser than that. Use `getRouterParam(event, 'id')` (still typed `string | undefined`) or `getValidatedRouterParams` with a schema when you want a guaranteed, typed value.\n\n## Cheat sheet\n\n| Want to... | Do this |\n| --- | --- |\n| Match any method at `\u002Fapi\u002Fx` | `server\u002Fapi\u002Fx.ts` |\n| Match only `GET`\u002F`POST`\u002Fetc. | `server\u002Fapi\u002Fx.get.ts` \u002F `x.post.ts` |\n| Match a dynamic segment | `server\u002Fapi\u002Fx\u002F[id].ts` → `getRouterParam(event, 'id')` |\n| Match a catch-all | `server\u002Fapi\u002Fx\u002F[...slug].ts` → `getRouterParam(event, 'slug')` |\n| Serve a path with no `\u002Fapi` prefix | `server\u002Froutes\u002Frobots.txt.ts` |\n| Run code before every request | `server\u002Fmiddleware\u002FNN.name.ts` (zero-padded prefix) |\n| Pass data from middleware to a handler | `event.context.yourKey = value` |\n| Read the query string \u002F body | `getQuery(event)` \u002F `readBody(event)` |\n| Fail with a real HTTP status | `throw createError({ status, statusText })` |\n| Share logic between server routes | a plain function in `server\u002Futils\u002F` |\n| Keep a value out of the client bundle | `runtimeConfig` (not `.public`) in `nuxt.config` |\n\n\u003C!-- playground:start -->\n\n## 🎮 Try it yourself\n\n**[▶️ Open the interactive playground →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnuxt-weekly-nitro-server-routes\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- `server\u002Fapi`, `server\u002Froutes`, and `server\u002Fmiddleware` run in Nitro — a separate request-handling world from the Vue app your pages render in, with no component instance and no access to composables like `useState` or `useRoute`.\n- Routing is entirely filename-driven: the path, the HTTP method, dynamic segments, and catch-alls are all decided by how you name the file, not by any registration code.\n- `server\u002Fmiddleware` order is alphabetical string-sort of the filename, not creation order and not numeric order — zero-pad any prefix that has to hold a specific position.\n- Calling your own API route with `useFetch` during SSR skips the network and calls the function directly; the same call from the browser after hydration is a real HTTP request.\n\n\u003C!-- quiz:start -->\n\n## 🧠 Test yourself\n\nThink it clicked? **[Take the 8-question quiz →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnuxt-weekly-nitro-server-routes\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\n## The endpoint that finally made sense\n\nThat `\u002Fapi\u002Fprofile` handler from the top of the article has an honest fix now: drop the `useState` call, read the user from `event.context` (set by an auth middleware upstream), and return plain data. Nothing about the fix is exotic — it's just respecting that the file it lives in was never part of the Vue app to begin with.\n\nNext time a server route throws `useState is not defined`, or a middleware runs in an order you didn't expect, you'll know exactly which of the two worlds you're standing in — and that's most of the debugging done before you've even opened the stack trace.\n\n\u003C!-- related:start -->\n\n## 📚 Read next\n\n- [Nuxt Hydration Mismatch: Why It Happens and How to Fix It](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnuxt-weekly-hydration-mismatch)\n- [useAsyncData Keys in Nuxt: Caching, Dedupe & the Sharing Bug](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnuxt-weekly-useasyncdata-keys-dedupe)\n- [Nuxt useState vs ref(): Why Server State Leaks Across Users](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnuxt-weekly-cross-request-state-leak)\n\n\u003C!-- related:end -->\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":47,"canonical":399,"description":49},"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnuxt-weekly-nitro-server-routes","01a0ec72-faae-767b-a924-6235bd2f75c5",{"name":402,"part":403,"total":403,"items":404},"Nuxt Deep Dive",4,[405,410,414,418],{"slug":406,"title":407,"publishedAt":408,"readingMinutes":409},"nuxt-weekly-cross-request-state-leak","Nuxt useState vs ref(): Why Server State Leaks Across Users","2026-08-30T11:37:11.335Z",14,{"slug":411,"title":412,"publishedAt":413,"readingMinutes":409},"nuxt-weekly-useasyncdata-keys-dedupe","useAsyncData Keys in Nuxt: Caching, Dedupe & the Sharing Bug","2026-09-13T11:26:28.014Z",{"slug":415,"title":416,"publishedAt":417,"readingMinutes":409},"nuxt-weekly-hydration-mismatch","Nuxt Hydration Mismatch: Why It Happens and How to Fix It","2026-09-20T16:26:05.191Z",{"slug":46,"title":47,"publishedAt":52,"readingMinutes":51},{"id":420,"locked":18},"01a0ec72-fb91-734d-9e97-66158102e2fe",[422],{"id":423,"slug":46,"title":424,"_count":425},"01a0ec72-fbbc-7637-af2d-e2b95bc8089b","Nuxt Server Routes & Nitro",{"questions":426},8,[428],{"locale":13,"slug":46},{"id":423,"slug":46,"title":424,"_count":430,"questionCount":426},{"questions":426},[432,436,438,442,444,448,452,455,458,462,466,469],{"slug":433,"name":434,"articles":435},"webdev","Webdev",114,{"slug":69,"name":70,"articles":437},96,{"slug":439,"name":440,"articles":441},"frontend","Frontend",75,{"slug":72,"name":73,"articles":443},41,{"slug":445,"name":446,"articles":447},"css","Css",36,{"slug":449,"name":450,"articles":451},"typescript","Typescript",17,{"slug":453,"name":454,"articles":409},"performance","Performance",{"slug":456,"name":457,"articles":51},"react","React",{"slug":459,"name":460,"articles":461},"browser","Browser",11,{"slug":463,"name":464,"articles":465},"node","Node",10,{"slug":467,"name":468,"articles":426},"html","Html",{"slug":470,"name":471,"articles":472},"accessibility","Accessibility",7,{"items":474,"meta":534},[475,487,504,519],{"id":45,"slug":46,"title":47,"subtitle":48,"excerpt":49,"coverUrl":50,"locale":13,"readingMinutes":51,"publishedAt":52,"viewCount":476,"likeCount":19,"commentCount":19,"author":477,"vertical":478,"topic":479,"tags":480,"_count":485,"playground":486,"hasQuiz":17,"hasPlayground":17},67,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[481,482,483,484],{"slug":61,"name":62,"color":48},{"slug":66,"name":67,"color":48},{"slug":69,"name":70,"color":48},{"slug":72,"name":73,"color":48},{"assessments":75},{"slug":46},{"id":488,"slug":415,"title":416,"subtitle":48,"excerpt":489,"coverUrl":490,"locale":13,"readingMinutes":409,"publishedAt":417,"viewCount":491,"likeCount":19,"commentCount":19,"author":492,"vertical":493,"topic":494,"tags":495,"_count":502,"playground":503,"hasQuiz":17,"hasPlayground":17},"01a0b07e-f66a-736f-a8f4-3c87da225d8f","A Nuxt hydration mismatch happens when the server's HTML disagrees with the client's first render. Learn why, and the fixes that actually work.","\u002Fmedia\u002Fcovers\u002Fnuxt-weekly-hydration-mismatch.png",250,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[496,497,500,501],{"slug":61,"name":62,"color":48},{"slug":498,"name":499,"color":48},"ssr","SSR",{"slug":69,"name":70,"color":48},{"slug":72,"name":73,"color":48},{"assessments":75},{"slug":415},{"id":505,"slug":411,"title":412,"subtitle":48,"excerpt":506,"coverUrl":507,"locale":13,"readingMinutes":409,"publishedAt":413,"viewCount":508,"likeCount":19,"commentCount":19,"author":509,"vertical":510,"topic":511,"tags":512,"_count":517,"playground":518,"hasQuiz":17,"hasPlayground":17},"01a08c59-464c-7039-acf3-b32a0b9ee7ce","Learn how Nuxt's useAsyncData and useFetch generate cache keys, how dedupe (cancel vs defer) really works, and why wrapper composables silently share data.","\u002Fmedia\u002Fcovers\u002Fnuxt-weekly-useasyncdata-keys-dedupe.png",284,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[513,514,515,516],{"slug":61,"name":62,"color":48},{"slug":498,"name":499,"color":48},{"slug":69,"name":70,"color":48},{"slug":72,"name":73,"color":48},{"assessments":75},{"slug":411},{"id":520,"slug":406,"title":407,"subtitle":48,"excerpt":521,"coverUrl":522,"locale":13,"readingMinutes":409,"publishedAt":408,"viewCount":523,"likeCount":19,"commentCount":19,"author":524,"vertical":525,"topic":526,"tags":527,"_count":532,"playground":533,"hasQuiz":17,"hasPlayground":17},"01a04446-3f08-701c-8cdc-e5ab5e87086e","A module-scope ref() in Nuxt is shared by every request that hits your server. Learn why useState isolates state per user, and how to fix the leak.","\u002Fmedia\u002Fcovers\u002Fnuxt-weekly-cross-request-state-leak.png",296,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[528,529,530,531],{"slug":61,"name":62,"color":48},{"slug":498,"name":499,"color":48},{"slug":69,"name":70,"color":48},{"slug":72,"name":73,"color":48},{"assessments":75},{"slug":406},{"page":75,"perPage":472,"total":403,"totalPages":75},"\u003Cdiv class=\"shj shj-lang-ts shj-multiline\" data-lang=\"ts\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-cmnt\">\u002F\u002F server\u002Fapi\u002Fprofile.get.ts — looks reasonable, isn't\n\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">export\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">default\u003C\u002Fspan> \u003Cspan class=\"shj-func\">defineEventHandler\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>event\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> user \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-func\">useState\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">'user'\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-cmnt\">\u002F\u002F ❌ throws at runtime\n\u003C\u002Fspan>  \u003Cspan class=\"shj-kwd\">return\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> user\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> user\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>value \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-ts shj-multiline\" data-lang=\"ts\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003Cdiv>8\u003C\u002Fdiv>\u003Cdiv>9\u003C\u002Fdiv>\u003Cdiv>10\u003C\u002Fdiv>\u003Cdiv>11\u003C\u002Fdiv>\u003Cdiv>12\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-cmnt\">\u002F\u002F server\u002Fapi\u002Fusers\u002F[id].get.ts\n\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">export\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">default\u003C\u002Fspan> \u003Cspan class=\"shj-func\">defineEventHandler\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">async\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>event\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> id \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-func\">getRouterParam\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>event\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-str\">'id'\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> includeOrders \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-func\">getQuery\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>event\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-cmnt\">\u002F\u002F ?includeOrders=true\n\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> user \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">await\u003C\u002Fspan> \u003Cspan class=\"shj-func\">findUser\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>id\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">if\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">!\u003C\u002Fspan>user\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n    \u003Cspan class=\"shj-kwd\">throw\u003C\u002Fspan> \u003Cspan class=\"shj-func\">createError\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> status\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-num\">404\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> statusText\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">'User not found'\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\n  \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n\n  \u003Cspan class=\"shj-kwd\">return\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> user\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> includeOrders\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> includeOrders \u003Cspan class=\"shj-oper\">===\u003C\u002Fspan> \u003Cspan class=\"shj-str\">'true'\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-ts shj-multiline\" data-lang=\"ts\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-cmnt\">\u002F\u002F server\u002Fmiddleware\u002Fauth.ts\n\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">export\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">default\u003C\u002Fspan> \u003Cspan class=\"shj-func\">defineEventHandler\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>event\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> token \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-func\">getHeader\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>event\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-str\">'authorization'\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\n  event\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>context\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>user \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> token \u003Cspan class=\"shj-oper\">?\u003C\u002Fspan> \u003Cspan class=\"shj-func\">verifyToken\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>token\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-num\">null\u003C\u002Fspan>\n  \u003Cspan class=\"shj-cmnt\">\u002F\u002F no return — request continues to the matched route\n\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>",{"locked":18,"total":19,"comments":539},[]]