Folders That Survive Growth
Group a Vue project by feature instead of by file type, and use the @ alias so moving a file does not rewrite its imports.
import StatusBadge from '@/shared/components/StatusBadge.vue'Every new Vue project starts with src/components and src/views. That arrangement holds for ten files. At a hundred, one change touches four folders, and no folder tells you what the application does.
Group by feature
Put everything a feature needs in one folder: its components, its composables, its API calls, its types. A composable is a reusable function that uses Vue's reactivity, covered in Stage 2. So src/features/invoices/ holds components/, composables/, api.ts and types.ts. Removing the feature becomes removing the folder.
Where shared code goes
Code that two or more features use moves to src/shared/. The views the router mounts stay in src/views/, because routing is the one concern that is application-wide. Keep a view thin: it composes feature components and little else.
The import tangle
Deep folders produce imports like ../../../shared/components/StatusBadge.vue. Projects created with npm create vue@latest ship an alias for exactly this: @/ means src/. These two are the same file:
import StatusBadge from '../../../shared/components/StatusBadge.vue'import StatusBadge from '@/shared/components/StatusBadge.vue'
The alias form survives a file move; the relative form does not. One rule keeps the graph honest: a feature may import from shared, never from another feature. When two features need the same thing, that thing belongs in shared. The component below sits inside a feature folder and reaches shared code through the alias.
Example
<script setup lang="ts">
import { computed } from 'vue'
import StatusBadge from '@/shared/components/StatusBadge.vue'
import { useInvoiceTotals } from '@/features/invoices/composables/useInvoiceTotals'
import type { Invoice } from '@/features/invoices/types'
const props = defineProps<{ invoices: Invoice[] }>()
const { formattedTotal } = useInvoiceTotals(computed(() => props.invoices))
</script>
<template>
<ul>
<li v-for="invoice in props.invoices" :key="invoice.id">
<strong>{{ invoice.number }}</strong>
<status-badge :status="invoice.status" />
</li>
</ul>
<p>Total outstanding: {{ formattedTotal }}</p>
</template>
<style scoped>
ul {
list-style: none;
padding: 0;
}
</style>When to use it
- Splitting a src/components folder that has grown past fifty files into feature folders.
- Deciding whether a new button component belongs to one feature or to src/shared.
- Adding a second path alias so test fixtures import by name rather than by relative path.
More examples
The alias, as the bundler sees it
This is the vite.config.ts that npm create vue@latest generates, with Vite 8. resolve.alias is what makes '@/shared/...' resolve at build and dev time. Reach for this file when you want a second alias, for instance '#test' pointing at a fixtures folder.
// vite.config.ts
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
})The same alias, as the type checker sees it
vue-tsc reads tsconfig, not vite.config, so the alias has to be repeated here as a path mapping. If an editor underlines an '@/' import in red while the page loads correctly in the browser, this is the file that is out of step.
// tsconfig.app.json
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}A layout that reads like the product
Each feature folder is self-contained, and src/shared holds only what more than one feature uses. Use this shape once a project has more than two or three screens; below that, the default components and views folders are not yet a problem.
src/
api/
client.ts
features/
invoices/
components/InvoiceList.vue
composables/useInvoiceTotals.ts
api.ts
types.ts
settings/
components/ProfileForm.vue
api.ts
shared/
components/StatusBadge.vue
composables/useClipboard.ts
views/
InvoicesView.vue
SettingsView.vue
router/index.ts
stores/auth.ts
App.vue
main.ts
Discussion