Nuxt 4 moved the Vue application into app/ (the srcDir), leaving server/, shared/, public/, layers/ and modules/ at the root. Each app/ directory page in the docs is short and full of checkable rules: how names are derived, what is scanned, in what order things run.
assets/ — files processed by the bundler (hashed, optimised); referenced as ~/assets/.... Unprocessed files belong in public/.components/ — scanned recursively; the name is the path prefix plus the file name (components/base/foo/Button.vue → <BaseFooButton>), with duplicated segments collapsed (components/Base/BaseButton.vue is still <BaseButton>); components.pathPrefix: false turns prefixing off. Dynamic components by name need resolveComponent('Name'). Lazy prefix makes an async import; hydration strategies on lazy components (hydrate-on-visible, -idle, -interaction, -media-query, hydrate-after, hydrate-when, hydrate-never) with a @hydrated event to listen for; direct imports from #components; custom directories via components.dirs with prefix/global; npm components via addComponentsDir in a module; .client.vue components render only on the client (a .client component paired with a same-name .server component works as a pair); .server.vue components are server components/islands; components/global/ registers globally (avoid; hurts splitting); extensions limit which file types count.composables/ — auto-imported; only top-level files and index.ts files are scanned (composables/useFoo.ts, composables/nested/index.ts); enable deeper scanning with imports.dirs: ['composables/**']. Named exports keep their name; a default export is imported under the camelCased file name.layouts/ — default.vue is applied by <NuxtLayout>; a page picks another with definePageMeta({ layout: 'custom' }), at runtime with setPageLayout(), or <NuxtLayout :name> in app.vue; layout: false disables it. Layout names are kebab-cased from the file name (customLayout.vue → custom-layout). Layouts need a <slot /> and, if you use transitions, a single root element.middleware/ — route middleware: anonymous (inline in definePageMeta), named (auth.ts, referenced by name), global (*.global.ts, runs on every route). Order: global middleware alphabetically (prefix with numbers), then page middleware in array order. Return nothing to continue, navigateTo() to redirect, abortNavigation() to stop. Runs on the server for the first request and on the client for navigations; addRouteMiddleware registers more from a plugin.pages/ — file-based routes: index.vue, dynamic [id].vue, optional [[id]].vue, catch-all [...slug].vue, nested parent.vue + parent/, route groups (group)/, client-only pages with .client.vue. Metadata with definePageMeta (layout, middleware, name, path, alias, redirect, validate, key, keepalive, pageTransition, layoutTransition, viewTransition, scrollToTop, props, groups). <NuxtPage> renders them; pageKey controls re-rendering; a page must have a single root when transitions are on. Programmatic navigation with navigateTo and useRouter; the whole table can be edited in the pages:extend hook.plugins/ — top-level files only (and index files in subdirectories) are registered, in alphabetical order; .server/.client suffixes; object syntax with enforce, order, dependsOn, parallel, hooks, env.islands; provide for helpers with type augmentation of NuxtApp and ComponentCustomProperties when inference is not enough; Vue plugins via nuxtApp.vueApp.use, directives via vueApp.directive (implement getSSRProps for SSR).utils/ — auto-imported like composables, kept separate for semantics (stateless helpers); same scanning rules and default-export naming.app.vue — entry component. Minimal usage renders content directly (no router); with pages it renders <NuxtPage />; with layouts it wraps in <NuxtLayout>.app.config.ts — defineAppConfig(); typed by augmenting AppConfigInput/AppConfig in nuxt/schema; merging across layers uses a custom deep merge where the project wins and arrays are replaced, not concatenated (verify); limitations: cannot use environment variables, cannot hold secrets, is bundled into the client.error.vue — the root-level error page (not a page, no layout by default); receives error as a prop; clearError({ redirect }) leaves.router.options.ts — customises Vue Router: scrollBehavior, hashMode, and a routes(defaultRoutes) function that can replace the generated routes (return null to keep them).app/
├── app.vue entry: <NuxtLayout><NuxtPage/></NuxtLayout>
├── app.config.ts reactive public config (theme tokens)
├── error.vue full-page error, gets `error` prop
├── router.options.ts scrollBehavior, hashMode, routes()
├── assets/ processed by the bundler (~/assets/…)
├── components/ recursive scan, path-prefixed names, Lazy*, .client/.server
├── composables/ top level + index.ts only (imports.dirs to deepen)
├── layouts/default.vue needs <slot/>; kebab-case names
├── middleware/ auth.ts (named) · 01.log.global.ts (global)
├── pages/ index.vue · [id].vue · [[id]].vue · [...slug].vue · (group)/
├── plugins/ top level only, alphabetical, .client/.server
└── utils/ stateless helpers, auto-imported
<script setup lang="ts">
definePageMeta({
layout: 'account',
middleware: ['auth'],
validate: route => /^\d+$/.test(String(route.params.id)), // false -> 404
key: route => route.fullPath, // remount when params change
pageTransition: { name: 'page', mode: 'out-in' },
})
</script>
<template>
<!-- single root: required when transitions are enabled -->
<section>…</section>
</template>
import type { RouterConfig } from '@nuxt/schema'
export default {
scrollBehavior(to, _from, savedPosition) {
if (savedPosition) return savedPosition
if (to.hash) return { el: to.hash, top: 80 }
return { top: 0 }
},
} satisfies RouterConfig
composables/cart/useCart.ts is ignored unless you add composables/cart/index.ts that re-exports it, or set imports.dirs. The symptom is a runtime "is not defined" with no build error. .nuxt/imports.d.ts lists what was actually registered.
The exact app.config merging strategy for arrays across layers and the current list of "known limitations" on that page.
components/base/foo/Button.vue and components/Base/BaseButton.vue; check .nuxt/components.d.ts for the generated names.composables/cart/useCart.ts without an index file, use it in a page, read the error, then fix it both ways.01.a.global.ts, 02.b.global.ts and a named middleware, log their order on a page that uses the named one."Components are scanned recursively under app/components and named from the path plus the file name in PascalCase, with repeated segments collapsed, so components/base/foo/Button.vue becomes BaseFooButton; pathPrefix: false turns that off, Lazy in front of any name gives an async import, and .client or .server suffixes decide where it renders. Composables and utils are different: only the top level of their directories and index.ts files are scanned, named exports keep their names and a default export takes the camelCased file name; deeper folders need imports.dirs or a re-export. The generated .nuxt/components.d.ts and .nuxt/imports.d.ts are the source of truth."
Nothing yet. Notes filed here show up on this page and the section overview.
Concepts
The Guide's Concepts chapters — Auto-imports, Nuxt Lifecycle, Vue.js Development (including Vapor interop), Rendering Modes, Server Components, Server Engine, Modules, ES Modules, TypeScript and Code Style — as statements to remember.
Directory structure — project root
The root-level entries the docs describe — content, layers, modules, public, server, shared, test, .env, .gitignore, .nuxt, .nuxtignore, .nuxtrc, .output, nuxt.config.ts, package.json and tsconfig.json — and the rule each one carries.