Maki Frontend Architecture: go far for less (episode 1)

Nicolas Grégoire
makipeople
Published in
6 min readMay 3, 2022

Hi, we are Nicolas Grégoire & Benoit Senechal, Software Engineers at Maki. In this article, we will explain which principles drove our decisions to architecture Maki’s frontend MVP. If you are to start a new application, you could be interested about how we made technical choices, and how we kept code simple without compromising user and developer experience.

The white sheet : where are we going ?

Starting a codebase from scratch can be a huge headache. If you make the wrong decisions, it can cost your team a lot of energy to rewrite problematic parts. One of the biggest challenges you could face is complexity. Not the complexity related to your product, but the one introduced by your initial decisions.

  • How to keep a clear mental model of your product, of your codebase, while it’s growing every day?
  • How to settle an environment where your team can move fast on structure, and focus on your product implementation?
  • How to onboard new engineers with a little effort?
  • How to avoid multiple code patterns into your application?
  • And more generally, how to avoid your code to turn into a Pisa Tower?

What caused the Pisa Tower to end up like this? The foundations. What should you take care of first? You already got the answer.

By foundations, we don’t only mean the technical aspect (libraries), but also also include our expectations that you could call ‘developer experience’.

Remember your previous experiences on 1 year+ old codebases? Remember how frustrated you ended up and what you would have done differently? But you had to cope with it because of time constraints?

When we founded @makipeople, we had this unique chance to get at the stage where we asked ourselves: what should we expect from our codebase? After a few discussions and brainstorming, the following principles drove our rational for the technical aspect:

  • Organize code in a clear & scalable way
  • Enforce pages / views persistency, anytime we can
  • Ship fast
  • Write as less code as possible
  • Keep things simple, especially the data flow
  • Contain technical debt (not having debt is an utopia)

Once we defined what we wanted, we defined how we would implement it.

Easy as 1, 2, 3 : react, react-router, react-query

While this tryptic sounds too familiar, it gets more useful and robust than it looks.

Use the platform (that’s what they say)

You may have witnessed the rise of remix , the web framework developed by the react-router team (Michael Jackson & Ryan Florence)

Their motto, use the platform was a real inspiration for us.

It had an impact on the way we design our code, as it validated one thought that grew bigger and bigger after years of development. By platform, we should not only consider HTML and data HTTP requests, but also the browser itself.

We chose to focus on the url on top of everything, and built our views around this idea. react-router is the heart of our app.

Another great inspiration comes from react-query official site, when they describe their library as a backend state manager for client apps. You would remove many responsibilities and computations from the client by just applying this principle. Sounds good.

All right, with those 2 pillars, we are ready to build our product.

The following steps will explain the mental model of Maki’s architecture, using super simple concepts.

The code organization

The way we organize the code is directly impacted by the url.

Let’s start with this classic project folder structure, and put focus on the pages folder as it’s the most structuring part of the app.

/components  -- business components (related to business logic / domain)
-- UI components (the ones your design system cannot provide)
-- layouts
/design -- design system / css reset / global styles
/hooks -- global shared hooks
/i18n -- translation files
/pages -- pages folder (see details below)
/page1
/subpage1
...
/page2
...
/services -- graphql / ...
/utils -- utils

It has to fulfil the 3 following constraints:

  • consistency: each page folder path is mapped after its URL path
  • scalability: new pages should be added without adding complexity to the hierarchy
  • isolation: a page is responsible for its own logic and concerns, and can be removed or moved with a minimum effort

As a result, the following URL

www.makiepeople.com/assesments/results/score

would be folded this way

/pages      -- pages folder (see details below)
/assessments
/results
/score

This straight URL mapping is extremely useful. Your product organization is consistent, the hierarchy is clear, wherever you use the app, or you browse its code. And it obviously defines your routes.

Thanks to this article from Johannes Kettman, we realized that we are at the growth stage. That said, let us give you some advice: start at the growth organization immediately, the previous stages just don’t scale, and they would lead you to bad architecture decisions.

react-router as the cornerstone, react-query as the provider

Now that our structure is defined, let’s focus on a page, and more specifically on the last constraint of a page: isolation. In the example above, we used a static route, but in real life, many of your routes would contain parameters in order to display specific content.

Let’s focus on the user details to figure out how a Maki page works in isolation

settings/users/user/{userId}

export const User:React.FC = () => {
// react-router
const { userId } = useParams<{ userId: string}>()
// react-query
const { data } = useQuery(
["user", userId],
async () => {
const { user } = await request(
endpoint,
gql`
query {
users(id: ${userId}) {
name
email
}
}
`
);
return user;
},
{ enabled: !!userId }
);
return (
<div>
<div>{user.name}</div>
<div>{user.email}</div>
</div>
)
}

Still, there’s nothing new or special here if you only consider implementation. But there is another way to frame it from an architectural point of view.

The URL defines the page location, hierarchy

The URL defines the data entry point: userId

The page gets its data through react-query, thanks to the URL param

Our learnings

For many years now, the community used to think about an application’s model (call it state, store) as the shared data you can query or mutate within your code.

We have another point of view.

The data you fetch is just a side effect of your location in the browser, and the real source of truth comes from your browser’s location.

Think about the URL as your model, think about how it drives the content, and how much impact it has on your product.

The URL is your model, or your entry point for a page data if you prefer.

You may object that we still need a state manager to put all the pieces working together. And you are right. But once again, we framed the problem in a different way, which we will tell you in episode 2 of this series.

We will explain how to move responsibilities to the backend, and how it will help you to keep pieces isolated and write less code (still)

As a conclusion, how does it live after the constraints we initially aimed for?

  • Organize code in a clear & scalable way ✅
  • Enforce pages/views persistency, anytime we can ✅
  • Ship fast ✅
  • Write as less code as possible ✅
  • Keep things simple, especially the data flow ✅
  • Contain technical debt (not having debt is an utopia) ✅
  • Frontend engineers at Maki are happy so far ⭐

Thanks for reading and stay tune for episode 2 of our Frontend Architecture journey!

Nico & Benoit.

--

--