JavaScript на миллион строк: вступление в мир enterprise
Вы когда-нибудь видели JavaScript-проект, который весит больше, чем операционная система вашего ноутбука? Добро пожаловать в enterprise-разработку. Здесь нет места «быстренько написал на коленке» и «авось заработает». Здесь код живёт годами, его читают сотни разработчиков, а ошибка в одном npm-пакете может остановить работу целого банка.
Enterprise JavaScript — это не про фреймворки (хотя React или Angular там тоже есть). Это про культуру, процессы и архитектуру, которые позволяют управлять хаосом. Если вы хотите понять, чем отличается стартап от корпорации, или готовитесь к собеседованию в крупную компанию — эта статья для вас. Мы разберём ключевые особенности разработки на JavaScript в enterprise-среде, подкрепив всё реальными примерами кода.
Монорепозиторий vs. мультирепозиторий: битва титанов
В стартапе у вас, скорее всего, один репозиторий на GitHub. В enterprise всё сложнее. У вас может быть 50 микросервисов, общая библиотека UI-компонентов, отдельный пакет для утилит и ещё десяток легаси-проектов на jQuery.
Почему монорепозиторий побеждает?
Крупные компании (Google, Facebook, Microsoft) используют монорепозиторий. Один репозиторий, который содержит весь код компании. Это упрощает рефакторинг: вы меняете API в одном месте — и все проекты подтягивают изменения. Но есть нюанс: инструменты вроде Lerna, Nx или Turborepo становятся обязательными.
Пример структуры монорепозитория с помощью npm workspaces:
// package.json (корневой)
{
"name": "enterprise-monorepo",
"private": true,
"workspaces": [
"packages/*"
],
"scripts": {
"build": "npm run build --workspaces",
"test": "npm run test --workspaces"
}
}
Теперь у вас есть папка packages/, где лежат отдельные пакеты (например, ui-kit, api-client, core). Каждый пакет имеет свой package.json, но зависимости устанавливаются один раз в корень.
Совет: В enterprise никогда не используйте npm link вручную. Только инструменты монорепозитория, иначе вы рискуете получить «ад зависимостей».
TypeScript — не роскошь, а необходимость
Многие думают: «Зачем TypeScript, если я и так пишу без ошибок?». В enterprise с этим строго. Когда у вас кодовая база на 500 000 строк, а в команде 30 человек, динамическая типизация становится источником багов, которые могут уйти в прод.
TypeScript позволяет:
- Создать чёткие контракты между модулями (через интерфейсы).
- Упростить рефакторинг (IDE подскажет, что сломалось).
- Снизить количество runtime-ошибок.
Пример того, как enterprise-проект использует TypeScript для описания API:
// types/api.ts
export interface User {
id: string;
name: string;
email: string;
role: 'admin' | 'user' | 'moderator';
}
export interface ApiResponse<T> {
data: T;
status: number;
message: string;
}
// services/userService.ts
import { User, ApiResponse } from '../types/api';
export async function getUser(id: string): Promise<ApiResponse<User>> {
const response = await fetch(`/api/users/${id}`);
return response.json();
}
Обратите внимание: ApiResponse<T> — это дженерик. Он гарантирует, что ответ от сервера всегда будет иметь предсказуемую структуру. В enterprise это стандарт.
Архитектура: слои, модули и инверсия зависимостей
В стартапе вы можете положить всю логику в компонент React. В enterprise это приведёт к катастрофе. Здесь используется строгая архитектура:
- Слой представления (UI) — React/Vue/Angular компоненты, которые только рендерят данные.
- Слой приложения (Application) — хуки, стор, бизнес-логика (например, Redux Toolkit или Zustand).
- Слой сервисов (Services) — работа с API, localStorage, внешними библиотеками.
Главное правило: UI-компонент никогда не вызывает fetch напрямую. Он обращается к сервису через слой приложения.
Пример плохой практики (так делать нельзя в enterprise):
// ❌ Плохо: логика в компоненте
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(setUser);
}, [userId]);
return <div>{user?.name}</div>;
}
Пример хорошей практики:
// ✅ Хорошо: разделение ответственности
// services/userService.ts
export const userService = {
getById: (id: string) => fetch(`/api/users/${id}`).then(res => res.json()),
};
// hooks/useUser.ts
import { useState, useEffect } from 'react';
import { userService } from '../services/userService';
export function useUser(userId: string) {
const [user, setUser] = useState(null);
useEffect(() => {
userService.getById(userId).then(setUser);
}, [userId]);
return user;
}
// components/UserProfile.tsx
function UserProfile({ userId }) {
const user = useUser(userId);
return <div>{user?.name}</div>;
}
Теперь сервис можно легко заменить (например, при тестировании), а компонент остаётся «глупым».
Тестирование: три уровня защиты
В enterprise без тестов вас просто не пустят в прод. Три уровня — это стандарт:
- Unit-тесты (Jest, Vitest) — тестируют отдельные функции и сервисы.
- Интеграционные тесты (Testing Library) — проверяют взаимодействие компонентов с сервисами.
- E2E-тесты (Playwright, Cypress) — проверяют критические пользовательские сценарии.
Пример unit-теста для сервиса с моком fetch:
// __tests__/userService.test.ts
import { userService } from '../services/userService';
// Мокаем глобальный fetch
global.fetch = jest.fn();
describe('userService', () => {
afterEach(() => {
jest.clearAllMocks();
});
it('should return user data', async () => {
const mockUser = { id: '1', name: 'Alice' };
(fetch as jest.Mock).mockResolvedValue({
json: () => Promise.resolve(mockUser),
});
const result = await userService.getById('1');
expect(result).toEqual(mockUser);
expect(fetch).toHaveBeenCalledWith('/api/users/1');
});
});
В enterprise-проекте покрытие кода тестами обычно требует не менее 80% (а для критических модулей — 100%).
CI/CD и код-ревью: как код попадает в прод
Вы нап