Nuxt 4.6 nuxt/server: Why Mixing h3 Imports Throws NUXT_E8012
Nuxt 4.6 added a new nuxt/server import source alongside h3 — mix the two in one handler and you hit NUXT_E8012. The real boundary, plus a cheat sheet.

A server/api/profile.get.ts handler that worked fine on Monday starts returning a NUXT_E8012 error on Tuesday, and the only thing that changed is a teammate added one line — reading the signed-in user's session with a helper imported from nuxt/server. Nothing else moved — not the route, not the defineEventHandler at the top of the file, not the dependency versions in any way you'd notice without reading package.json closely. The error message is honest about what's wrong, but only if you already know server/ now has two event shapes floating around, and that this one new line just handed a helper the wrong one.
This article is written against Nuxt 4.6, verified at v4.6.2 (the current latest on npm as of this writing; nuxt/server itself shipped in 4.6.0, released October 5, 2026, per the official GitHub release notes). If you've read the server routes episode in this series, you already know how server/api and server/middleware route a request through Nitro — this one picks up what changed on top of that in 4.6: a second, explicit way to write the exact same kind of file, and the sharp edge where the two meet.
By the end of this article you'll be able to:
- Explain what
nuxt/serveris, why Nuxt 4.6 added it, and what it means for Nuxt to be "server-agnostic" - Tell apart the two coexisting ways to write server code in Nuxt 4.6 — the default h3 auto-imports and the new explicit
nuxt/serverimports - Read a
NUXT_E8012error and fix the file it's pointing at - Spot the subtler bug: helpers with the same name that behave differently depending on which source they came from
- Decide whether to migrate existing
server/code now, or leave it exactly as it is
You've written server/api routes before — defineEventHandler, getQuery, readBody are already familiar. You don't need to have touched Nitro internals or h3 directly; this article treats the new nuxt/server import source as its own subject, not an h3 tutorial.
- The problem: one new import, one confusing error
- The mental model: two server APIs, one directory
- Walking through it, stage by stage
- Edge cases and gotchas
- Best practices
- FAQ
- Cheat sheet
- Key takeaways
Here's a server/api handler that looks like completely ordinary Nuxt 4 code, with one line added:
defineEventHandler and getRouterParam in this file are untouched — still the default auto-imports, exactly as they were last week. The only new thing is one call to useSession, imported explicitly from nuxt/server, to read the signed-in user's id out of the session. Run the dev server and hit the route, and the response comes back a 500:
The instinct is to assume a typo, a bad auto-import, or a stale .nuxt cache — none of which is true. getRouterParam and the rest of the file's identifiers were never touched, and this isn't a build failure either — the file compiles and the dev server starts just fine. The error only shows up once the route is actually hit, because it's thrown from inside useSession itself, the moment it notices the event it was handed doesn't look like the one nuxt/server expects. That's the whole bug: a handler still built with h3's own defineEventHandler handed one of nuxt/server's session helpers an event it can't work with — and it's also, it turns out, a deliberate guardrail rather than a bug at all.
Before 4.6, server/ had exactly one API: a set of h3 and Nitro helpers (defineEventHandler, getQuery, readBody, getRouterParam, createError, and so on) that Nuxt auto-imports into every file under server/. That hasn't gone away — the release notes are explicit that the auto-imported helpers "are still h3's own helpers," unchanged, and nothing in the 4.6 release requires you to touch them.
What 4.6 adds is nuxt/server — a second, explicit import source, described as "a complement to nuxt/app." Most of what it exports — getQuery, readBody, getRouterParam, and friends — are the exact same underlying h3/Nitro functions, re-exported under a new door, so importing them explicitly from nuxt/server instead of relying on the auto-import changes nothing about how they behave. A few exports are genuinely different, though: nuxt/server ships its own defineEventHandler (the one that builds the portable RequestEvent, with web-standard shape like event.req and event.res.headers), its own createError and sendRedirect with different contracts (more on those below), and its own session API — useSession, getSession, updateSession, clearSession — built specifically to read and write session data off that portable event.
That session API is the clearest place NUXT_E8012 shows up. Each of those four helpers checks the event it's handed: if it looks like an h3 v1 event (no .req, just the legacy node request/response pair) rather than nuxt/server's own event, it throws NUXT_E8012, naming the helper and telling you to import defineEventHandler from nuxt/server too. That only happens if the handler around it is still built with the auto-imported defineEventHandler — the one that still produces an h3 v1 event. Reach for useSession (or one of its three siblings) inside a handler like that, and the event shape it's handed doesn't match what the helper needs. The session helpers aren't the only ones that check — the release notes describe the error more broadly, as something you'll hit from mixing the two import sources in general, so treat the session API as the headline example, not an exhaustive list.
The reason nuxt/server exists at all is the headline of the release: Nuxt is moving toward being server-agnostic — not tied to Nitro as the only possible engine underneath server/. A handler built end-to-end on nuxt/server's portable event is meant to run unchanged whether the engine underneath it is Nitro, or the new experimental @nuxt/vite-server, or — per the release notes — Nitro v3 and h3 v2 once Nuxt 5 ships. Nitro itself isn't going anywhere; the same notes call it "still the right choice for almost everyone," including for 4.6.
Key concept: in this episode's example, NUXT_E8012 comes from nuxt/server's session helpers refusing to work with an event they can't read, because the handler around them never opted into the portable event that makes them work — getQuery, readBody, and getRouterParam don't care which defineEventHandler built the event, so pairing those with the auto-import doesn't trip it. The release notes frame the error more generally, as something mixing the two import sources can produce, so don't assume every other helper is exempt just because these three are.
If you've never heard of nuxt/server, your existing server/api, server/routes, and server/middleware files are unaffected by 4.6. The auto-imported defineEventHandler, getQuery, readBody, getRouterParam, and createError are exactly the h3 helpers they always were. Upgrading to 4.6 changes nothing here — this is the whole point of the release notes' line that "nothing in this release requires a migration."
To build a handler around the portable event — and unlock nuxt/server's own defineEventHandler, createError, sendRedirect, and session helpers — you import explicitly, by name, from nuxt/server:
Because defineEventHandler here is nuxt/server's own version, the event it builds has .req on it — exactly the shape useSession checks for. getQuery behaves identically either way (it's the same h3 helper regardless of source), but the handler itself is opted all the way in, which is what the session API actually needs.
This is the pairing from the mental model above, put to use: switch the handler's defineEventHandler to the nuxt/server import too, and the session helper gets the portable event it's checking for.
Notice what doesn't need to change to clear the error: getRouterParam was never what threw NUXT_E8012, whether it came from the auto-import or from nuxt/server — it doesn't check which defineEventHandler built the event the way the session helpers do. The one identifier that has to move to fix this error is defineEventHandler itself, because that's what decides which event shape every helper in the handler gets handed. getRouterParam itself behaves the same from either source, for what it's worth — see the next section for the helpers that actually do differ.
A couple of helpers behave differently depending on which source they came from, and none of these trip NUXT_E8012 either — that error is specific to the session API, not to these. nuxt/server's sendRedirect still sets the response status and Location header as a side effect, exactly like the auto-imported one — but instead of ending the response there, it also returns an HTML <meta http-equiv="refresh"> body as a fallback for a client that ignores the status code (inspecting the installed nuxt@4.6.2 package directly: dist/server/index.js's sendRedirect calls setResponseStatus(event, status) and event.res.headers.set('location', location) before returning that HTML string). So the redirect itself — the part every browser and fetch call actually relies on — works whether or not the handler returns that value; skipping the return only drops the fallback body, which matters solely to a user agent that ignores both the status and the header. Its createError takes status and statusText, where the h3-helper createError you already know takes statusCode and statusMessage — though nuxt/server's createError still accepts statusCode/statusMessage too, as deprecated aliases, so that direction keeps working. And response headers go through event.res.headers, a web-standard Headers-shaped object, rather than h3 v1's own header-setting helpers.
Neither example throws NUXT_E8012 — that error only ever comes from the session helpers, never from sendRedirect. The real gotcha here is narrower than it looks: dropping the return doesn't break the redirect, it just means a browser gets an empty body alongside the 302 instead of the fallback HTML. Worth knowing, but not worth losing an afternoon over — the loud NUXT_E8012 error from the session helpers is still the one that actually blocks a request.
Nothing forces you to touch nuxt/server today. The case for doing it anyway, per the release notes, is forward-looking: Nuxt 5 will move to Nitro v3 and h3 v2, and server code written against nuxt/server on 4.6 "runs unchanged there." Code left on the auto-imported h3 helpers isn't broken by this — it keeps working exactly as it did on Nuxt 4.5 — but it's the code that will need attention whenever that migration comes. If you're starting a new server/ file today and have no reason to care about portability, the auto-imports remain the simpler, fully-supported default. If you're building something you expect to still be maintaining past the next major, nuxt/server is the version of "write it once" that this release is actually offering.
It's not a per-project switch — it's per call. You can have some server/ files on the auto-imports and others fully on nuxt/server in the same project; most helpers don't care which source built the event they're handed. Only a session-helper call cares, and only at the moment it runs — importing useSession and never calling it is harmless, since the check happens inside the helper itself, not from a static scan of the file's imports.
@nuxt/vite-server is explicitly experimental. The release notes introduce it alongside nuxt/server as a second, pure-Vite server builder, but they're equally explicit that "Nitro is still the right choice for almost everyone" right now. Treat it as a preview of where server-agnostic Nuxt is headed, not a production alternative to reach for in 4.6.
Server-side useState/useRoute still don't belong in either server API. Nothing about nuxt/server changes the boundary the server routes episode in this series covers — both the h3-auto-import world and the nuxt/server world are still the Nitro/h3 side of the project, with no Vue application instance for a composable to attach to.
- If a handler calls
useSession,getSession,updateSession, orclearSession, make sure that same handler'sdefineEventHandleris imported fromnuxt/servertoo. That pairing is the one thingNUXT_E8012actually enforces — get it right and the rest of the file's imports (auto-imported or explicit) don't matter to this particular error. - Don't migrate existing files just because 4.6 shipped. The release notes are explicit that nothing requires it; migrate a file when you have an actual reason (adopting the session API, building for a new server builder, preparing early for Nuxt 5), not as busywork.
- When you do reach for
nuxt/server's own helpers, re-check the contract — not justsendRedirectandcreateError, but anything that used to write directly to the response. The upgrade guide's comparison table is the source of truth, not assumption-by-familiar-name. - If a
NUXT_E8012shows up in a file you didn't expect, look for a call touseSession/getSession/updateSession/clearSessionand check what built the event around it — it's almost always adefineEventHandlerthat's still the h3 auto-import.
No. The 4.6 release notes say directly that nothing in the release requires a migration — files using only the auto-imported h3 helpers keep working exactly as they did on 4.5.
Calling one of nuxt/server's session helpers — useSession, getSession, updateSession, or clearSession — inside a handler whose defineEventHandler is still the default auto-import. The helper gets handed an h3 v1 event instead of the portable one it needs, and throws at request time, naming itself in the error. It's not a build-time check and it's not about any other helper pairing — getQuery, readBody, and getRouterParam behave identically no matter which source you import them from.
No. Nitro remains the default engine underneath both import styles in 4.6, and the release notes call it "still the right choice for almost everyone." nuxt/server is a portable way to write handlers that happen to run on Nitro today and could run on a different server builder later — it isn't a replacement for Nitro itself.
Mostly, but not entirely. getRouterParam/getRouterParams behave identically either way — both default to the raw, undecoded route segment, and both accept { decode: true } to decode it. createError does differ: nuxt/server's version takes status/statusText (it still accepts statusCode/statusMessage too, as deprecated aliases), while the auto-imported h3 helper only recognizes statusCode/statusMessage. sendRedirect sets the status and Location header as a side effect either way, then additionally returns a <meta refresh> fallback body from the nuxt/server version — skip the return and the redirect still happens, you just lose that fallback body. None of this is NUXT_E8012, which comes only from the session API checking the event shape it's handed.
Not yet, for anything you're shipping. The release notes introduce it as experimental, in the same breath as recommending Nitro for almost everyone. It's worth knowing it exists as a sign of where Nuxt's server layer is heading, not as a current production choice.
| Situation | What to do |
|---|---|
New server/ file, no portability need | Leave it on the default auto-imports — no import needed |
| Want a handler portable beyond Nitro | Import everything server-related in that file from nuxt/server |
Calling getQuery/readBody from either source | Behaves identically — they're the same h3 helper either way |
Calling getRouterParam/getRouterParams from either source | Behaves identically — both default to the raw, undecoded segment; pass { decode: true } on either to decode it |
Calling useSession/getSession/updateSession/clearSession | Only works if that handler's defineEventHandler is also from nuxt/server |
NUXT_E8012 in a file you didn't expect | Find the session-helper call and check whether its handler still uses the auto-imported defineEventHandler |
Redirecting with the auto-imported sendRedirect | Just call it — it ends the response immediately as a side effect |
Redirecting with nuxt/server's sendRedirect | Sets status + Location immediately too; return sendRedirect(event, path) additionally sends a fallback body for clients that ignore the status |
Throwing with the auto-imported createError | { statusCode, statusMessage } |
Throwing with nuxt/server's createError | { status, statusText } |
| Deciding whether to migrate at all | Only if you have a real reason — nothing requires it on 4.6 |
Trying @nuxt/vite-server | Experimental — fine to explore, not yet for production |
Runs right in your browser — poke at it and watch the concept react live.
- Nuxt 4.6 added
nuxt/server, a new explicit import source for server code, alongside the default auto-imported h3 helpers thatserver/has always had — most of what it exports is the same h3/Nitro functions under a new door, not a parallel implementation. NUXT_E8012is narrower than "don't mix imports": it fires specifically whennuxt/server's session helpers (useSession,getSession,updateSession,clearSession) are called inside a handler whosedefineEventHandleris still the auto-import — the fix is pairing that one import withnuxt/servertoo.createErrorshares a name across both sources but not a contract (status/statusTextvs.statusCode/statusMessage), andsendRedirectsets its status andLocationheader the same way in both but only returns a fallback body fromnuxt/server— skipping thatreturnloses the fallback body, not the redirect.getRouterParam/getRouterParamsbehave identically from either source. None of these throwNUXT_E8012.- Migrating existing code to
nuxt/serveris optional today, and mainly worth it if you want a handler to keep working unchanged once Nuxt 5 moves to Nitro v3 and h3 v2.
Think it clicked? Take the 8-question quiz →
Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.
That NUXT_E8012 from the top of the article wasn't a cache problem or a typo — it was nuxt/server's session API refusing to read a session off an event it doesn't recognize. Once you know the error belongs specifically to the session helpers, not to every helper in the file, it stops looking like a mystery and starts looking like a guardrail with a narrow, specific job — and the subtler case, the same-named helper that behaves differently with no error at all, is the one worth remembering long after a request stops failing.
- Nuxt Server Routes Explained: How Nitro Builds Your API
- Nuxt Hydration Mismatch: Why It Happens and How to Fix It
- Nuxt 4.5 SSR Streaming: The Route Rules That Disable It
🚀 Want more like this? Every guide, playground, and quiz lives on bestpractic.org — open it and sign up free so the next one finds you.
Thanks for reading! Let's stay connected:
- ⭐ GitHub — follow me and star the projects: github.com/parsajiravand
- 💬 Discord — join the frontend best-practices community: discord.gg/d9KRhuAwQ
- 📸 Instagram — frontend best practices, daily: @bestpractice___
Keep reading
One post a day, in your inbox
Each one with a runnable playground and a quiz. No pitch, no digest, unsubscribe in one click.
0 comments
Sign in to join the discussion, like comments, and save articles for later.