How to Structure a Large Nuxt / Vue 3 Application: Modular Architecture That Scales
Moving away from the default flat components/ and composables/ directories toward domain-driven, feature-sliced architecture that stays maintainable when a project hits 100+ pages.
Nuxt 3 & 4 provide an extraordinarily productive developer experience out of the box. You create a file in components/, define a function in composables/, and auto-import handles the rest.
However, once your application grows beyond 50 pages, 150 components, and multiple distinct business domains (Authentication, Billing, Onboarding, Analytics, Settings), the default flat directory structure falls apart:
components/turns into a chaotic junk drawer of 200 files.composables/becomes a tangled web of cross-feature dependencies.- Onboarding new developers requires knowing the exact naming convention of every component.
In this article, I present the domain-driven, feature-sliced architecture I use to keep large Nuxt applications maintainable and scalable.
1. The Anti-Pattern: Layer-First Organization
The default structure organizes code by technical layer:
π components/
βββ UserAvatar.vue
βββ InvoiceTable.vue
βββ CheckoutModal.vue
βββ PricingCard.vue
π composables/
βββ useAuth.ts
βββ useBilling.ts
βββ useUser.ts
When you need to modify the Billing flow, you have to jump between 6 different top-level folders. Deleting or refactoring a feature requires hunting down disconnected files.
2. The Solution: Domain-Driven Modules with Nuxt Layers
Instead of organizing by technical layer, organize by Business Domain:
π app/
π layers/
βββ core/ # Design system, layout primitives, base utilities
β βββ components/ui/ # Button, Badge, Modal, Input
β βββ composables/ # useApi, useStorage, useColorTheme
β
βββ auth/ # Auth domain
β βββ components/ # LoginForm, RegisterForm, ResetPasswordModal
β βββ composables/ # useAuth, useSession
β βββ pages/ # /login, /register, /forgot-password
β
βββ billing/ # Billing & Subscription domain
β βββ components/ # PricingTable, SubscriptionCard, InvoiceRow
β βββ composables/ # useStripe, useInvoices
β βββ server/api/ # /api/stripe/webhook, /api/billing/portal
β βββ pages/ # /billing, /checkout
β
βββ projects/ # Core product domain
Enabling Layers in nuxt.config.ts
Nuxt has native support for this through Nuxt Layers (Extends):
// nuxt.config.ts
export default defineNuxtConfig({
extends: [
'./layers/core',
'./layers/auth',
'./layers/billing',
'./layers/projects',
],
})
3. Strict Dependency Rules Between Domains
To avoid spaghetti dependencies:
- Feature layers can import from
core, butcorecan NEVER import from a feature layer. - Feature layers cannot directly cross-import each other's components. If the
projectsdomain needs to know if the user has an active subscription, it asks a centralized shared store or event bus (useAuth().isSubscribed), rather than directly mounting a Billing component.
4. Colocating Server APIs with Their Domain
In Nuxt 4, you can colocate your server endpoints directly inside the corresponding domain layer:
π layers/billing/server/api/
βββ checkout.post.ts
βββ webhook.post.ts
βββ invoices.get.ts
This keeps the full vertical slice of a feature (UI, state, database query, API route) together in a single self-contained directory. If you ever retire the billing system or extract it into a standalone microservice, you can isolate it cleanly.
Share this article
Adam Abdel-Djamal
Full Stack DeveloperAdam Abdel-Djamal is a Full-Stack & AI Engineer crafting high-performance web applications with Nuxt, Vue, Laravel, and autonomous AI agents.
Related Articles
6 Common API Mistakes (And How to Fix Them)
From wrong HTTP methods to leaking sensitive data β the errors most developers make when building their first API, and the concrete fixes.
API Rate Limiting in Practice: Token Bucket vs. Sliding Window with Redis
Comparing rate-limiting algorithms to protect your backend against scrapers, credential stuffing, and bot spam with minimal latency overhead.