Carbonteq
Best Practices/Frontend/Architecture

Overview

A comprehensive guide to frontend architecture and design patterns.

Hey there! So, you're about to dive into building a frontend application at Carbonteq, or maybe you're just looking for some battle-tested ways to organize your React project. You're in the right place!

This guide will walk you through our approach to frontend architecture. It's not a rigid set of rules, but a collection of patterns and practices that have helped us build scalable and maintainable applications. Our goal is to create a clear separation of concerns, making the codebase easy to navigate and reason about.

The 'Why' Behind the 'What': Our Architectural Principles

Before we dive into folders and files, let's talk about the 'why'. This structure isn't just for show. It's built on a few key principles that help us write better, more maintainable code. Understanding these upfront will make the rest of the guide click into place.

Separation of Concerns

Each part of our application has a single, well-defined job. This makes our codebase easier to understand, test, and debug.

  • Pages: Handle routing and page-level orchestration.
  • Containers: Manage state and business logic, often by using co-located hooks.
  • Components: Focus on presentation and user interaction.
  • Services: Handle external API communication.
  • Hooks: Encapsulate reusable logic.
  • Context: Provide state to a tree of components.

Co-location

We believe that related code should live together. If a piece of state, logic, or even a style is only used by one component, it should live in the same directory as that component. This principle is key to keeping our features modular and self-contained.

Modularity

By breaking our application into smaller, independent, and interchangeable modules, we can develop and maintain features with less friction.

  • Components are designed to be composable and reusable.
  • Business logic is extracted into custom hooks.
  • Features can be developed in isolation and then composed together.

With these principles in mind, let's build up our project structure step-by-step, just like you would when starting a new application.

The Foundation: Starting with Pages

Every web app starts with what the user sees. In React, we often think in terms of Pages. These are your top-level components that correspond to different routes in your application, like a homepage, a login page, or a dashboard. It's a good practice to keep them organized in a dedicated directory.

Home/
Login/
Dashboard/

Building the UI: The Components Directory

Of course, pages are made up of smaller, reusable pieces of UI. This is where the components directory comes in. At first, you might just throw all your components in there. But as your app grows, that components folder can get messy pretty quickly.

Bringing Order to Chaos: Primitives vs. Composites

To keep things tidy and understandable, we split our components into two categories: Primitives and Composites.

Primitives are the fundamental building blocks of our UI. They are either native HTML elements or base components from a UI library like Ant Design or Material UI. Think of Button, Input, or Card. Most primitive components are placed flat inside the primitives directory.

Sometimes, you'll have a variation of a primitive that's used in several places, but it doesn't quite warrant being a new "variant". A good example is a button with a top border. Its primitive (Button) exists, but creating a new variant for it feels like overkill. In such cases, we create a derived primitive like TopBorderButton and place it in a derived folder inside primitives.

Composites are components built by combining one or more primitives. They encapsulate more specific business logic and are tailored to your application's features. We further organize composites into:

  1. common: Components that are reused across multiple features of the application (e.g., a Header or Footer).
  2. Feature-specific folders: Components that belong to a particular feature, like a TodoItem or TodoList in a todo application.

Here's how that component structure might look for a todo app:

Button/
Input/
Checkbox/
TopBorderButton/
Header/
TodoItem/
TodoList/
AddTodoForm/

This structure helps us maintain a clear separation between generic, reusable UI elements and feature-specific, complex components.

Making Components Smart: Containers and Co-located Logic

So far, our components are purely presentational. They look great, but they're "dumb" - they just render UI based on the props they receive. So, where does the data and logic come from?

Enter Containers! Containers are higher-level components that act as a bridge between your application's state and your presentational components. They are responsible for:

  • Handling data fetching
  • Managing state and logic
  • Passing data and functions down to the presentational components

This container/presentational pattern is powerful because it cleanly separates responsibilities. Your UI components stay simple and reusable, while your containers handle the complex logic.

But where should the logic itself live? Remember our principle of co-location? We apply it here. If a piece of logic is only used by a single container, it should live right alongside it as a custom hook.

Let's make this more concrete with our todo list feature.

The Presentational Components

These components only care about rendering the UI and reporting back to their parent when the user does something. They receive all data and functions as props.

// src/components/composites/todo/TodoList.tsx
// This component renders the list and passes the toggle handler to each item.
export const TodoList = ({ todos, onToggleTodo }) => {
return (
<ul>
{todos.map(todo => (
<li
key={todo.id}
onClick={() => onToggleTodo(todo.id)}
style={{ textDecoration: todo.completed ? 'line-through' : 'none', cursor: 'pointer' }}
>
{todo.text}
</li>
))}
</ul>
);
};
// src/components/composites/todo/AddTodoForm.tsx
// A simple form to add a new todo.
export const AddTodoForm = ({ onAddTodo }) => {
// ... form logic to get the text ...
// on submit, it calls the function from its props:
// onAddTodo(text);
};

The Container and its Co-located Hook

The container becomes the "brain" of the operation. It uses our custom, co-located hook to get the state and logic, and then passes them down to the dumb presentational components.

Here's what the TodoListContainer's directory looks like, with the hook living right beside the component that uses it:

TodoListContainer.tsx
useTodoList.ts
index.ts

The container itself is now incredibly lean. It's only job is to orchestrate the pieces.

// src/containers/TodoListContainer/TodoListContainer.tsx
import { TodoList } from '@/components/composites/todo/TodoList';
import { AddTodoForm } from '@/components/composites/todo/AddTodoForm';
import { useTodoList } from './useTodoList';
// A mock function to simulate fetching initial data.
const fetchTodos = () => Promise.resolve([
{ id: 1, text: 'Learn about containers', completed: true },
{ id: 2, text: 'Enrich the example', completed: false }
]);
export const TodoListContainer = () => {
// The container gets the state and the updater functions from the hook.
const { todos, addTodo, toggleTodo } = useTodoList(fetchTodos);
// It then passes them down to the presentational components.
return (
<div>
<AddTodoForm onAddTodo={addTodo} />
<TodoList todos={todos} onToggleTodo={toggleTodo} />
</div>
);
};

All the state management and business logic is encapsulated in the co-located hook.

// src/containers/TodoListContainer/useTodoList.ts
import { useState, useEffect } from 'react';
interface Todo {
id: number;
text: string;
completed: boolean;
}
// The hook now encapsulates fetching, adding, and toggling todos.
export const useTodoList = (fetcher) => {
const [todos, setTodos] = useState<Todo[]>([]);
useEffect(() => {
fetcher().then(initialTodos => {
setTodos(initialTodos);
});
}, [fetcher]);
const addTodo = (text: string) => {
const newTodo: Todo = {
id: Date.now(),
text,
completed: false,
};
// In a real app, you'd likely call an API here as well.
setTodos(prevTodos => [...prevTodos, newTodo]);
};
const toggleTodo = (id: number) => {
setTodos(prevTodos =>
prevTodos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
};
return { todos, addTodo, toggleTodo };
};

This pattern of co-locating logic with a custom hook keeps our container components clean and focused, while making the logic itself easy to find, understand, and test. The same principle applies to React Context: if a context is only needed by a small subtree of components, it should live with them in their feature folder.

Architecture Breakdown

Scroll through the interactive visualization below to understand how data flows through our frontend architecture layers:

Component Hierarchy

PageRoutes & Layouts
ContainerData & State
CompositeBusiness Components
PrimitiveUI Components
Scroll Progress:
0%

Sales Dashboard

Fakebooks

OVERDUE
$10,800
DUE SOON
$62,000
INVOICE LIST
Santa Monica
1995
$10,800
OVERDUE
Stankonia
2000
$8,000
DUE TODAY
Ocean Avenue
2003
$9,500
PAID
Stankonia

DUE TODAY • INVOICED 10/31/2000

$8,000
Custom$2,000
Net Total$8,000
Page
Routes & Layouts
Container
Data & State
Composite
Business Components
Primitive
UI Components

Data Flow

The data flows in a predictable, top-down pattern:

  1. Pages compose Containers.
  2. Containers use services from shared/services to fetch data.
  3. Containers use co-located or shared Hooks for state management.
  4. Containers pass data to Components for presentation.
  5. Components are built from Primitives and Composites.

The shared Directory: A Home for Truly Reusable Code

Not all code is specific to a single feature. Some things need to be accessed from many different, unrelated parts of the application. For this, we have the shared directory. It's the designated home for any code that is genuinely global and reusable.

Here's a breakdown of what you'll find inside:

AuthContext.tsx
ThemeContext.tsx
useAuth.ts
useForm.ts
api.ts
auth.ts
helpers.ts
constants.ts
  • context: For state that is truly global and needs to be accessed by most of the application (e.g., AuthContext, ThemeContext).
  • hooks: For hooks that contain logic genuinely reusable across many unrelated components (e.g., a generic useForm for handling form state or useMediaQuery for checking screen sizes).
  • services: We centralize all our external API communication here. These services abstract away the details of API calls (e.g., using Axios or Fetch), handle data transformation, and provide type-safe functions for our containers to call. This creates a clean boundary; if you ever need to change an API endpoint, you only have to update it in one place.
  • utils: A home for pure, deterministic helper functions and constants that can be used anywhere in the application (e.g., date formatters, validation functions, or application-wide constants).

Managing Static Assets

Finally, our application needs a place for static files like images, fonts, and global stylesheets. The assets directory serves this purpose.

images/
styles/
fonts/

Phew, that was a lot! We've journeyed from high-level principles to a sophisticated structure involving pages, components, containers, and co-located logic. Now, let's put it all together and see the complete directory structure.

images/
styles/
TopBorderButton/
Button/
Input/
Card/
Typography/
Header/
Footer/
TodoItem/
TodoList/
AddTodoForm/
TodoListContainer.tsx
useTodoList.ts
index.ts
Home/
Login/
Dashboard/
AuthContext.tsx
ThemeContext.tsx
useAuth.ts
useForm.ts
api.ts
auth.ts
helpers.ts
constants.ts
package.json
tsconfig.json

As you can see, this structure gives every part of our application a clear and logical home, guided by our principles of co-location and separation of concerns.

Resources