Imagine: you are starting a new project. Enthusiasm is at its peak, and the code is written quickly. After a couple of months, you've already forgotten why you need that strange function in utils.js. Six months later, adding a new feature turns into a quest. A year later, the project turns into a swamp, where every change can break everything.
Sounds familiar? This is a classic result of the lack of a well-thought-out architecture. Let's figure out how to build frontend projects that will live and evolve for years.

Why do projects die?
Before we talk about solutions, let's understand the main reasons for the death of projects:
Spaghetti code. Everything is connected to everything. A change in one place breaks the other three components.
Lack of structure. Files are scattered chaotically. Finding the right component is an archaeological expedition.
Duplication of logic. The same functionality is implemented in five different places in five different ways.
No documentation. Even you yourself don't remember how your code works in a month.
Technical debt. "I'll fix it later" turns into "I'll never fix it."
Foundation: the correct folder structure
Good architecture begins with file organization. Here is a proven structure for most projects:
src/
├── components/ # Reusable components
│ ├── ui/ # Basic UI elements (buttons, inputs)
│ ├── layout/ # Layout components (header, footer)
│ └── features/ # Business components
├── pages/ # Application pages
├── services/ # Working with the API
├── store/ # Global state (Vuex, Redux, Pinia)
├── utils/ # Auxiliary functions
├── hooks/ # Custom hooks (for React)
├── composables/ # Composite functions (for Vue)
├── types/ # TypeScript types
├── constants/ # Application constants
└── assets/ # Static resourcesPrinciple: each folder is responsible for one area of responsibility. When you need a UI component, you go to components/ui. If you need a function to work with the API, go to services.
Principle of sole responsibility
Each module should do one thing, but do it well.
Bad:
// UserCard.js - does everything at once
function UserCard({ userId }) {
const [user, setUser] = useState(null);
// Data collection
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(setUser);
}, [userId]);
// Validation
const isValid = user?.email && user?.name;
// Formatting
const formattedDate = new Date(user?.created).toLocaleDateString();
// And more rendering...
return <div>...</div>;
}Good:
// services/userService.js
export const fetchUser = async (userId) => {
const response = await fetch(`/api/users/${userId}`);
return response.json();
};
// utils/validation.js
export const validateUser = (user) => {
return user?.email && user?.name;
};
// utils/dateFormatter.js
export const formatDate = (date) => {
return new Date(date).toLocaleDateString();
};
// components/UserCard.js - display only
function UserCard({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId]);
if (!validateUser(user)) return null;
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
<span>{formatDate(user.created)}</span>
</div>
);
}Now each function is tested separately, used in different places, and easily modified.
Abstraction layers: divide and conquer
Good architecture is built in layers, like an onion:
1. Data Layer
Responsible for receiving and sending data. All API requests live here.
// services/api/userApi.js
const API_BASE = 'https://api.example.com';
export const userApi = {
getUser: (id) => fetch(`${API_BASE}/users/${id}`).then(r => r.json()),
updateUser: (id, data) => fetch(`${API_BASE}/users/${id}`, {
method: 'PUT',
body: JSON.stringify(data)
}),
deleteUser: (id) => fetch(`${API_BASE}/users/${id}`, { method: 'DELETE' })
};2. Business Logic Layer
Processes data, applies business logic rules.
// services/userService.js
import { userApi } from './api/userApi';
export const userService = {
async getActiveUsers() {
const users = await userApi.getUsers();
return users.filter(user => user.isActive);
},
async promoteToAdmin(userId) {
const user = await userApi.getUser(userId);
if (!user.email.endsWith('@company.com')) {
throw new Error('Only company emails can be admins');
}
return userApi.updateUser(userId, { role: 'admin' });
}
};3. State Layer
Manages the global state of the application.
// store/userStore.js (Pinia/Vue)
export const useUserStore = defineStore('user', {
state: () => ({
currentUser: null,
users: []
}),
actions: {
async loadUser(id) {
this.currentUser = await userService.getUser(id);
}
}
});4. Presentation Layer
Components that display data to the user.
// components/UserProfile.vue
<script setup>
import { useUserStore } from '@/store/userStore';
const userStore = useUserStore();
const { currentUser } = storeToRefs(userStore);
onMounted(() => {
userStore.loadUser(route.params.id);
});
</script>
<template>
<div v-if="currentUser">
<h1>{{ currentUser.name }}</h1>
<p>{{ currentUser.email }}</p>
</div>
</template>Golden rule: upper layers can use lower ones, but not vice versa. Components use store, store uses services, but services never import components.

Composition instead of inheritance
In the modern front end, composition wins over inheritance. Instead of giant base classes, create small reusable functions.
Example with React hooks:
// hooks/useLocalStorage.js
export function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : initialValue;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
}
// hooks/useDebounce.js
export function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(handler);
}, [value, delay]);
return debouncedValue;
}
// Use
function SearchComponent() {
const [query, setQuery] = useLocalStorage('searchQuery', '');
const debouncedQuery = useDebounce(query, 500);
// Now you have a search with debounce and saving in localStorage
useEffect(() => {
if (debouncedQuery) {
searchApi(debouncedQuery);
}
}, [debouncedQuery]);
}Typification is your best friend
TypeScript may seem superfluous for beginners, but it will save the project from many problems.
// types/user.ts
export interface User {
id: number;
name: string;
email: string;
role: 'user' | 'admin' | 'moderator';
createdAt: Date;
}
export interface ApiResponse<T> {
data: T;
error?: string;
status: number;
}
// services/userService.ts
export async function getUser(id: number): Promise<ApiResponse<User>> {
const response = await fetch(`/api/users/${id}`);
return response.json();
}Now the IDE will prompt you for available fields, and TypeScript will catch errors at the development stage, not in production.
Configuration and constants
Do not scatter magic numbers and strings around the code. Collect them in one place.
// constants/config.js
export const API_CONFIG = {
BASE_URL: process.env.VITE_API_URL || 'https://api.example.com',
TIMEOUT: 5000,
RETRY_ATTEMPTS: 3
};
export const UI_CONSTANTS = {
ITEMS_PER_PAGE: 20,
DEBOUNCE_DELAY: 300,
TOAST_DURATION: 3000
};
export const ROUTES = {
HOME: '/',
PROFILE: '/profile',
SETTINGS: '/settings'
};When you need to change the number of items on the page, you will know where to do it.
Error handling
A systematic approach to errors is a sign of a mature architecture.
// utils/errorHandler.js
export class ApiError extends Error {
constructor(message, status, data) {
super(message);
this.status = status;
this.data = data;
}
}
export async function handleApiCall(apiFunction) {
try {
return await apiFunction();
} catch (error) {
if (error instanceof ApiError) {
// We show the user a clear message
toast.error(error.message);
// Logging for developers
console.error('API Error:', error.status, error.data);
} else {
// Unexpected error
toast.error('Something went wrong. Please try again later.');
console.error('Unexpected error:', error);
}
throw error;
}
}
// Use
async function loadUserData(userId) {
await handleApiCall(async () => {
const user = await userApi.getUser(userId);
if (!user) {
throw new ApiError('User not found', 404);
}
return user;
});
}Document architectural solutions
Create a file ARCHITECTURE.md in the project root:
# Project architecture
# # Structure
- `/components` - переиспользуемые компоненты
- `/pages` - страницы приложения
- `/services` - бизнес-логика и API
# # Agreements
- Компоненты именуются в PascalCase
- Утилиты и сервисы в camelCase
- Константы в SCREAMING_SNAKE_CASE
# # Layers
1. API Layer (services/api/)
2. Business Logic (services/)
3. State Management (store/)
4. UI Components (components/)
# # Important decisions
- Используем Pinia для состояния
- Axios для HTTP запросов
- День.js для работы с датамиCode review and linting
Configure ESLint and Prettier at once. This will prevent 90% of code readability problems.
// .eslintrc.js
module.exports = {
rules: {
'no-console': 'warn',
'no-unused-vars': 'error',
'complexity': ['error', 10], // Warns about complex functions
'max-lines-per-function': ['warn', 50]
}
};Architecture testing
Good architecture is easy to test.
// userService.test.js
import { userService } from './userService';
import { userApi } from './api/userApi';
jest.mock('./api/userApi');
test('promoteToAdmin rejects external emails', async () => {
userApi.getUser.mockResolvedValue({
id: 1,
email: 'external@gmail.com'
});
await expect(
userService.promoteToAdmin(1)
).rejects.toThrow('Only company emails');
});If your functions are difficult to test, it is a signal that the architecture is flawed.
Scaling: Feature-Based Structure
When the project grows, group the code by features, not by file types:
src/
├── features/
│ ├── auth/
│ │ ├── components/
│ │ ├── services/
│ │ ├── store/
│ │ └── types/
│ ├── products/
│ │ ├── components/
│ │ ├── services/
│ │ └── store/
│ └── cart/
│ ├── components/
│ └── store/
└── shared/ # Common components and utilitiesNow everything related to authorization is in one folder. Easy to find, easy to delete, easy to transfer to another developer.
Common mistakes of beginners!
Premature optimization
You don't need to build an architecture for a million users right away. Start with a simple but scalable solution.
Excessive abstraction
If you only have one button, you don't need to create a system of five base classes for it.
Ignoring conventions
Use the common practices of your framework. Don't reinvent the wheel.
Lack of refactoring
Set aside time to improve the architecture. Technical debt accumulates unnoticed.
Practical advice.
Start with README. Describe how the project works before writing the code. This will make you think about architecture.
Refactor regularly. Set aside an hour a week to improve existing code.
Learn from others. Study open-source projects, see how they are organized.
Don't be afraid to redo. If the structure doesn't work, it's better to fix it now than to live with it for years.
Tools to help.
ESLint/Prettier — automatic code formatting
Husky — code check before commit
TypeScript - standardization for large projects
Storybook — development of components in isolation
Jest/Vitest — logic testing
Conclusion
Good architecture is not perfect code the first time. It is a systematic approach that allows the project to evolve. Start with a simple but logical structure. Follow the principle of single responsibility. Document important decisions. And most importantly, regularly review and improve your code.
In a year, you will thank yourself for every minute you invested in architecture. Your project will live, develop and bring joy, and will not turn into a spaghetti ball that is scary to touch.
This and much more can be learned in Codice — analyze everything in detail and consolidate it with practice and tasks. We teach not just to write code, but to build the right, scalable applications that will last for years.
And if you need support, we already have more than 2000 like-minded people in an active Telegram channel, where you can ask any question, discuss architectural solutions and get a review of your code from experienced developers.
Join the community where real professionals grow! 🚀
