Choosing the right folder structure for a middle-size or big vue application might be intimidating and it's even more so when there are not many suggestions about the topic.
From 2017 to the moment of writing this post, I've been working on Vue applications for years from medium to large, established apps migrating to Vue to projects at the start of the process. From Nuxt, Laravel, and Vue SPA; here I will try my best to describe what I think has been the more intuitive easy to work and maintain folder structure.
The alternatives
First I want to talk about the alternatives considered.
Atomic Design
This is a very complete structure to define your building blocks according to their complexity, size, and how they interact with each other taking inspiration from one of the most well-structured systems the building blocks that compose our body
It is organized as follows:
- Atoms
- Molecules
- Organisms
Those will help us to aisle functionality, making portable and reusable components even easier to unit test.
Then we used to group those organisms in templates to give them context and form in a layout and pages that are the single entry point of the content we show to our users.
- Atoms
- Molecules
- Organisms
- Templates
- Pages
|_atoms/
|__Input.vue
|__Button.vue
|__Label.vue
|_molecules
|__FieldGroup.vue
|__organisms
|__ContactForm
Benefits
Hint of complexity
You get a hint about the complexity of your components ahead. just by seeing where the component is placed: atoms are an indicator that the complexity is low and if are placed in organisms it is more complex.Reusability
As you are working with building blocks they are like Legos that you can move around atoms can be part of many molecules and organismsTestability
As components tend to be smaller and only do in isolationPerfect match with design systems
If you are starting an application from 0 and need to build a consistent design system this is almost a no-brainer
Cons
- When applied directly in an application structure you'll feel your components are all over the place.
- If an atom is just used by one molecule or single organism you can't group them by context
- Business logic
In my experience, we can get the most from Atomic Design when we are building a component library/Design System that will be the foundation of our system: Buttons, Input, Input groups, Form Wizards, SearchBars, Selectors, etc.
The Chosen One
When you are building an application you need as much context as possible in your project in the components that handle the business logic. For example, if I have a budget
|_Budget/
|__Budget.vue
|__BudgetCategory.vue
|__BudgetCategoryItem.vue
The dev experience could improved greatly when building a new feature or removing or refactoring a component because it has much more context of where is used and its props (BudgetCategoryItem could have a BudgetCategory
prop for example, and not a generic row
prop) en what domain dominio y and which is its closest parent component.
Proposed structure:
|__ assets
|__ components/
|__ locales/
|__ plugins
|__ i18n/index.ts
|__ auth0/index.ts
|__ loger/index.ts
|__ axios/index.ts
|__ config/
| |__ index.ts
|__ domains/
|__ [domain]
|__ api/
|__ models/
|__ enums/
|__ components/
|__ RequestModal.vue
|__ composables/
|__ tests/unitTest.ts
|__ pages
| |__ auth/
| |__ Partials/
| |__ AuthLayout.vue
| |__ AuthSignIn.vue
| |__ AuthSignUp.vue
| |__ AuthRecover.vue
| |__ AuthReset.vue
| |__ [domain]
| |__ Partials/
| |__ SubpageLayout.vue
| |__ [ComponentUsedOnce].vue
| |__ subpageList.vue
| |__ subpageEdit.vue
| |__ subpagesCreate.vue
|__ router/index.ts
|__ store/
|__ Modules/
|__ index.ts
|__ utils/
|__ App.vue
|__ main.ts
assets
This directory can contain images, SVG, CSS or SCSS for the application.
components
This component holds all the shared components of the application and they can be used in any part of the system.
config
Access to env variables with import.env.VITE_VARIABLE_NAME
across our app loads too much dependency on the bundler to centralize all this in a single place and give us some good ts support and add default values.
In one of my projects, it looks like:
``// config/index.ts
interface AppConfig {
FIREBASE_API_KEY: string;
FIREBASE_PROJECT_ID: string;
FIREBASE_APP_ID: string;
FIREBASE_SENDER_ID: string;
PUSH_PK: string;
MEASUREMENT_ID: string;
GOOGLE_APP_KEY: string;
GOOGLE_APP_CLIENT: string;
FIREBASE_VAPID_KEY: string;
IS_DEMO: boolean;
}
const isDemo = import.meta.env?.VITE_APP_DEMO
export const config: AppConfig = {
FIREBASE_API_KEY: import.meta.env.VITE_FIREBASE_APP_KEY,
FIREBASE_PROJECT_ID: import.meta.env.VITE_FIREBASE_PROJECT_ID,
FIREBASE_APP_ID: import.meta.env.VITE_FIREBASE_APP_ID,
FIREBASE_SENDER_ID: import.meta.env.VITE_FIREBASE_SENDER_ID,
PUSH_PK: import.meta.env.VITE_PUSH_PK,
MEASUREMENT_ID: import.meta.env.VITE_MEASUREMENT_ID,
GOOGLE_APP_KEY: import.meta.env.VITE_GOOGLE_APP_KEY,
GOOGLE_APP_CLIENT: import.meta.env.VITE_GOOGLE_CLIENT_ID,
FIREBASE_VAPID_KEY: import.meta.env.VITE_FIREBASE_VAPID_KEY,
IS_DEMO: Boolean(isDemo) && isDemo !== 'false'
}
domains
Contains the business logic of the application grouped by domain (remember DDD?) and each domain will have the sections: api, models, components, composables, unit tests
Subfolders:
api: contains all the API calls of the application as endpoints are in a central place we change once if it used in different parts of the app
models: Contains all the interfaces and types of the domain
components: Contains all the domain-related components
composables: Our domain-related vue composables
tests: Our unit tests
Pages
These components would be used only by the router and would make the main call to the endpoint, permissions (ACL), and also group different domain controllers or partials.
Subfolders
-
Partials: Partials are the only directory allowed on pages. They are only used by their respective pages
BudgetSectionTemplate.vue
think about them as templates or layouts.
Plugins
Third-party services that you can change like eg. Auth0, Axios, i18n
Trade-offs
Pros:
Provide context about business logic/business domain in the application. It would benefit onboarding new devs to the code.
As the code is grouped by domains you can extract functionality easily to make a library if it's required for another project.
Cons
Nested folders
Elements could have more than one domain
Solving conflict points:
- How to avoid over-nested components? Sometimes we tend to group subcomponents by their technical connotation eg.
Components
|__ Notifications
|__ NotificationCard
|__ NotificationTypes/
|__ TaskApproved
|__ TaskRejected
An unnecessary nesting could be avoided by naming the component with the domain in front or creating another domain.
Eg.1
Components
|__ Notifications
|__ NotificationCard
|__ NotificationTypeTaskApproved
|__ NotificationTypeTaskRejected
Eg. 2
Components
|__ Notifications
|__ NotificationCard
|__ // ...more components
|__ NotificationTypes
|_ NotificationTypeTaskApproved
|_ NotificationTypeTaskRejected
-
How to deal with a component that seems to have multiple domains?
Sometimes a domain may cross with another for example widgets that are shown in the dashboard, should be put in a dashboard domain or its domain (Projects/ Tasks/ Tracks).
In this case we should analize the composition of the component and the requirements In this case the widgets are only used in the dashboard and if we go to its composition and props like
title
graphType
anddata
all are the same in structure. Then we can conclude that each widget can be safely placed in theDashboard
domain.
Top comments (4)
What about Feature Sliced Design? It looks similar
It's a good alternative, it certainly shares some similarities but I think FSD is a more nested option.
For example a Budget widget or feature would go in independent folders instead of being in the same Domain.
GREAT!!!
Thank you!