How to properly use Typescript, OOP and SOLID in modern web applications

Ali Afsahnoudeh
6 min readMay 9, 2022

--

The problem, the motivation and the goal

In my opinion using Typescript and object oriented programming paradigm is beneficial for medium to large and in general enterprise projects.

We can leverage many things like types and abstractions which can lead to loose coupling and it means more usability and more maintainability. These topics might seem fancy at first sight but they are important for big applications!

When I learned more about software architecture and tried to write better codes I was a backend developer. Later when I re-entered the client side world, I was confused how to organize my code as cleanly as I did in backend ones! Especially in JavaScript codebases I saw lots of failed attempts to use functional programming paradigm which were just violating every single principle of this paradigm. They were basically just using functions in a very bad and confusing way and most of them ended up making spaghettis!

I want to share what I’ve learned so far about how to leverage advanced techniques to improve a web client codebase quality.

To explain the implementation of each piece, I’ve created two SPAs with React.js and Vue.js and will refer to them in each step.

There is a link to each repository here:

React.js codebase

Vue.js codebase

A simple example for comparison

Imagine that we have two pretty standard server-side and client-side code bases. No third parties, no specific complexity! Our backend is a simple CRUD server which exposes a RESTful api and our client application is a SPA with a couple of pages. But they are going to be big applications eventually! Implementation of object oriented programming and SOLID principles and advanced techniques like dependency injection are more obvious in our imaginary backend project! I think because we are usually dealing with one type of element, logical components. For instance in a node.js app, usually JavaScript and Typescript files which contain our logic. This logic can vary from listening to a port to working with a database but still one type of component.

On the other hand in the web client, we also have UI components and all things related to them. And that’s why it’s getting less obvious how we can leverage those mentioned techniques.

The solution: Basic principles

Finally I ended up just thinking about some important basic principles and by respecting them, tried to find my way to have a better code base incrementally. And here they are:

1) Three layered architecture

The base idea is to split everything into three layers, similar to MVVM or MVP. Everything related to UI should be in the presentation layer. Everything related to business and logic in the logic layer and everything related to data and state in the model layer

  • Presentation layer: views and ui-components
  • Logic layer: services and logical-components
  • Model layer: stores and types

Look at these two examples:

React.js project structure

React.js code structure

and Vue.js project structure

Vue.js code structure

2) Separated presentation

Very simple but very important principle. In the client side applications we need to separate our business logic from UI logic. Which means we need to keep everything related to UI inside our UI components and extract the business logic somewhere else which I’ll talk about very soon!

By doing this, we are making life easier for everyone including ourselves! UI components can focus on just UI logic. Things like what to show and when to render them, how to deal with user actions and so on.

3) Separation of concerns

We need to split UI and logical components to smaller pieces and each piece should do just one thing ideally. These small pieces can be reused all across our application. In a real world situation, a perfect result is probably impossible but we can at least try to be as close as possible to this goal.

In UI components:

By having a component based approach.

And in our logical components:

By using compositions.

By having these two approaches we can have reusability and more code coherence. So as a result we will have reusable pieces which can contain a very specific logic and keep all of the elements in the same place rather than split them in different places!

For example in a React.js application we can achieve it with costume hooks and in Vue.js with composition api. Both very clean and practical. If you haven’t tried them, I strongly suggest doing it. After properly using them, you will have a cleaner code which is much easier to test and maintain.

An example of custom hook in a React.js app.

An example of composition API in a Vue.js app.

On top of that, the business logic can be isolated and just gives an abstraction to their consumers, in our case presentation layer.

4) OOP

Now that we have our reusable, testable and clean components, we need to think about the logic, the business logic.

First of all, based on our imaginary application we need to interact with a server. We definitely don’t want to mess it up with UI stuff. Also after receiving the data from the server or an action from the user, we might need to process some data or do some calculations/mappings.

I always divide this part into two pieces. I separate everything related to the outside world like API calls from the actual logic like calculations, mappings etc.

In an ideal world every single piece should solve a single problem and encapsulates what it’s doing from outside. On the other hand it should provide an abstraction to its consumers and show just necessary information. We can achieve this by using interfaces, IOC containers and dependency injection. Different elements will be coupled loosely to each other and be injected to the presentation layer. We can have this mechanism in one or multiple places. For instance if our application is big and we separated our code base into different modules based on business domains, and each module is maintained by a different team, we can have a separated injection unit for each module but keep the common stuff in the root.

Hopefully TypeScript provides interfaces. Also for the actual injection we have some tools as well! For instance we can use context in React.js or provide/inject in Vue.js.

An example of IOC container in TypeScript.

An example of injecting dependencies in a React.js app.

An example of using injected dependency in React.js.

An example of injecting dependencies in a Vue.js app: here.

and Providing it here.

An example of using injected dependency in Vue.js

5) Types and model layer

Finally for our model layer, we can keep state of some specific actions inside the related compositions so we can make sure about code coherence. And also put the shared ones in our global state management. This state management can have different sub modules. For instance in React.js if we use Zustand, we can create multiple stores and keep related data in them. Or in Vue.js we can have our modules inside Vuex or Pinia.

An example of global state management in a React.js app.

An example of global state management in a Vue.js app.

Also I strongly suggest having a lot of types to leverage TypeScript more. And I think it’s beneficial to keep them in the central place to prevent confusion.

One important note to mention

At the beginning of my exploration in frontend/web code bases, sometimes I’ve over engineered some parts! Namely in the Presentation/UI layer. In both React.js and Vue.js applications I’ve tried to implement dependency injection in UI components to make them loosely coupled. And the result was a very confusing and complex codebase. So I’ve learned to stick with the library/framework’s best practices if they make scenes and combine them with my thoughts, not over write them!

Summary

Split the code base into three layers:

  1. Presentation / View: Put ui components and pages here.

Use separated presentation, separation of concerns, component base approach and compositions.

2. Business / Logic: For keeping everything related to logic including server communications and calculations.

Use interfaces, IOC containers and dependency injection.

3. Model / Data: Put global state management and everything related to data here.

At the end

This is my personal opinion about how to use these techniques and organize a modern web code base. What do you think? :)

--

--