Building Inventa: Frontend Stack

Soter Padua
Building Inventa
Published in
10 min readApr 19, 2023
Photo by Patrick Tomasso on Unsplash

As a software-reliant company, we understand the importance of selecting the right tools and technologies for our projects. With so many options available, we’ve carefully chosen our frontend stack based on their unique benefits and ability to help us build robust and reliable frontend applications quickly and efficiently. This article aims to present the challenges, specific requirements, and trade-offs that we considered when defining our frontend stack.

Past:

At the moment I’ve joined the company (August 2022), we had some different ways of doing/deploying frontend applications:

This fragmentation made every team a universe in itself, each doing things its own way and solving the same problems many times over.

As a first investigation, I’ve attempted to identify the cons from our current usage of NextJS (with AWS Amplify), mainly because I believe that NextJS is a good tool to solving the kind of frontend problems that we have here at Inventa.

Some of the issues, at the time, that this approach involving NextJS and AWS Amplify presented:

  • Next.js major version at the time (12) wasn’t supported so we were locked into 11.x.x
  • Amplify creates all the AWS resources required for the deploy in a automatic fashion, the problem with that is managing all this resources was not trivial, some of it wasn’t ‘visible’ on AWS Console, neither clearly specified on billing section.
  • At the time we’re doing some restructuring of our AWS management principles in order to optimize the billing and provide more uniformity to our resources and its configurations. This context also pushed us away from keeping Amplify as a solution internally.
  • Also regarding the automatic management of AWS resources, we’ve had problems when trying to fine-tune the Cloudfront distribution invalidations upon deploying ( resulting in a wasteful deploy process from the perspective of billing).
  • Required a lot of webpack-specific customizations

After taking the decision to abandon AWS Amplify as the default “deploy stack” we started reviewing which components of our current stack made sense to be kept and formalized and which should be, at least, not recommended.

Engineering Design Review Process:

At the company we have a design review process to solicit feedback from peers and domain experts on your proposed changes to the codebase in pursuit of some objective.

When is a design review necessary?

  • A new project, service, or component is being created
  • Anytime a data contract change is proposed for an established service
  • Any significant change to an existing design or deployment
  • Services/Patterns that could be used as cross-company approach

The result of this process is a series of documents and discussions structured in a way to enable all engineers to quickly grasps what’s being discussed and, if applicable, a high-level of implementation details.

Given that we were considering to change the default frontend stack for the company, this process was required to make sure that the solution was appropriated.

Throughout this article we’ll use the material from this process to present a what was researched.

Present/Future:

We’re in a continuous process of improvement so what follows is a description of one of those iteration processes.

Stack Core

One of the kept frontend technologies is Next.js, a React framework that offers complex features and provides ‘missing’ React features in a well-defined API. Next.js comes with a built-in router that is easy to use and helps maintain uniformity in the project’s folder structure. Additionally, Next.js offers multiple rendering strategies, including Static Generation, Server-Side Rendering, Incremental Static Regeneration, and Client-Side Rendering.

This flexibility allows us to choose the best rendering strategy for each project and ensures our code is well-documented and widely used. Next.js also allows for the easy creation of BFFs (using the `/api` route) which reduces the necessity of extra logic in a different project to ensure that we’ll be able to interact with a specific service.

(We also support static deployed web applications but they aren’t the focus of this article).

Internationalization

To streamline our internationalization efforts and ensure our applications are ready to be accessible to users that speak other languages, we use react-intl, a library related to internationalization. It helps us build applications that can be easily translated into different languages. It does that by using messages.

Simple message definition:

const presentationMessage = {
description: 'A simple presentation message', // Description should be a string literal
defaultMessage: 'My name is {name}', // Message should be a string literal
}

Simple message usage in React:

// Hook
import { useIntl } from 'react-intl'

const Component = () => {
const intl = useIntl();

return (<div>{intl.formatMessage(presentationMessage)}</div>)

}
// Component
import { FormattedMessage } from 'react-intl'

const Component = () => {
const intl = useIntl();

return (<FormattedMessage {...presentationMessage} />)
}

Styling

For building responsive and accessible user interfaces, we rely on ChakraUI, a flexible and easy-to-use component library. ChakraUI offers a range of pre-built components that can be easily customized to fit our specific needs, making it a valuable tool for building complex interfaces quickly and efficiently. When deciding we’ve compared it with Material UI, Chakra UI offers developers more flexibility in modifying CSS classes of exported components and layouts, which can save time and effort in customizing interfaces. On the other hand, Material UI provides a wider range of pre-styled UI components and may be more suitable for projects that do not require extensive custom styling. For our context as a company, the ability to do more custom styling but still keeping some sort of theme structure is really valuable.

Another option considered was Tailwind CSS (we have projects that used tailwind and still do); Chakra UI provides a more comprehensive solution, offering not only styling benefits but also a library of carefully created React components that takes care of all the details, such as accessibility, keyboard navigation, and component composition. While Tailwind CSS provides atomic CSS classes to help style components, Chakra UI comes with an intuitive, prop-based model of styling components, making it easier to learn and understand. We’re still maintaining the internal projects that use Tailwind and constantly learning its limitations and advantages, but for now the decision is to avoid it in new projects.

In a goldilocks-like situation, it seemed like MaterialUI was too much (in various senses but mainly: bundle size, opinionated components), and Tailwind CSS was too little (in the sense of requiring too much time/code to create simple React components that matched our current design). ChakraUI provided a more direct approach to do the necessary modifications without defining too hardly the component style.

To style and manage our React components, we use Emotion (@emotion/styled, @emotion/react) — a library designed for writing CSS styles with JavaScript. Emotion provides powerful and predictable style composition, it uses the same API as `styled-components` (pure CSS inside a template string syntax that gets parsed by a utility function). Another point for `emotion` was the fact that ChakraUI uses it under the hood.

import styled from '@emotion/styled'

const Button = styled.button`
padding: 32px;
background-color: hotpink;
font-size: 24px;
border-radius: 4px;
color: black;
font-weight: bold;

&:hover {
color: white;
}
`

We also use Typescript to add type safety to our projects, which helps us catch errors earlier in the development process and improve our overall development experience. With Typescript, we get better IDE support, making it easier to work with complex codebases. What is currently ongoing is a complete transition to TS only, as we have a legacy codebase where the majority of code is in JavaScript, there is the necessity to support less strict linter rules to enable this hybrid environment.

Testing

And when it comes to testing, we’ve chosen Jest and React Testing Library as our preferred testing frameworks, one more focused on unit tests and the other on testing the interaction with React components. Jest offers a range of features that make it easy to write and maintain tests, such as a simple and intuitive API and code coverage analysis.

Given the need for robust and reliable frontend applications, it is important to use a tool that can ensure that the applications are working as expected, for that we are using Cypress as our end-to-end testing tool in order to write tests that simulate user interactions and provide features that can help us to deliver fast and reliable tests such as:

Data Fetching

Finally, we use Axios for handling HTTP requests in our projects. We’ve considered using the new `fetch` API available for Node 18 (the majority of our projects were using it), but the fact that it was experimental (Stability 1) made us choose a more consolidated solution.

Axios offers a unified API that can be used across both Node.js and browser environments, allowing us to easily reuse code regardless of the JS runtime. It’s a powerful tool that abstracts some of the more tedious aspects of other implementations, such as `await data.json()` from XMLHttpRequests. This makes it easy for us to extract parts of the logic to its own packages using the instance mode, creating “SDK-like” structures.

During the research about how to architect isolated(or at least easy to extract) solutions using Axios some options were considered:

Option 1

Overview

  • Create a function for every endpoint that will be consumed in the application.
  • Avoid using SSR to simplify the flow and implement the applications on the client side only.
Option 1

Pros

  • Simpler approach.
  • Avoids having to know anything about SSR.
  • Provides more flexibility for the engineer.

Cons and Limitations

  • It does not scale so well if we have a bigger quantity of endpoints.
  • Each endpoint is an independent part of the code.
  • Specific logic that is shared cross API needs to be repeated, request timeout, auth logic, for example.
  • Doesn’t leverage SSR.

Option 2

Overview

  • Map the APIs used on the application using axios’ instance mode
  • This way is like we have mini SDKs for the APIs and later on, is easy to extract this logic to a separated package and re-use it across projects.
  • This approach can be used in conjunction with Next’s SSR for more simple pages.
Option 2

Pros

  • Easy to re-use.
  • Enables us to create sensible defaults for all the APIs, such as error reporting, cache rules, custom headers.
  • Easy to share API configurations across endpoints, such as timeout, authentication, custom redirect logic, status code validation.

Cons and Limitations

  • More overhead when compared with Option 1
  • Makes more sense when we have multiple endpoints to implement at once, implementing a single endpoint with this logic would be overkill.

Option 3

Overview

  • This approach builds upon the second option, with a slight change.
  • Use react-query as a wrapper around axios, providing cache manager behavior and enabling SSR data fetching to be simpler and consistent across client and server.
Option 3

Pros

  • leverages Next’s SSR.
  • added cache layer that is SSR aware helps to fine-tune cache behavior.
  • prevents unnecessary refetches, react-query manages fetch calls and tracks information staleness.

Cons and Limitations

  • adds an overhead that can be confusing or misused introducing bugs.
  • usage is not trivial
  • as we would be promoting react-query to a core dependency, there would need to be extensive internal documentation to support the teams’ ramp-up when using this dependency.

As a default option we’ve chose the second option, keeping the third as a special case to be studied and formalized later on. The main reason behind this decision was a trade-off, we’ve recognized the value of the 3rd option but also knew it to be more complex to implement and maintain at our current stage.

Conclusion

At the end of the day, our frontend stack choices are critical to the success of our projects. By carefully selecting the right tools for the job, we’re able to deliver high-quality software that meets the needs of our clients and end-users alike. We’re proud of our frontend stack choices, and we’re confident that they will continue to help us build robust and reliable applications for years to come.

Did you like the challenge? How about joining us!? We are looking for software engineers who enjoy the state-of-the-art platform and application development, and want to help build a very reliable, observable, and scalable product in Inventa! Hit here to apply and, if you have any questions, don’t hesitate to call me on LinkedIn.

--

--