Frontend Architecture for Insane Scalability ⚠️
Probably you have an idea banging upon your head and are wondering the best way to implement it. Don’t be anxious, don’t hurry! Don’t start writing code right away. If you have a project idea, either to start a company from it or to show it off to employers or recruiters, it needs to be done correctly, and thinking about a scalable architecture is a good way to start. Let’s dig in!
What do we mean by “Architecture”?
Of course, let’s go to the Wikipedia and check the definition:
Software architecture refers to the fundamental structures of a software system and the discipline of creating such structures and systems. Each structure comprises software elements, relations among them, and properties of both elements and relations.
Yes, I know you could’ve done it by yourself, but you didn’t. Let’s break this definition down into pieces, naturally, giving my opinion!
For me, the definition of architecture depends on the scope, on the “zoom” you are seeing the system with. It can mean how to structure the system as a whole, with frontend, backend, and databases; it can mean how to organize microservices communication… But in our case, we’ll zoom in and just look to the frontend scope, which is already enough, trust me.
Frontend architecture contemplates choosing the right tech stack (in our case it is already chosen, and I’ll explain to you why later), structuring our project files, and establishing the communication between them.
Why do we need a frontend architecture?
If you reached this point, you are probably wondering why? WHY!? Well, let me explain by coming back to my first paragraph. Imagine that you start a project to sell second-hand stuff. Your MVP consists of creating user accounts with their phone number who can post products with a photo, a description, and a price. Then you go to market and your clients start using your platform and ask you to include a chat. Your investors want to throw money into your brand new startup, but first, they need you to include categories and a search bar. Then you do some market research and find out that you can collaborate with existing car manufacturers to sell their used cars, but you need to differentiate them from the rest of the products your users are already selling in your application. Don’t you see how sick this is growing? Don’t you see where do I want to go?
The best part of this is that everybody will be setting deadlines, your investors, your users, your partners… And if your codebase isn’t ready to handle these new features, your startup is dead.
How can you avoid this situation? Good question, by thinking before coding at the beginning, establish how to organize your code, how to manage your information, and how the different parts of your software will communicate.
When do we need to think about frontend architecture?
I’d say you should always think about it because it will make you a better developer. However, if the project solves a single small concrete problem and you are sure it won’t go further (waterfall), it’s not that important and you can probably deliver the project faster without any negative impact.
Why did we choose Next.JS?
This is a good question, and the answer is simple. Next.JS is a great framework for starting a project and making it grow over time. It’s easy to kick it off, without much configuration. It provides you with all the React.JS power with steroids, hybrid static & server rendering, TypeScript support, smart bundling, route pre-fetching, and more. In addition, if your project needs backend support it can be easily developed within the Next.JS ecosystem. And when the project grows and needs its backend or even its microservices, the existing code can be easily moved to an express server with its API. Is it enough, or do you need more? It’s SEO friendly, easy to deploy, gets on well with other technologies, has great documentation, and all the React community behind it.
What looks good with Next.JS?
As I told you in the last paragraph, Next.JS gets on well with a lot of technologies and selecting the right ones for our needs, it’s key for our successful architecture. Regardless, we should always choose stable libraries and packages that have good community support in case we have any problems.
Going towards high scalability is almost the same as saying that we need to choose some kind of typed language, sacrificing some time typing our code can avoid spending time, money, and energy in the future seeking bugs.
Styling is an important part of the frontend development and we should take care of it, but not spend too much time on setting it up, maintaining it, or taking care of a lot of different screen sizes… For this reason, a styling library can take some work out of the way. However, adding a components library isn’t the best way to approach this, because we don’t have full control of our product and we can get conditioned by it in the future.
I recommend you to have your own components library, ideally in a separate repository and installable through npm. However, in the beginning, this can be tedious, so we will set up our folders structure to ease this process in the future.
Tailwind is a great solution for styling our components, as it lets us easily customize every line of CSS, with defined class names or create our own on top of the default ones and use them with CSS modules.
React Hook Forms
A typical web application nowadays is going to have some forms that need to be handled in the easiest way possible with validation and calls to the backend. I’ve found that using the react-hook-forms library makes this process easier, as it provides us with hooks that handle a lot of the logic for us under the hood.
We’ll see the best approach to organize our API calls later, but we’ll need a library like React Query to handle caching for us. Caching may have an important economic impact on our end application, saving costs by reducing backend calls, and serving images from, for example, s3 buckets that bill by request made.
Eslint and Prettier
Our linter and code formatter of confidence. They are here to make our jobs easier. The purpose of having a linter is to show bad practices, potential bugs, and stylistic errors. And combined with a good formatter, such as prettier, we can make sure that every member of our team is on the same page, with the same code style. It’s very common that when working with several people in the same code base, some pull requests show more changes regarding formatting issues, as there are some lines of code changed by the editor formatter (tabs, intros, spaces, parenthesis…), which can vary from one developer to another. This way, we make sure that everybody follows the same rules and the best thing is that both technologies can be easily incorporated into our code editors.
Although having a linter and a formatter may help a lot to deliver good quality code, there are some more techniques that we can use to improve this process. The first one that comes to my mind is having absolute imports with directory aliases. Why should we use ../../../../filename instead of saying @directory/filename? This is a pretty silly change and easy to implement, by adding some aliases to the tsconfig.json file and creating.
How to Organize your Code for a Potential Grow
In my professional experience, I’ve seen projects that didn’t invest time to set up a folder structure and a data flow between components and ended up becoming a mess. The code should speak by itself, and you should be able to find anything without a doubt. This is done with a good folder structure along with a consistent naming convention.
Divide and Conquer
- Components. Classic, right? Every React-based project has this folder, but the problem is what’s inside it. On most occasions is where the mess resides. For me, it should have the following subfolders, at least:
- Ui. All the basic components, the smallest components of your interface should be inside this folder, which can be exported to a new repository and uploaded to a package manager like npm later down the road.
- Layout. Your web application may have different layouts depending on your route. For example, you may have a home page layout and a dashboard layout, depending on if you are logged in or not. All these layouts should be inside this folder.
- Page-related components. Imagine you have a products page with a ProductGrid component that is only useful in the context of the products page. Then you should have a folder inside the components directory called products with this component inside. Of course, ProductGrid will use components from the ui and products folder.
- Section. All the components that are common to different layouts or pages such as headers, and footers, but are not that generic to export them to a common package should go in this folder. These components should be built on top of components from the ui folder, but they are used by many page-related components.
- Seo. I like having a page just for SEO components that I will use in my pages.
- Context. Put here your context providers and your hooks to access them.
- Lib. In this folder, I handle all the logic of the application itself. Internationalization, hooks to reuse some logic across components and constants.
- Api. Of course, this folder will have all the backend calls and services, preferably using hooks to create the requests (React Query). I divide them by domain. Following the previous examples, CRUD operations related to products would go in api/products.
- Public. All the media: photos, videos, fonts, favicon.
- Styles. Usually pretty silly, but I add my tailwind styles to it.
- Locale. JSON files including the translations to all the languages covered by the application.
- Pages. provided when generating a Next.JS project, it will contain the different routes within your web application. I recommend you set up an internationalization system in the URL itself. This can be easily done by creating a folder named [locale]. Then you can create a Provider with access to the route query and get the language from the URL. This can be covered in another article with code examples.
- Configurations files such as .prettier, .eslintrc, nextjs.config.js, tailwlind.config.js, etc… are in the top-level folder.
React Patterns you should follow
This section can be an article by itself, so I’ll just say two important things to consider at the beginning and extend this topic in the future with another post.
How to Manage your Application State
In a nutshell, this section will cover how to engender the communication and sharing of data across the components of the application. This section won’t cover the simple props passing from parent to child. To get to the point, there are two options on the table for the global application state management. Either using a state management library such as Redux or sticking to the React Context API already included in the React library, which can be easily combined with hooks to power it up.
Redux works for large applications, with dynamic data, and establishes a separation between the UI and the State Management logic, which is pretty good. However, you have to sacrifice code readability and time (especially at the beginning, because it requires an extensive setup time). So, this is a tradeoff, if you need to handle a pretty complex state with a lot of things to consider, such as a trading application, which needs to handle real-time data of stocks and at the same time let you draw in graphs and create visual representations of data, using a global state management tool such as Redux is mostly a must.
If the case described above does not apply to your project, you should probably go for the Context API, it will result in a more readable and maintainable code, less verbose, and without unnecessary complexity.
In any case, as we are considering a huge potential growth in your application, let’s say you start handling your global state with the Context API, and suddenly you need to go for a solution that is easier to handle with Redux, you can create a Store using the Context API and the useReducer hook, both provided by React itself (link here to a tutorial of how to do that).
When do we need to abstract logic?
This one is pretty simple, read The Clean Code by Robert Martin. Then you will see a lot of things that are pretty obvious, but you are not doing. For example, how many times does a code snippet need to be repeated after I have to abstract its logic? Spoiler alert, more than 2. Everything will be OK as long you follow two principles, DRY (Don’t Repeat Yourself) and KISS (Keep It Simple Silly). Be lazy, the less code you write, the fewer bugs you will find, and the less you will have to maintain and that’s the quality of life you will be gaining.