Everything you need to know when building new features or making changes to code that has been migrated from Nuxt 3 to Nuxt 4.
Change 01 — Most Important
app/ DirectoryThis is the biggest change in the whole migration. In Nuxt 4, all frontend code lives inside a folder called app/. This did not exist in Nuxt 3.
Here is how the folder structure changed:
apps/web/app/. Server code (API routes, server utilities) stays in apps/web/server/.Change 02
~ and ~~Nuxt has shortcuts for import paths. These shortcuts changed in Nuxt 4 because of the new app/ folder.
| Alias | Points To | Use It For |
|---|---|---|
~ (single tilde) | apps/web/app/ | Components, composables, pages, assets, utils — anything inside the app/ folder |
~~ (double tilde) | apps/web/ | Config files, server utilities, i18n files — anything outside the app/ folder |
import MyHelper from '~/utils/myHelper'; import { config } from '~/config/tile-mountain';
import MyHelper from '~/utils/myHelper'; // app/utils ✓ import { config } from '~~/config/tile-mountain'; // outside app/ ✓
~ to import something from the config folder or the server folder, Nuxt will look inside app/ and not find it. Use ~~ for those.Change 03
deep: trueIn Nuxt 4, useAsyncData and useLazyAsyncData store data in a "shallow" ref by default. This means Vue will not automatically notice when the data inside changes. To fix this, you must add { deep: true }.
const { data } = await useAsyncData( 'my-data-key', () => useSdk().magento.getProduct() );
const { data } = await useAsyncData( 'my-data-key', () => useSdk().magento.getProduct(), { deep: true } // ← ALWAYS add this );
{ deep: true } to both useAsyncData() and useLazyAsyncData() any time you use them.Change 04
useSdk() Replaces useNuxtApp().$sdkThe old way to call Magento APIs used useNuxtApp().$sdk. In Nuxt 4, there is a cleaner composable called useSdk() that does the same job with less code.
const { $sdk } = useNuxtApp(); const { data } = await useAsyncData( 'key', () => $sdk.magento.getProduct() );
// No need to import — auto-available const { data } = await useAsyncData( 'key', () => useSdk().magento.getProduct(), { deep: true } );
useSdk at the top of your file. Nuxt 4 makes it available automatically, just like ref() and computed().Change 05
import.meta Replaces processNuxt 3 used process.client and process.server to check where code is running. Nuxt 4 uses the modern standard: import.meta.client and import.meta.server.
| Old — Nuxt 3 | New — Nuxt 4 | Meaning |
|---|---|---|
process.client | import.meta.client | Code runs in the browser |
process.server | import.meta.server | Code runs on the server |
if (process.client) { window.addEventListener('scroll', handler); } if (process.server) { // server-only code }
if (import.meta.client) { window.addEventListener('scroll', handler); } if (import.meta.server) { // server-only code }
Change 06
Files inside the server/ folder are separate from the app/ folder. If a server route needs to import a utility from server/utils/, it cannot use the ~ alias (which now points to app/).
import { adminAuth } from '~/server/utils/adminAuth' — In Nuxt 4, ~ points to app/, so this path becomes app/server/utils/adminAuth, which does not exist.You have two correct options for server route imports:
import { validateAdmin } from '../../../utils/adminAuth'; // Count the dots to reach server/utils/
import { validateAdmin } from '~~/server/utils/adminAuth'; // ~~ = apps/web/ (the root directory)
~~ if the path would have many ../../../ levels and is hard to read.Change 07
Ref<T | undefined>In Nuxt 3, useAsyncData returned data typed as Ref<T | null>. In Nuxt 4, the return type changed to Ref<T | undefined>. This can cause TypeScript errors in functions that receive this data as a parameter.
function setupWatcher( data: Ref<ProductData | null> ) { // ... }
function setupWatcher( data: Ref<ProductData | null | undefined> ) { // Add | undefined to fix TS error }
Ref<T | undefined> is not assignable to parameter of type Ref<T | null>", simply add | undefined to the parameter type in the function definition.| undefined, make sure the function code handles the case where data is undefined. Use optional chaining (data.value?.field) to be safe.Change 08
consola — Named Import Onlyconsola is the logging library used in this project. In Nuxt 4, you must import it using a named import. The default import no longer works.
import consola from 'consola'; consola.log('Hello world'); consola.error('Something failed');
import { consola } from 'consola'; // Note the curly braces { } consola.log('Hello world'); consola.error('Something failed');
Change 09
apiPath() — No Template Literals InsideThe apiPath() function accepts only fixed string values. You cannot use a variable or a template literal inside the function call itself. If you need to add a query string, build the full URL outside the function.
// ❌ Template literal inside apiPath() const url = apiPath( `fetchProductData${queryString}` );
// ✓ Build the URL outside apiPath() const url = `${apiPath('fetchProductData')}${queryString}`;
apiPath(). Then, if you need to add anything to the URL, combine it outside using a template literal.Change 10
All components that were in the Trepanel/ folder have been renamed to TileMountainAE/. The brand "Trepanel" is being retired and its products now appear on the Tile Mountain UAE website.
TileMountainAE/ instead. The old Trepanel/ folder has been removed.| Deleted File | Replacement |
|---|---|
apps/web/app/config/trepanel.ts | Use tile-mountain-ae.ts |
apps/web/app/assets/trepanel/style.scss | Use TileMountainAE assets |
TrepanelReviews component | Removed — no replacement needed |
Change 11
When you create a new file, this table will help you decide exactly where it should go.
| File Type | Correct Location in Nuxt 4 |
|---|---|
| New Vue component | apps/web/app/components/YourComponent/ |
| New composable (useXxx) | apps/web/app/composables/useYourThing/ |
| New page | apps/web/app/pages/your-page.vue |
| New layout | apps/web/app/layouts/your-layout.vue |
| New plugin | apps/web/app/plugins/your-plugin.ts |
| New utility function | apps/web/app/utils/yourUtil.ts |
| New middleware | apps/web/app/middleware/yourMiddleware.ts |
| New SCSS styles | apps/web/app/assets/{site}/ |
| New server API route | apps/web/server/routes/your-route.ts |
| New server utility | apps/web/server/utils/yourUtil.ts |
| New site config | apps/web/config/your-site.ts |
| New i18n translations | apps/web/i18n/lang/en.json |
app/. "Does it run on the server?" → Put it inside server/. "Is it shared config?" → Put it at apps/web/ root level.Change 12
Use this checklist every time you write or change code. Go through each item before you push your changes.
apps/web/app/?All frontend code (components, composables, pages, assets, plugins, utils) must be inside the app/ folder.~ for things inside app/ and ~~ for things outside app/ (config, server).{ deep: true } to useAsyncData and useLazyAsyncData?Without this, reactive updates to fetched data may not work correctly in Nuxt 4.useSdk() instead of useNuxtApp().$sdk?The new composable is cleaner and auto-imported.process.client / process.server?Use import.meta.client and import.meta.server instead.~~ — never ~ for server utilities.useAsyncData, does it accept | undefined?Update the TypeScript type to Ref<YourType | null | undefined> to avoid TS errors.consola as a named import?import { consola } from 'consola' — with the curly braces.app/components/TileMountainAE/.yarn lint before pushing?This checks TypeScript types too. Fix all errors before pushing your branch.Change 13
Keep this page open as a handy reference while you code.
Components, composables, pages, plugins, assets, utils, middleware, layouts
apps/web/app/API routes, server utilities, server middleware
apps/web/server/Use single tilde — resolves to apps/web/app/
~/components/MyCompUse double tilde — resolves to apps/web/
~~/config/tile-mountainAlways add deep: true option
useAsyncData('key', fn, { deep: true })Use the new composable, auto-imported
useSdk().magento.someQuery()Replaces process.client
import.meta.clientReplaces process.server
import.meta.serverNamed import with curly braces
import { consola } from 'consola'useAsyncData now returns undefined not null
Ref<MyType | null | undefined>Trepanel folder has been renamed
app/components/TileMountainAE/Checks types and lint rules
yarn lintComplete Rule Summary Table
| Topic | Nuxt 3 (Old) | Nuxt 4 (New) |
|---|---|---|
| Frontend code location | apps/web/components/ | apps/web/app/components/ |
| Path alias ~ points to | apps/web/ | apps/web/app/ |
| Path alias ~~ points to | apps/web/ | apps/web/ (same) |
| Async data reactivity | Works without options | Requires { deep: true } |
| Magento SDK | useNuxtApp().$sdk | useSdk() |
| Browser detection | process.client | import.meta.client |
| Server detection | process.server | import.meta.server |
| consola import | import consola from ... | import { consola } from ... |
| useAsyncData return type | Ref<T | null> | Ref<T | undefined> |
| UAE components folder | components/Trepanel/ | app/components/TileMountainAE/ |
| apiPath() usage | apiPath(`name${query}`) | `${apiPath('name')}${query}` |
| Run before pushing | — | yarn lint |