React

Structuring a Nuxt 3 project: folders and boundaries

In Nuxt 3 folders are APIs: pages, server and composables change behaviour. A structure that says where each file belongs.

Key points

  • Every reserved-name folder in Nuxt 3 turns on a behaviour, so moving a file changes the app.
  • server/ never reaches the browser, so database calls belong there.
  • components/ui/Button.vue is called UiButton: the subfolder becomes part of the name.

In Nuxt 3 the project structure is configuration: every folder with a reserved name turns on a behaviour, so moving a file changes the app. The useful rule is to keep pages/ for routes, server/ for code the client must never see and composables/ for shared state.

The folders that change behaviour

FolderWhat it turns onBoundary
pages/Routes generated from the file systemClient
server/api/HTTP endpoints named after the pathServer
server/utils/Functions handlers can importServer
composables/Auto-import from the file nameClient and server
middleware/Guards that run before the renderClient and server
plugins/Code that runs at startupDepends on the suffix

The plugin suffix decides the boundary: .client.ts runs only in the browser, .server.ts only on Node. Without a suffix it runs in both, and one access to window breaks the build.

A tree that holds

app.vue
pages/
  index.vue
  blog/[slug].vue
layouts/
  default.vue
components/
  ui/            # primitive: button, input, dialog
  app/           # composizioni legate a una pagina
composables/
  useSession.ts
server/
  api/posts.get.ts
  utils/db.ts
middleware/auth.ts
nuxt.config.ts

Components in a subfolder take their name from that folder: components/ui/Button.vue becomes <UiButton />. This is the most common cause of the «component not found» error after a move.

Server and client: two worlds, one repository

// server/api/posts.get.ts
export default defineEventHandler(async (event) => {
  const limit = Number(getQuery(event).limit ?? 10);
  return await db.post.findMany({ take: limit });
});

Code in server/ never reaches the browser bundle. A database call inside a client component is not a syntax error: it fails at runtime, when the build tries to include it.

Mistakes that cost time

  • Calling useFetch inside a loop: one request per item and the same cache key for all of them.
  • Putting domain logic in composables/ and calling it from server/: the same file gets auto-imported into two different contexts.
  • Creating utils/ at the root and expecting the server to auto-import it: handlers use server/utils/.
  • Skipping nuxi prepare after a clone: the generated types are missing and the editor reports false errors.

The same server/client boundary applies in other frameworks: SvelteKit form actions work precisely because the write path never reaches the browser.

The known limitation

Auto-import saves lines and hides dependencies. In a project with several teams, turn it off for components and import them by hand, or accept that finding a symbol happens through the editor extension rather than the file.

Commands

npx nuxi prepare
npx nuxi dev

From the catalogue: shadcn-vue, the Nuxt icon and the Vue logo block. The guides: UI libraries for Vue 3 and Vue Router navigation guards; the comparison: Vue against React in 2026.

Frequently asked questions

Where does app.vue go in Nuxt 3?

In the project root, next to nuxt.config.ts. From Nuxt 4 the app code lives in app/ and app.vue moves inside that folder.

Is the pages folder mandatory?

No. Without pages the app uses app.vue as its only page. The folder earns its place when you want routes generated from the file system.

Why is my component not found?

The name includes the subfolder: components/ui/Button.vue is used as UiButton. Move the file into components/ if you want the short name.

Can I use server/utils in a component?

No. That code is compiled for the server and never enters the browser bundle; extract the shared part into utils/ or a package.

How do I turn off auto-import?

In nuxt.config.ts set imports.autoImport to false for the APIs and components to false for components, then import everything by hand.