Modular Architecture: A Framework For Building Clean, Easy-To-Maintain JavaScript Apps

I, for one, can barely remember a world without the internet. 

In truth, I can scarcely remember life without a smartphone. Looking at the stats, I think it’s fair to say many people are in the same boat.

Given the trajectory we’re on, we’re unlikely to see a drop in the need for developers (of all kinds) any time soon, and the below studies attest to that:

This developer shortage means that now more than ever, we need repeatable ways to build apps that use clean code that’s also easy to maintain.

The following article distills down our experience from multiple projects into a JavaScript framework we now call ‘Modular Architecture’ — and it’s a guideline that both new and more experienced engineers can adopt.

By reading this text, we hope you’ll learn:

  • How DLabs.AI approaches JavaScript
  • Jargon functioning in JS development
  • How to build JS apps with a clear semantic project structure and well-defined data models and flows 
  • How to design an easily readable and testable codebase

At the end of the article, you’ll also find a repository with a working example that uses the guideline, helping you put what you’ve learned to use. And to make sure everyone can follow what we’re talking about…

 We’ll start with a glossary of terms used in this text.

Glossary

Term

Meaning

Business logic

The part of the program that encodes the real-world business rules determining how data can be created, stored, and changed. Prescribes how business objects interact with one another and enforces the routes and methods by which business objects are accessed and updated. In simple terms: this is the meat in your app.

Component

Components are UI building blocks in the app.

Constant

A variable whose value cannot be changed (in the case of a constant object in JS, the reference cannot be changed).

Data flow

Movement of data through an app.

Data mock

Fake data which is artificially inserted into a piece of software, often used for testing in isolation.

Data model

Organizes elements of data and standardizes how they relate to one another (and to the properties of real-world entities).

Data type

A classification that specifies which type of value a variable has — alongside what type of operations can be applied to it without causing an error.

E2E test

A technique that tests the entire software product from beginning to end to ensure the application behaves as expected.

Flux architecture

An application architecture developed by Facebook. It complements composable view components by utilizing a unidirectional data flow.

Helper

Place for small, repeatable operations used across the app.

Module

The fundamental element in Modular Architecture that gathers code elements related to a domain in one place.

Service

The code responsible for business logic within a module.

Software architecture

Defines functional and non-functional requirements and high-level technical dependencies.

Software design

Defines low-level requirements for the software, including aspects like code structure, data models, UI, and business logic composition.

Store

The place for data management tools (e.g., Redux).

UI

User interface.

Unit test

A way of testing a unit (which is the smallest piece of code that can be logically isolated in a system).

Wireframe

A skeletal blueprint or framework that outlines the basic design and functions of a user interface.

View

The UI component that represents a single page or major route in the application.

What is Software Architecture? And Why Does it Matter?

Let’s kick things off by reviewing two fundamental aspects of engineering: software architecture and software design.

First up, software architecture.

Software architecture

People often confuse software architecture for software design. But these two terms are not interchangeable.

Software architecture helps us see the big picture of a project. It covers the high-level components of the design and how they’ll interact within a project, answering questions like ‘where’ and ‘how’ by guiding:

  • What needs to be done from a business perspective 
  • How these needs translate to technical requirements (infrastructure, data storage, services, etc.)
  • How these technical components will be arranged and connected

Deciding on the above helps us determine the functional and non-functional requirements — alongside the high-level technical dependencies. And with these aspects defined, we have a clearer path forward, which will help us keep the overall quality high.

“In general, software architecture helps us monitor performance, scalability, and reliability.”

— Iryna Deremuk, Modern Web Application Architecture Explained

Software design

On the other hand, software design answers the question of ‘how’ — but it does this at the code level.

In place of considering the big picture, this is where we handle the details, deciding how to build the individual parts of the system, including the project structure, data models, UI, and business logic composition, among other aspects.

— ‘Why is that important?’ you ask. Why can’t we just write code? Well, let’s get some perspective. If you’ve ever tried to assemble a Lego set without instructions, you’ll know how important that piece of the puzzle is. 

The instructions are the part of the design that gives us a universal language to follow: a language that enables multiple parties to work together and create well-defined solutions for known problems.

With a clear architecture and code design, development is much more straightforward. Now, let’s dive into the guidelines for building clean, easy-to-maintain JavaScript apps.

 

Note: Even though you can apply these guidelines to backend Node applications, in this article, we’ll only focus on web and mobile apps that we can create in React and React Native, respectively.

Modular Architecture: Components

First up, we’ll look at the core components.

Module

As the ‘Modular Architecture’ name suggests, a module is fundamental to the framework. A module represents a single responsibility within an application (say, authentication, reporting, or user account management). 

A module often corresponds to an epic. And to allow a module to represent a responsibility, it collects all related items in one place.

View

A view is a simple UI component representing a single page or major route in the application. It serves as a place to bootstrap underlying components, rarely holding any UI logic.

Component

Components are the UI building blocks in the app. They’re small pieces of code representing UI elements, mostly holding the majority of the UI logic.

Service

This is the place for business logic. Each service should serve a specific purpose, and the name of each service should reflect said purpose (the service that handles reports = ReportsService; auth = AuthService; you get the point!).

Helper

Here’s where to put small, repeatable operations used across the app. Something that’s not exactly business logic or UI logic, but that might help them.

Store

We keep all things related to data management in the store. Most of the time, we stick to Redux or ContextAPI (only when building with React) and use it for reducers, actions, and store slice.

The store is optional: you might not need Flux architecture in every app.

Now, let’s move on to the code structure and learn how you’ll put each of these components to good use.

Code Structure

A module collects all related items in one place. That said, it still manages to emphasize the division of responsibility within the module.

After all, we want to keep the UI, business logic, and data management separate. And the element that helps us achieve this separation is the code structure, so let’s now take a look at a project that benefits from this modular design.

Structure of an example project grouped by modules

/Project root
|-ui (global UI components)
|-[...] (other directories)
|-modules
 |-ExampleModule
   |-components (UI components related only to the module)
   |-constants
   |-store (place for data management tools)
   |-helpers (place for minor, reusable functions)
   |-mocks (place for data mocks)
   |-services (module’s business logic)
   |-types (module’s type declarations)
   |-views

Structuring a project as above improves the semantics, making it easier to position new elements and look for existing code pieces. 

In our experience, it’s also offered better unit test coverage. That’s because the business logic was well organized and separated from the UI logic.

Components Split

The approach to use depends on whether we have UI designs available.

Assuming we do, we can use them as a base to draw boundaries between components and decide what belongs where (which is also helpful when verifying if we should split the underlying user story into correct pieces and define data models).

Let’s take the wireframe of the reports feature as an example.

Knowing that a module should gather related items and the expected code structure, we could draw certain boundaries.

 

This yields in the following module:

|-ReportsModule
  |-components
  | |-GenericReport
  | | |-Controls
  | | |-Settings
  | | | |-Filters
  | | | |-Sorting
  | | | |-Metrics
  | | |-ReportTable
  | |   |-Entry
  | |-SavedReports
  |   |-[...]
  |-constant
  |-store
  |-helpers
  |-mocks
  |-services
  | |-ReportsService
  |-types
  |-views
    |-ReportsView

 

We can use the boundaries drawn on the wireframe and a view of what the data might look to determine an initial data model for a future contract between the backend and the frontend.

The below is the data model created from the boundaries drawn on the wireframe.

interface Settings {
 filters: string[];
 sorting: string[];
 metrics: string[];
}

interface Item {
 id: number;
 name: string;
 property: string;
}

interface Report {
 settings: Settings;
 items: Item[];
}

 

Where UI designs aren’t available (or applicable), we can define similar boundaries, structure, and data models using business needs as expressed in a user story.

Data Flows

We’ve touched on data models; now’s the time to talk about them.

I’ve seen the UI manipulate the data in many projects I’ve worked on. This often introduces unnecessary complexity to a project with multiple negative consequences. 

Having UI manipulate data calls for business logic inside the UI, breaking the separation of responsibilities. It also introduces another source of truth within the application and makes the logic inside the UI barely testable. 

You might say, ‘So what?’ — but just wait till you have to debug, test, or maintain an app built in this way.

So please trust me when I say: Business logic that lives in the services should never be mixed with the UI logic. UI simply takes care of rendering the data to the user. Which is why when using modules, we keep the data flow unidirectional (see Flux architecture). 

Views and components can only request an action to be executed by a service, and the process is entirely transparent to the UI. 

Services and helpers take care of the operation (API communication, data processing, pushing data to a store), then a store manager provides new data to the UI.

In the above image, the solid line shows the data flow.

Thanks to this approach, we gain a single source of truth and solid separation of concerns between the UI, business logic, and data handling. 

The UI only ever consumes data, never processing it. And if the code doesn’t need flux (say, in a tiny web app or backend service), you don’t have to use a store management tool at all until the business logic lives only within the services.

Naming Conventions

As you’ve probably noted, we like to have our code easily readable and semantically meaningful, which is why we agreed on a set of naming conventions. 

But what you see below is only a baseline. And it’s not written in stone. Every team can adapt it to fit their purpose. And once you have the changes documented, your team should stick to the established rules.

It’s worth noting: we apply these conventions to our React, React Native and Node apps. That said, they’re not a mandatory aspect of the Modular Architecture approach.

While if you have your own conventions, they’ll also work.

  • Classes, views, helpers, store files, mocks, etc. use PascalCase with the suffix pointing to the role (MainView.tsx, MainStyles.ts, MainModel.ts, and so on). The only exception of the suffix rule are UI components.
  • Classes, variables, and functions are named to indicate their purpose (we favor readability over the name length)
  • Variables serving as flags should have the ‘is’ prefix (e.g., isVisible)
  • Functions serving as event handlers should have the ‘handle’ prefix (e.g., handleOptionSelection)
  • Actions handling components should expose them with the ‘on’ prefix (e.g., onClick)
  • Classes, components, and modules should be named as a singular noun
  • Functions should be named as a verb

Cons of Modular Architecture

Right, so we’ve got this far, and you’re probably thinking, ‘What’s the catch?’ 

I can openly admit: there are some disadvantages to the modular approach, which I have summarized below:

  • The import path can get long (which can be annoying)
  • The config is slightly more confusing than you might expect
  • The prep means it can take a little longer to actually start coding

Pros of Modular Architecture

Despite the cons, we’ve built multiple apps using our Modular Architecture guidelines, and while it takes additional effort and has some disadvantages, the positives easily outweigh the negatives.

Just take a look and see what you’ll get:

  • A clear and readable file structure
  • A clear separation of business and UI logic
  • Simpler testing of business logic and UI
  • Piece together applications like Lego
  • No concerns about unexpected dependencies
  • A single source of truth
  • Spot overloaded components

And to top it all off — the separation of the business logic means you can share the app across mobile and web applications (assuming they both use JavaScript).

How to Test Modular Architecture

If you felt like this detail was missing, rest assured: I deliberately left testing to last. 

That’s because the subject is way too important to sit hidden amidst the other content; it requires its own section.

In fact, I’m going to write an in-depth article on our testing approach alone. But for now, I’ll simply share a quick summary. Basically, we write two types of tests: unit tests — and end-to-end tests. 

  • Unit tests cover business logic using Jest (the minimum code coverage is 80% for most projects; however, with Modular Architecture, 100% isn’t uncommon);
  • End-to-end tests cover the entire product (just to be sure the app behaves exactly as expected).

No great architecture can thrive without a robust testing structure, which is why we invested time in building one for Modular Architecture.

See Modular Architecture In Action

There you have it: this is how DLabs.AI codes JavaScript. 

To help you better understand the principles, you can experience our working example, which is a slice of our Kalkulator WBT app.

I even challenged myself to reuse the business logic across mobile and web, so have fun exploring the repo, and please — do share your thoughts. And if you have an idea on how we can improve the framework or use it in other scenarios.

Drop me a message.


P.S. If you are curious about the photo at the top of the article – yes, it’s me on the right. In DLabs.AI, we have no dress code, so we can work even in pyjamas. If this suits you, check out the open positions and join us.
P.P.S. And more seriously – this is a photo from our company integration 🙂

Krzysztof Miśtal

JavaScript Tech Lead focused on everything JS. He is also fascinated with cameras.

Read more on our blog