Developer Guide

Nuxt 4 Migration
Developer Guide

Everything you need to know when building new features or making changes to code that has been migrated from Nuxt 3 to Nuxt 4.

ProjectTile Mountain SDK
Branchrelease/release_1.94 (Nuxt 4)
Nuxt Version^4.3.1
DateJune 2026

Contents

Change 01 — Most Important

The New app/ Directory

This 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.

⚠️
Read This FirstIf you put a new file in the wrong place, it will not be found by Nuxt. Always check where you are saving your file.

Here is how the folder structure changed:

Before — Nuxt 3
  • apps/web/
    • components/
    • composables/
    • pages/
    • layouts/
    • plugins/
    • middleware/
    • assets/
    • utils/
    • server/
After — Nuxt 4
  • apps/web/
    • app/NEW
      • components/
      • composables/
      • pages/
      • layouts/
      • plugins/
      • middleware/
      • assets/
      • utils/
    • server/← stays at root level
Simple Rule to RememberEverything that runs in the browser goes inside apps/web/app/. Server code (API routes, server utilities) stays in apps/web/server/.

Change 02

Path Aliases — ~ and ~~

Nuxt has shortcuts for import paths. These shortcuts changed in Nuxt 4 because of the new app/ folder.

AliasPoints ToUse 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
Before — Nuxt 3
import MyHelper from '~/utils/myHelper';
import { config } from '~/config/tile-mountain';
After — Nuxt 4
import MyHelper from '~/utils/myHelper';       // app/utils ✓
import { config } from '~~/config/tile-mountain'; // outside app/ ✓
🚫
Common MistakeIf you use ~ 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

useAsyncData — Always Add deep: true

In 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 }.

ℹ️
What does "shallow ref" mean?Think of it like a box. A shallow ref only watches if you replace the whole box. A deep ref also watches if you change anything inside the box. For complex data like product lists or cart items, you need deep watching.
Before — Nuxt 3
const { data } = await useAsyncData(
  'my-data-key',
  () => useSdk().magento.getProduct()
);
After — Nuxt 4
const { data } = await useAsyncData(
  'my-data-key',
  () => useSdk().magento.getProduct(),
  { deep: true } // ← ALWAYS add this
);
⚠️
This applies to both functionsAdd { deep: true } to both useAsyncData() and useLazyAsyncData() any time you use them.

Change 04

useSdk() Replaces useNuxtApp().$sdk

The 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.

Before — Nuxt 3
const { $sdk } = useNuxtApp();

const { data } = await useAsyncData(
  'key',
  () => $sdk.magento.getProduct()
);
After — Nuxt 4
// No need to import — auto-available

const { data } = await useAsyncData(
  'key',
  () => useSdk().magento.getProduct(),
  { deep: true }
);
useSdk() is auto-importedYou do not need to import useSdk at the top of your file. Nuxt 4 makes it available automatically, just like ref() and computed().

Change 05

import.meta Replaces process

Nuxt 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 3New — Nuxt 4Meaning
process.clientimport.meta.clientCode runs in the browser
process.serverimport.meta.serverCode runs on the server
Before — Nuxt 3
if (process.client) {
  window.addEventListener('scroll', handler);
}
if (process.server) {
  // server-only code
}
After — Nuxt 4
if (import.meta.client) {
  window.addEventListener('scroll', handler);
}
if (import.meta.server) {
  // server-only code
}

Change 06

Server Route Imports

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/).

🚫
This Will Breakimport { 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:

Option A — Relative Path
import { validateAdmin } from
  '../../../utils/adminAuth';
// Count the dots to reach server/utils/
Option B — Double Tilde
import { validateAdmin } from
  '~~/server/utils/adminAuth';
// ~~ = apps/web/ (the root directory)
ℹ️
Which option to choose?Both work. Use relative paths if you prefer simplicity. Use ~~ if the path would have many ../../../ levels and is hard to read.

Change 07

TypeScript: 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.

Before — Nuxt 3 Type
function setupWatcher(
  data: Ref<ProductData | null>
) { // ... }
After — Nuxt 4 Type
function setupWatcher(
  data: Ref<ProductData | null | undefined>
) { // Add | undefined to fix TS error }
⚠️
Symptom: TypeScript ErrorIf you see "Argument of type Ref<T | undefined> is not assignable to parameter of type Ref<T | null>", simply add | undefined to the parameter type in the function definition.
ℹ️
Check your function bodyAfter adding | 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 Only

consola 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.

Before — Nuxt 3
import consola from 'consola';

consola.log('Hello world');
consola.error('Something failed');
After — Nuxt 4
import { consola } from 'consola';
// Note the curly braces { }
consola.log('Hello world');
consola.error('Something failed');

Change 09

apiPath() — No Template Literals Inside

The 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.

Wrong — TypeScript Error
// ❌ Template literal inside apiPath()
const url = apiPath(
  `fetchProductData${queryString}`
);
Correct — Nuxt 4
// ✓ Build the URL outside apiPath()
const url =
  `${apiPath('fetchProductData')}${queryString}`;
ℹ️
Simple RuleAlways pass a plain, fixed string to apiPath(). Then, if you need to add anything to the URL, combine it outside using a template literal.

Change 10

Trepanel → TileMountainAE Component Rename

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.

⚠️
Important for UAE / Trepanel WorkIf you are asked to work on any Trepanel-related feature, look for the files under TileMountainAE/ instead. The old Trepanel/ folder has been removed.
Old Path — Does Not Exist
  • app/components/Trepanel/
    • Home/
    • Checkout/
    • Samples/
    • Successpage/
New Path — Use This
  • app/components/TileMountainAE/
    • Home/
    • Checkout/
    • Samples/
    • Successpage/
Deleted FileReplacement
apps/web/app/config/trepanel.tsUse tile-mountain-ae.ts
apps/web/app/assets/trepanel/style.scssUse TileMountainAE assets
TrepanelReviews componentRemoved — no replacement needed

Change 11

Where to Put New Files

When you create a new file, this table will help you decide exactly where it should go.

File TypeCorrect Location in Nuxt 4
New Vue componentapps/web/app/components/YourComponent/
New composable (useXxx)apps/web/app/composables/useYourThing/
New pageapps/web/app/pages/your-page.vue
New layoutapps/web/app/layouts/your-layout.vue
New pluginapps/web/app/plugins/your-plugin.ts
New utility functionapps/web/app/utils/yourUtil.ts
New middlewareapps/web/app/middleware/yourMiddleware.ts
New SCSS stylesapps/web/app/assets/{site}/
New server API routeapps/web/server/routes/your-route.ts
New server utilityapps/web/server/utils/yourUtil.ts
New site configapps/web/config/your-site.ts
New i18n translationsapps/web/i18n/lang/en.json
Quick Memory TrickAsk yourself: "Does this code run in the browser?" → Put it inside app/. "Does it run on the server?" → Put it inside server/. "Is it shared config?" → Put it at apps/web/ root level.

Change 12

Developer Checklist

Use this checklist every time you write or change code. Go through each item before you push your changes.

Change 13

Quick Reference

Keep this page open as a handy reference while you code.

Frontend Files Go Here

Components, composables, pages, plugins, assets, utils, middleware, layouts

apps/web/app/

Server Files Go Here

API routes, server utilities, server middleware

apps/web/server/

Path Alias for app/

Use single tilde — resolves to apps/web/app/

~/components/MyComp

Path Alias for root

Use double tilde — resolves to apps/web/

~~/config/tile-mountain

Async Data

Always add deep: true option

useAsyncData('key', fn, { deep: true })

Magento API Calls

Use the new composable, auto-imported

useSdk().magento.someQuery()

Check for Browser

Replaces process.client

import.meta.client

Check for Server

Replaces process.server

import.meta.server

Logging

Named import with curly braces

import { consola } from 'consola'

TypeScript Data Type

useAsyncData now returns undefined not null

Ref<MyType | null | undefined>

UAE Components

Trepanel folder has been renamed

app/components/TileMountainAE/

Run Before Push

Checks types and lint rules

yarn lint

Complete Rule Summary Table

TopicNuxt 3 (Old)Nuxt 4 (New)
Frontend code locationapps/web/components/apps/web/app/components/
Path alias ~ points toapps/web/apps/web/app/
Path alias ~~ points toapps/web/apps/web/ (same)
Async data reactivityWorks without optionsRequires { deep: true }
Magento SDKuseNuxtApp().$sdkuseSdk()
Browser detectionprocess.clientimport.meta.client
Server detectionprocess.serverimport.meta.server
consola importimport consola from ...import { consola } from ...
useAsyncData return typeRef<T | null>Ref<T | undefined>
UAE components foldercomponents/Trepanel/app/components/TileMountainAE/
apiPath() usageapiPath(`name${query}`)`${apiPath('name')}${query}`
Run before pushingyarn lint