Better Frontend Application Folder Structure — shared & isolated code, components, views, routers, styles and other cool things

Andrew Winnicki
Level Up Coding
Published in
10 min readMar 24, 2024

--

Starting a new project from the ground up rarely happens, but we would like it to be PERFECT when it does. A well-organized folder structure in applications is always a challenge. Let’s be honest, so-called “good practices” are often only suitable for specific scenarios and not many others. To me, many of such practices feel awkward. While knowing and learning from them is an essential part of every software engineer’s life, there comes a time when things have to be a little bit different and better. If we all stick to using only “best practices,” we would probably still use jQuery to write JavaScript for browsers.

This article continues a few others I wrote about our frontend architecture, so check them out.

The Frame story

My first attempt to structure a big JS application code well started over 10 years ago. The first architecture I wrote was while still working at NVIDIA. The name for this project was “nvFrame,” derived from “NVIDIA framework.” It later evolved into “yFrame” as I built the 2.0 version for Yell.com and re-architected the whole web app into modern JavaScript.

Plenty more work was done around the framework until I arrived at Omnevue (formerly ESGgen), and it all became version 3.0. All the learning from building frontend architectures came in handy, but this is the first time it was a SPA. Weirdly, I didn’t give it a formal name like oFrame ;)

So, we will talk about the folder structure that has been proven in battle, developed, and tested with multiple applications throughout the years. It is a part of our frontend monorepo architecture that currently runs four different web applications (soon expanding to six), allowing multiple developers and teams to work and deploy code simultaneously.

Let’s start from the top…

The root of a project

This is where everything starts, and the root folder should be familiar to you. I’ve removed all the noise, config files, etc., and left only the most essential things.

  • docs—it’s an automatically generated folder with documentation for the code. We are using JSDoc to describe all our code, and we also run a job that builds navigable HTML documentation, which is easily available and deployed to our environments for quick reference.
  • reports—all reports from unit tests, end-to-end tests, visual tests, and performance tests land here. We deploy all HTML reports with our code to allow us to see all test results for the given version on each environment.
  • scripts — deployment scripts, pipeline files, etc., to support all build-related processes.
  • src — all of our source code, which we will focus on below in the rest of this post.
  • tests—all scripts, configs, helpers, utilities, and other things needed for all types of testing, all in one place.
  • public—which is, of course, our code after the build and ready for release.

High-order folders (applications)

Sorry, just really wanted to use the word “high-order”. Makes this headline sound fancy and cool. A real buzzword :)

All our applications are part of a monorepo (we have one for the front end and a separate one for the backend). The content of the root/src folder splits the code by application first.

While each folder represents a separate deployable app, Core is very special on the list and is the only one that never gets deployed.

  • Core—starting with the “special one”. Each application includes Core and uses its functionality. It is responsible for all shared code across multiple apps. It contains all critical styling (grid, style utilities, etc.), design system components, reusable components, and templates. Beyond typical visual stuff, it also handles all libraries and provides JavaScript utility functions like tracking, authentication, and API connections. All code is heavily unit-tested as it is used across multiple apps and needs to be consistent. After building all the reusable code, we rarely make changes here.
  • Admin — is an application that helps us manage users, businesses, data, sales, and other typical administrative tasks to provide our services to customers.
  • Client — the frontend-facing applications used by our customers, which are the core of our business and the biggest one here.
  • DesignSystem — we have a separate application that displays all our design system components (a homemade Storybook, if you like). Its purpose is to share all reusable components with the business and other teams and remind them how everything looks “right now” in case they have any doubts. It helps frontend engineers who can access all component examples, get the implementation code (without opening the actual source code), and save time. We also use the DesignSystem app to run all our visual tests on each release to find UI changes and highlight errors.
  • Developers—this application serves as a Developer Platform for our customers. It is similar to the Client application but for a different audience.

This approach is very scalable by providing isolation between apps and allowing easy sharing of existing code. When building the Developer platform recently, getting a skeleton up and running took barely two days with all the build, test, and deployment updates.

There would never be a situation where code from the Client is directly included in code from Admin. Each app has its own code and everything from the Core.

Some companies have a repo for each app as an alternative to this approach. They regret their decisions once reaching a certain complexity level. This approach creates a lot of overhead with testing, kills the flexibility to deploy fast, increases some risks (mitigates others), and creates invisible dependencies and backward compatibility issues. In our case, every engineer can update a design system component without jumping between different codebases and then quickly roll out the changes to their application or all of them.

Our backend application architecture is very similar to the front end. This approach is scalable and flexible for different types of apps and usage.

In-app folders and their purpose

Let’s examine a typical application folder. Note that Core looks very similar but does not have things like routes or views since it is not a public-facing standalone app. Whenever you move between apps’ folders, they all feel familiar, and you instantly know where to find your components and how to navigate throughout the tree.

Every app needs an entry point, and since this app is a SPA, we have the app.js starter and a lonely HTML file.

  • assets—all extra things needed for the application. These might be logos and images that are used in multiple places. You might also find the “assets” folder in any component or view folder where things are more local.
  • components — reusable components across the whole application. Each subfolder is responsible for one component only and might have extra folders like tests, assets, and lang. Each components folder has an index.vue/jsx file as a “starting point” is (this is a very important bit, although it might feel weird, I know).
  • composables—reusable, state-aware logic for Vue/React. In our case, this folder doesn’t hold much (yet). Since the functionality differs from components or modules, we decided to keep these separate.
  • lang — translations folder where we keep all reusable text across the applications. Note that practically every component or view in the app has its own lang folder with more strings. This folder is quite specific to how we handle application translations and make text strings separate from the code itself. Ps. I wrote about our translation layer in one of my older posts, and if you are interested, go to my profile and find it :)
  • modules—JS-only code that can be reused anywhere in the given application (we have a modules folder in Core, too) and doesn’t require styles and HTML. These might be utilities, data transformation code, custom tracking implementation, etc.
  • static—static assets and pages that should be deployed with the application but are not used directly by the main code. Imagine things like a 404 page, no access page, or maybe a Not Supporter Browser error page (we’ve got one of these).
  • store — all Vue’s/React store files are in one place as none are closely bonded to a particular component or view (they should never be). The store is split into smaller files to isolate by purpose, such as user, business, products, reports, etc. All are easy to maintain and use across the whole application, built to be useful anywhere, not in a particular component.
  • tests—end-to-end tests are written in Playwright to cover the app’s top-level functionality. You can find /test folders all around the views and components as they cover particular areas and functionality.
  • views — all pages that can be viewed in the applications. Subfolders closely resemble what I have described so far, often having subfolders for assets, local components, and tests. Each folder contains router configuration for the given view; hence, we don’t have a top-level router folder serving any purpose. I explain this slightly more in the “quirks” section below.

Quirks and things you might ask

  • Using Core — we import everything from Core as one import which helps to avoid noise and the import hell in every file. All Core functionality — utilities, components, design system, etc, are available under Core.xxx namespace. Easy :)
  • Design System—all design system components are part of the Core. Still, we also have a separate application that serves as a homemade Storybook where all components can be previewed and tested.
  • Always using index.js/vue/jsx — as the entry file into the given component or view. This way, we always know where to start when navigating through folders. As an example, we actually have a complex component that doesn’t follow this rule (due to some mistakes), and it’s impossible to figure out which file is the “root” one (every single time).
  • Remove all content rule—the idea that every view or component folder is self-enclosed. If we drop a specific functionality and a view is unnecessary, deleting the folder will delete all relevant files like translations, styles, assets, tests, routes, etc.; these files are not spread across multiple folders. We don’t end up in a situation where removing some functionality from the system leaves a lot of noise and files, and nobody bothers to check if it’s safe to kill them.
  • No cross-folder imports—if a view has components, these are not used by anything outside. A component can be used only by all views within the given area. It’s a good rule of thumb.
  • No cross-app imports—similar as above, but applies to applications. The Client will never import anything from the Developer’s app. Simple.
  • Files as classes — all components and views file names are capitalised to be more aligned with being a class naming. Imports in JS behave pretty much like a singleton class; hence, we are trying to align with that. I’m still not sure if that was a good idea, but that’s what we are doing :)
  • Build process—a topic for another post. Our build process is very specific to our needs. We can build any application at any time, and they all end up as separate deployable packages. This way, we can have independent release cycles, and apps don’t affect each other in the process. We can also quickly deploy all at once if necessary.

I’m sure there are more quirks and things worth mentioning, but these felt like the most obvious and important ones to me. Maybe I will add a few more after getting some feedback from you.

Go for it!

There is no silver bullet, and the example I shared with you might be totally irrelevant to the projects you are working on. In the end, this approach might feel a bit hard if you don’t customise your build process or you are not keen on monorepo. I hope you found at least something interesting in the ways we do things at Omnevue. If you found one thing helpful, I will consider this article a success.

Let me know if you have any questions, and I will do my best to reply and clarify.

--

--

Software Engineering Changemaker. Driving digital transformation and sharing experiences and thoughts from my journey. 20 years and counting…