What is the best way to structure a Vue.js application so that it scales and remains maintainable and extendable the more it grows? This is a question that I've heard on numerous occasions and I think one answer to that question lies in the principle of predictability. When it comes to creating a scalable project you want everything about it to be as predictable as possible.
What exactly do I mean by predictability? At it's simplest, it's the ability to intuitively go from a feature request or bug report to the location in the codebase where said task can be addressed. Furthermore, it's the ability to quickly know what tools you have access to at that location in the codebase in order to complete the task at hand.
Why is this important? Well, like me, you've probably had the experience of inheriting or being introduced to an existing project and then on that first task, thinking: "I don't even know where to start!".
You might have even been dealing with a codebase for a while and had the same thought! A predictable codebase alleviates this experience as much a possible, making introducing developers to projects easier and continued work more efficient.
I think it's worth noting here that, while predictability is possible, no project will ever be 100% predictable. Every project, new or existing will have at least a slight learning curve. Also, know that predictability doesn't mean that the codebase or application is quickly understandable as a whole. Many large scale applications are simply too complex for this to be possible and they'll take time to grasp in their entirety. Thus, predictability isn't about seeing the complete finished puzzle but more like knowing the shape of a certain piece and being able to quickly see where it fits. In fact, the nature of a good codebase lends itself to being understandable a piece at a time and shouldn't require its developers to ever have to think about the whole at once.
So how do we accomplish predictability in a codebase? The answer: standards, plain and simple. Maybe that's not the answer you're looking for but it's true. The best way to make anything predictable is to make it follow a set of standards. For example, I can predict with almost 100% certainty that the new full size sheets that I bought just today, will fit my bed even though I've never dressed it with those sheets before. Why? Because of the standard sizing system for bed sheets.
So this begs the question, what kind of standards exist for the Vue.js community at large? I'd say that there are 4 sources of standards available.
While some of these are more officially meant to be standards over others, I think they all provide the opportunity to have some common patterns between projects and developers resulting in more predictable codebases.
Let's start by talking about the standardization that official libraries and popular component libraries bring. While the primary purpose of such libraries is to bring functionality to the table, a side affect of this is that shared standards are adopted.
If you use Vue Router, for instance, you're not only benefiting from the functionality of the package itself, but you end up implementing routes in one project much the same way as you implement them in another project, and much the same way as Vue.js developers all over the world implement them.
This may seem obvious, but there is a point to be made. If there is an existing popular or recommended solution for a problem in Vue.js (and even more so if it's an official solution), I would think long and hard before using anything else. I'm just as happy as the next guy, DIY-ing my own components, stores, etc but usually it pays off in the long run to use the tried and true solutions. Not just because of the functionality, test coverage, and documentation they offer but also because of the standardization they bring.
When it comes to choosing to use these more standardized solutions it's important to remember what it is you’re building. Are you building a site that needs an extremely custom and branded design? Then maybe a popular component library isn't for you. Or maybe you can get the functionality for free from an unstyled component library like Radix Vue while retaining complete control over the design. If the look of the app can be a bit more generic, then there’s absolutely NO reason to build it all yourself. Reach for a popular UI library like Vuetify, PrimeVue, or Quasar.
When it comes to project standards, file structure is an often talked about topic and while Vue has no documentation specifying a particular structure, it does provide a good starting place from the codebase generated with npm create vue@latest.
Most of us are probably familiar with this structure and that is awesome! That means we're a step closer to predictability! BUT… there really isn’t a ton to go off of. There are still a LOT of decisions you need to make with this file structure.
So are there any more community wide standards available? I’m glad you asked! Nuxt is super popular in the Vue community and has a more in-depth file structure pattern. A Nuxt project’s file structure looks like this:
This is much more comprehensive and there is literally a whole page of documentation for every folder and file on the Nuxt docs website. Could you ask for any better transparency and standardization!?
Plus, if you are into Domain Driven Design (DDD), you can even utilize Nuxt layers to replicate this file structure but into separate little logical “buckets” based on your business needs. We have an article dedicated to the topic, if you’re interested in learning more.
Some might think: “But I don’t need server side rending” for my Vue.js application, so I don’t really need Nuxt. However, I think there is NO GOOD REASON not to start any new Vue.js project without Nuxt. Don’t need SSR? No problem, just turn it off.
export default defineNuxtConfig({
ssr: false
})
Or even configure it per route (that’s right, get your perfect mix of SSR and pure CSR!)
export default defineNuxtConfig({
routeRules: {
"/admin": { ssr: false },
// all other routes are SSR
}
})
The takeaway here is that Nuxt provides a whole host of highly documented file structure standards that, in my opinion, would be irresponsible to pass up. (Not to mention, these file structures come with some auto import magic, that make coding a more pleasant experience AND if you suddenly need quick support for some server side API endpoints, Nuxt has your back). If you are a Vue pro but are new to Nuxt I recommend checking out Mastering Nuxt and/or the Nuxt.js 3 Fundamentals Course. It’s really nothing but juicy features layered on top of Vue. Do yourself a favor, it’s not a hard jump.
Finally, I certainly think there are additions that can be wisely made and certainly MUST be to handle other use cases but we'll talk more about those in a minute as they aren’t standardized at the community level.
Now, focusing on the component's directory, the Vue style guide has some further advice for us to make our file structure more predictable. Among other things, the style guide encourages the following when it comes to defining components:
Base
or App
)
Table
or a Button
component.The
TodoListItem
in a TodoList
SearchWidgetInput
, SearchWidgetResultsList
, SearchWidget
Besides these, the full style guide has a number of other standards that will help your project be more predictable to a community-wide audience of developers. I won't regurgitate them all here but highly recommend you read and follow the style guide yourself.
While there are some great standards set in place for the Vue.js community at large by official sources, there are other patterns not so widely adopted that, in my experience, can be just as helpful and made into standards for you or your team's projects. Such standards are necessary as community wide ones aren't 100% comprehensive but just beware and be strict when it comes to how team standards are decided upon and maintained... it can be a rabbit hole of ever changing rules if you're not careful. That said here are some of my recommendations for your Vue.js project standards.
Another practice that makes sense is a standardized way of naming our routes and page components. In your typical CRUD application you have the following different pages for each resource:
While some of these may end up being a nested route (like viewing the single resource in a modal overlay from within the list page), they usually end up having a dedicated route with a corresponding page.
Since I have a background in the PHP framework Laravel, when it came to naming routes and defining their paths in a predictable manner I intuitively fell back on the standards that Laravel already had in place. This made it easier for my Laravel experienced team to more quickly and intuitively work with Vue. Using a "photos" resource as an example, the naming convention prescribed by Laravel and adapted for Vue that I recommend is as follows in a Nuxt project:
Example of page file structure in a nuxt project.
Let’s break down what this is doing.
create.vue
page is where you go to CREATE a new photo. (user visits: /photos/create
)[id].vue
page is the page where you can READ the individual photo resource. The index.vue
page is where you list multiple photos at once allowing the user to nest down into the one they are interested in. (user visits: /photos/[whatever-the-id-is]
)edit-[id].vue
page is where you go to UPDATE the photo resource. (user visits: /photos/edit-[whatever-the-id-is]
)index.vue
or the [id].vue
pageThese are just the views of course. They’ll probably need some API endpoints to get and post the data from/to your database. If you already have a backend setup with a different technology, great!… hopefully it’s endpoints are organized to a certain standard. If you don’t already have a backend setup though, the following structure works great in the Nuxt server/api
directory.
Let’s break down this as well.
/api/photos/
is handled by index.post.ts
and creates a new photo in the DB/api/photos/[whatever-the-id-is]
is handled by [id].get.ts
and returns the info for that individual post. Also a GET request to /api/photos
is handled by index.get.ts
and returns a paginated list of photos./api/photos/[whatever-the-id-is]
is handled by [id].put.ts
and updates the photo data in the database/api/photos/[whatever-the-id-is]
is handled by [id].delete.ts
and deletes (or soft deletes) the resource from the databaseBesides the file structure that Nuxt gives out of the box, I suggest standardizing a few more directories. Here are my suggestions for some further team standard directories based on a Vue/Nuxt project I’ve recently been developing.
Sometimes you have components that are only useful on a single page or for a couple pages of a single resource (think the photos
resource from above). In such cases, I’ve found it helpful to store those components not inside the components
directory but in a directory inside pages/[resource (ie. photos)]
. I like to call this directory partials
. This is what it would look like in the file system:
screenshot of filesystem with partial directory nested inside of pages/photos
This just keeps me from jumping back and forth between totally disparate directories while I’m developing the page. PLUS, it let’s me know very quickly that PhotoForm.vue
is only used in one or more of the 4 pictured pages.
If you’re worried about Nuxt turning these partials into actual pages (which it will), you can easily remove these at build time.
// app/router.options.ts
import type { RouterConfig } from "@nuxt/schema";
// https://router.vuejs.org/api/interfaces/routeroptions.html
export default <RouterConfig>{
routes: (_routes) => {
if (import.meta.env.MODE === "production") {
_routes = _routes.filter((route) => !route.path.includes("partials"));
}
return _routes;
},
};
Another standard directory I like to include in all my Vue/Nuxt projects is a @/types
directory. This is my central hub for defining all the types of the project. Even if the types are autogenerated with a tool like Prisma or Drizzle, I like to import and export those generated types from this custom directory. Why? It means I don’t have to think about file location when I want to import and use a type. I write import {} from "@/types"
and then have a list of autocomplete options of ALL my types whether custom or generated.
A common need in all applications is the ability to validate user input on the backend before it makes it into your DB and often times on the front-end as well, this way the user has quick and immediate feedback about what they need to fix.
In order to organize these in my projects, I like to create a @/validators
directory and then have a file for each of my different resource types. Something like this:
Note that the .validator
keyword before the extension is nothing magic, it’s just a naming convention for easy file searching. Then inside each of these files I’ll use Zod to define the actual validation logic for the various types of requests. Something like this:
import { z } from "zod";
const BaseRules = z.object({
id: z.string().uuid(),
title: z.string().min(1),
content: z.string().min(1),
});
const fields = Object.keys(BaseRules.shape) as [keyof typeof BaseRules.shape];
export const CreateRules = BaseRules.omit({ id: true });
export const UpdateRules = BaseRules;
export const DeleteRules = z.object({
id: z.string().uuid(),
});
export const ReadRules = z.object({
id: z.string().uuid(),
include: z.enum(fields).optional(), // for limiting the fields to return
});
export const ListRules = z.object({
keywords: z.string().optional(), //for searching
limit: z.number().int().positive().optional(), // for pagination
offset: z.number().int().positive().optional(), // for pagination
sortBy: z.enum(fields).optional(),
sortOder: z.enum(["asc", "desc"]).optional(),
include: z.enum(fields).optional(), // for limiting the fields to return
});
Now I can import these various rules in BOTH my API endpoints AND my front-end page components in order to do validation on both the server side and the client side while declaring the validators only once. I like to pair Zod with Formkit for my front-end validations. You can learn how to do that in this lesson of our FormKit course or in the documentation for the FormKit zod plugin.
Alternatively, you might want to look into Valibot to handle these validations. It’s essentially the same as Zod but it’s modular architecture means it tree-shakable. This means that the validation rules you ship to the browser for you front-end validation will only include the rules you actually use for your app.
While there are some community-wide standards that you would do well not to ignore, there are also a number of standards you can make for you or your team in order to make your code bases more predictable. While some of the standards mentioned above have proven useful for me there might be others that work well for you or your team. The kicker is sticking to them across projects so they will serve their purpose.
While standards for predictability are a great benefit for your large scale Vue.js applications, there's still more that can be done. Be sure to setup linting and formatting tools like ESLint and Prettier to keep your code clean, error free, and consistent. You can set those up with a new Vue project from the prompts or use Nuxt’s ESLint module.
Finally, if you’d like even more tips for building your large scale Vue.js apps, checkout this talk I did at Vue Conf US in 2023.
Our goal is to be the number one source of Vue.js knowledge for all skill levels. We offer the knowledge of our industry leaders through awesome video courses for a ridiculously low price.
More than 200.000 users have already joined us. You are welcome too!
© All rights reserved. Made with ❤️ by BitterBrains, Inc.