arsalandywriter.com

<Enhancing Your Frontend Application Structure: A Comprehensive Guide>

Written on

Starting a new project from scratch is a rarity, but when it happens, we aim for it to be flawless. Achieving a well-structured application folder setup presents ongoing challenges. The so-called "best practices" often apply only to particular situations, leaving many developers feeling constrained. Although learning these practices is crucial for every software engineer, there comes a time when we must innovate beyond them. If we solely adhered to these "best practices," we might still be using jQuery for JavaScript development.

This article is part of a series discussing our frontend architecture, so be sure to check out the related pieces.

Shipping Code. Unopinionated frontend release pipelines delivering complex code, fast.

Writing code is just one part of the process; safely deploying it to production presents a different set of challenges.

levelup.gitconnected.com

A Better Versioning Technique for Frontend Applications

This approach promotes a healthy method for managing versions.

betterprogramming.pub

Better Git branching strategy — Multi-apps, monorepos and multiple teams in focus — SimGit Flow.

This strategy facilitates collaboration among multiple teams working within the same repository.

levelup.gitconnected.com

Code: Views-driven router paths, instead of a hardcoded mess — VueJS

Router configurations often end up in a bulky index.js file, where the complexities can become overwhelming.

andrewwinnicki.medium.com

My initial endeavor to structure a large JavaScript application effectively began over a decade ago. While employed at NVIDIA, I developed the first architecture named "nvFrame," derived from "NVIDIA framework." This evolved into "yFrame" when I created the 2.0 version for Yell.com and modernized the entire web application.

Much work surrounded the framework until I reached Omnevue (previously ESGgen), culminating in version 3.0. The knowledge gained from constructing frontend architectures proved invaluable, marking my first experience with a Single Page Application (SPA). Interestingly, I didn't assign it a formal name like oFrame.

We will discuss a folder structure that has been tested and refined through multiple applications over the years. This structure is part of our frontend monorepo architecture, currently supporting four different web applications (soon to expand to six), enabling multiple developers and teams to work and deploy simultaneously.

Let’s begin from the top...

The Root of a Project

This is the starting point, and the root folder should be familiar. I've stripped away the clutter, configuration files, etc., leaving only the essentials.

  • docs — An automatically generated folder containing documentation for our code. We utilize JSDoc to describe our code, and we also run a job that creates navigable HTML documentation, readily accessible for quick reference.
  • reports — This folder houses all reports from unit tests, end-to-end tests, visual tests, and performance tests. We deploy all HTML reports with our code to allow visibility into test results for each version across environments.
  • scripts — This contains deployment scripts, pipeline files, and other tools supporting our build processes.
  • src — The core of our source code, which we will delve into further below.
  • tests — Here, you'll find all scripts, configurations, helpers, utilities, and other resources required for various testing types, consolidated in one location.
  • public — This folder holds our code post-build, ready for release.

High-Order Folders (Applications)

Apologies, I just had to use the term “high-order.” It adds a certain flair to the title!

All our applications reside within a monorepo (one for the frontend and another for the backend). The contents of the root/src folder organize the code by application first.

Each folder corresponds to a distinct deployable application, with Core holding a unique position as it is never deployed.

  • Core — The standout application. Each app incorporates Core, which serves shared functionalities. It encompasses essential styling (grid, style utilities, etc.), design system components, reusable elements, and templates. Beyond visual components, it also manages libraries and provides JavaScript utility functions like tracking, authentication, and API integrations. All code is rigorously unit-tested to ensure consistency across applications. Once built, changes to reusable code are rare.
  • Admin — This application aids in managing users, businesses, data, sales, and other administrative tasks to support our services.
  • Client — The frontend applications utilized by our customers, forming the backbone of our business and the largest component here.
  • DesignSystem — A dedicated application showcasing all design system components (think of it as a custom Storybook). Its purpose is to share reusable components with the business and other teams, ensuring they know how everything currently appears. It assists frontend engineers in accessing component examples, obtaining implementation code (without diving into the actual source), and saving time. Additionally, we utilize the DesignSystem app to conduct visual tests on each release to detect UI changes and errors.
  • Developers — This application serves as a Developer Platform for our customers, mirroring the Client application but catering to a different audience.

This architecture is highly scalable, providing isolation between applications while facilitating easy code sharing. When developing the Developer platform recently, I set up the skeleton in just two days, thanks to the build, test, and deployment updates.

> There will never be a scenario where code from the Client is directly integrated into the Admin. Each application possesses its own code alongside everything from Core.

Some companies opt for a separate repository for each application, only to regret that choice upon reaching a certain complexity level. This method incurs significant overhead in testing, hampers rapid deployments, heightens certain risks (while mitigating others), and creates hidden dependencies and backward compatibility issues. In our case, every engineer can update a design system component without hopping between different codebases and can quickly deploy changes across applications.

Our backend application structure closely mirrors the frontend, ensuring scalability and flexibility for various applications and use cases.

In-App Folders and Their Purpose

Let’s delve into a typical application folder. Note that Core resembles this structure but lacks elements like routes or views since it is not a public-facing standalone application. Moving between application folders feels intuitive, allowing for easy navigation.

Every application requires an entry point, and since this is a SPA, we have the app.js starter along with a solitary HTML file.

  • assets — This folder includes additional resources required for the application, such as logos and images used in various places. You may also find an "assets" folder in any component or view folder for more localized resources.
  • components — These are reusable components across the entire application. Each subfolder is dedicated to a single component, potentially containing additional folders for tests, assets, and languages. Each component folder features an index.vue/jsx file as the entry point (a crucial aspect, albeit it may seem odd).
  • composables — This folder holds reusable, state-aware logic for Vue/React. Currently, it contains limited content, but it’s kept separate from components or modules due to different functionalities.
  • lang — A translations folder housing all reusable text across applications. Each component or view typically has its own lang folder for additional strings. This approach helps keep translations distinct from the code itself.
  • modules — JavaScript-only code that can be reused throughout the application (there's also a modules folder in Core) and does not necessitate styles or HTML. This includes utilities, data transformation code, custom tracking implementations, etc.
  • static — This contains static assets and pages that should be deployed with the application but are not directly utilized by the main code. Examples include a 404 page, access denied page, or even a "Not Supported Browser" error page.
  • store — All Vue/React store files are centralized, as none are closely tied to a specific component or view (and they shouldn't be). The store is divided into smaller files by purpose, such as user, business, products, reports, etc. Each file is designed for ease of maintenance and use across the entire application.
  • tests — End-to-end tests are crafted in Playwright to cover the application's top-level functionality. You'll find /test folders distributed among views and components, targeting specific areas and functionalities.
  • views — This encompasses all pages viewable within the applications. Subfolders typically align with the previously described structure, often containing subfolders for assets, local components, and tests. Each folder includes router configuration for the respective view, which is why we don’t maintain a top-level router folder.

Quirks and FAQs

  • Using Core — We consolidate all imports from Core into a single import, minimizing clutter and avoiding import chaos in each file. All Core functionalities — utilities, components, design system, etc., are accessible under the Core.xxx namespace.
  • Design System — Although all design system components are part of Core, we also maintain a separate application that functions as a custom Storybook for previewing and testing all components.
  • Always using index.js/vue/jsx — This practice designates the entry file for each component or view, ensuring we know exactly where to begin when navigating folders. For instance, we have one complex component that deviates from this rule (due to past errors), making it challenging to identify the "root" file each time.
  • Remove all content rule — Every view or component folder is self-contained. If we discard a specific functionality and a view becomes unnecessary, deleting the folder will also remove all associated files like translations, styles, assets, tests, routes, etc.; these files aren’t scattered across multiple folders. This prevents situations where removing a feature leaves behind irrelevant files.
  • No cross-folder imports — If a view contains components, these are not utilized externally. A component can only be employed by views within its designated area.
  • No cross-app imports — Similar to the above, this rule applies to applications. The Client will never import anything from the Developer app. It’s that straightforward.
  • Files as classes — All component and view file names are capitalized to align with class naming conventions. Imports in JavaScript behave similarly to singleton classes; hence, we strive for consistency. I’m still uncertain if this was the best decision, but that’s the direction we’ve taken.
  • Build process — A topic for another discussion. Our build process is uniquely tailored to our requirements, allowing us to construct any application at any time. Each ends up as a separate deployable package, enabling independent release cycles without mutual interference. We can also deploy everything simultaneously when necessary.

I’m sure there are additional quirks worth noting, but these seemed the most pertinent. I may add more insights after receiving feedback.

Take the Leap!

There is no one-size-fits-all solution, and the example I’ve shared may not apply to your specific projects. Ultimately, this approach could feel daunting if you don’t customize your build process or aren’t inclined towards a monorepo structure. I hope you found at least a few interesting points in how we operate at Omnevue. If you gleaned even one useful takeaway, I’ll consider this article a success.

Feel free to reach out with any questions, and I’ll do my best to respond and clarify!

Share the page:

Twitter Facebook Reddit LinkIn

-----------------------

Recent Post:

Embracing Good Memories: Overcoming Fear of Nostalgia

Explore how to cherish good memories without fear and the role of mindfulness in overcoming anticipatory nostalgia.

The Surprising Health Benefits of Humming and Sound

Discover the profound physiological effects of humming and sound on health and well-being.

# The Impact of AI on Employment: A Double-Edged Sword

Explore the potential job losses due to AI, its effects on various sectors, and the need for responsible development.

How to Stay Agile and Active as You Age: A Kinesiologist's Guide

Discover how to maintain mobility and reduce stiffness as you age with practical tips and exercises for a healthier lifestyle.

Lessons Learned from Writing My Debut Book: A Journey of Growth

Discover the valuable insights gained from writing my first book, from filtering thoughts to embracing challenges along the way.

Navigating the Challenges of Job Loss: Insights and Strategies

Discover essential lessons learned after job loss and strategies to cope effectively.

The Enchantment of Nature: A Poetic Exploration of Orchids

A poetic reflection on the romance of nature through the lens of orchids and their unique symbiotic relationships.

Navigating Career Advancement: Beyond Just Being Good at Your Job

Learn how to effectively navigate career advancement by understanding gatekeepers and opportunities in the workplace.