coding_style_guide
Coding Style Guide: Повышение Качества и Снижение Багов
Ниже представлен сжатый набор правил и рекомендаций. Цель — установить единые стандарты, облегчить чтение и понимание кода, сократить регрессии и ускорить доставку новых фич.
1. Общие принципы
-
“Чистый Код” на первом месте.
- Каждая функция и компонент должны выполнять только одну задачу (Single Responsibility).
- Если код начинает дублироваться или разрастаться, задумайтесь о выносах в утилиты, хуки, общие модули.
-
Принцип KISS (“Keep It Simple, Stupid”).
- Предпочитайте понятную логику перед “избыточными” оптимизациями.
- Избегайте сложных “трюков”: проще перебирать несколько строк, чем ломать голову над непонятным циклом.
-
YAGNI (“You Ain’t Gonna Need It”).
- Не пишите код “на будущее”, если конкретная задача не требует расширяемости.
- Не создавайте сразу универсальный, параметризуемый алгоритм, если нужна простая реализация.
-
Лучшее время обнаружить баг — как можно раньше.
- Пишите тесты до или одновременно с фичей (TDD/аттестация юнит-тестами).
- Запускайте линтер/статический анализатор/тесты в CI на каждое изменение.
2. Общие Правила Именования
-
Файлы и директории:
- Компоненты и сервисы —
PascalCase(например,UserCard.jsx,UserService.js). - Утилиты, хуки и обычные модули —
camelCase(например,formatDate.js,useAddress.js,validators.js). - Конфиги и константы —
kebab-case(например,db-config.js).
- Компоненты и сервисы —
-
Переменные и функции:
- Переменные, функции и аргументы —
camelCase(например,calculateTotalPrice). - Константы (как правило, легко узнаваемые) —
UPPER_SNAKE_CASE(например,MAX_LOGIN_ATTEMPTS). - Интерфейсы, типы и enum—
PascalCase.
- Переменные, функции и аргументы —
-
React-компоненты:
- Всегда использовать
PascalCaseдля имен компонентов (например,UserList,OrderForm).
- Всегда использовать
-
CSS/стили:
- Для styled-components — размещать стили рядом с компонентом в том же файле. Выносить стили в отдельный файл имеет смысл только если они переиспользуются.
3. Стандарты Типизации
-
Не использовать
anyбез крайней необходимости.- Если тип сложно определить сразу, можно применить
mixed(Flow) илиunknown(TS) и со временем ужесточить проверку.
- Если тип сложно определить сразу, можно применить
-
Явные возвращаемые типы функций/методов.
- Всегда указывать
(): Promise<User>или(): numberвместо оставлять вывод компилятором. - Это улучшает читаемость, помогает избежать неожиданных
voidилиany.
- Всегда указывать
-
Использовать
readonlyдля неизменяемых полей.-
Например Flow:
type User {+id: string;name: string;email: string;} -
Например TS:
interface User {readonly id: string;name: string;email: string;}
-
-
Обобщения (Generics):
- При создании утилит или функций-обёрток (например,
fetchData<T>(url: string): Promise<T>). - Не создавать “лишних” обобщений, если конкретика понятна.
- При создании утилит или функций-обёрток (например,
-
Типы и интерфейсы:
- Flow: типы определяются ключевым словом type, в то время как interface предназначен для описания классов.
- TS: Для описания объектов, сущностей и DTO используйте
interface(например, интерфейсы для моделей, параметров функций, props компонентов). - Для объединений типов (union), пересечений (intersection), алиасов примитивов, кортежей и сложных/выведенных типов используйте
type. - Не смешивайте оба подхода в одном блоке кода без необходимости. Для единообразия придерживайтесь этого правила во всём проекте.
-
Enum vs Union Types:
-
Flow: enum поддерживаются в более поздних версиях и рекомендуются к использованию.
-
TS: При фиксированном наборе значений используйте
enumилиas const+ union строк:export enum UserRole {ADMIN = 'admin',USER = 'user',GUEST = 'guest'}// илиexport const UserRoles = ['admin', 'user', 'guest'] as const;export type UserRole = (typeof UserRoles)[number];
-
4. React: Компоненты и Архитектурные Стандарты
-
Функциональные компоненты + React Hooks.
- Не писать классовые компоненты (кроме случаев, когда это действительно оправдано, но чаще всего — избыточно).
- Использовать
useState,useEffectтолько там где это действительно необходимо. - При сложной логике — выносить в кастомные хуки.
-
Деление на “презентационные” и “контейнерные” компоненты (по необходимости).
- Внимание: анти-паттерн в Relay. В Relay данные запрашиваются там, где используются, что позволяет избежать мертвые запросы и запрос неиспользуемых данных.
- Презентационные: отвечают только за UI, принимают данные через props, не знают об источнике данных.
- Контейнерные: обёртывают логику получения данных (Redux, Context API, сервисы) и передают их презентационным.
-
Обязательное указание PropTypes через Flow или TypeScript.
-
Типизируйте все входные props:
type UserCardProps = {user: User;onClick: (id: string) => void;};const UserCard = ({ user, onClick }: UserCardProps) => { /*…*/ };
-
-
Селекторы и мемоизация:
- Используйте
useMemoосмотрительно и только когда это необходимо:- Для дорогостоящих вычислений, которые блокируют event loop более чем на 100мс
- Когда мемоизированное значение используется как зависимость в других хуках
- При передаче сложных объектов или массивов в качестве пропсов в мемоизированные дочерние компоненты
- Когда вычисление включает сложные преобразования или фильтрацию данных
- Избегайте преждевременной оптимизации - не используйте
useMemoдля простых вычислений или примитивных значений (string, number, boolean) - Помните, что сам
useMemoимеет свою стоимость - ему нужно хранить предыдущее значение и сравнивать зависимости - Используйте
useCallbackдля мемоизации функций в следующих случаях:- При передаче колбэков в мемоизированные дочерние компоненты (React.memo)
- Когда колбэк используется как зависимость в других хуках (useEffect, useMemo)
- Когда функция сложная для создания или содержит сложную логику
- Избегайте
useCallbackв случаях:- Простых обработчиков событий, которые не вызывают перерендер
- Функций, которые пересоздаются при каждом рендере, но не влияют на производительность
- Функций, которые используются только внутри компонента и не передаются через пропсы
- Помните, что сам
useCallbackимеет накладные расходы - ему нужно хранить функцию и сравнивать зависимости - Всегда включайте все зависимости, используемые внутри колбэка, в массив зависимостей
- Применяйте
React.memoв следующих сценариях:- Компоненты, которые часто получают одинаковые пропсы, но перерендериваются без необходимости
- Элементы списков в больших таблицах данных или виртуализированных списках
- Компоненты форм, которые часто обновляются, но не требуют перерендера своих соседей
- Компоненты, которые дорого рендерятся (сложные вычисления, тяжелые DOM-операции)
- Компоненты, которые рендерятся много раз в списке
- Избегайте
React.memoкогда:- Компонент простой и быстро рендерится
- Пропсы часто меняются
- Компонент используется только один или два раза
- Компонент должен перерендериваться при каждом обновлении родителя
- Помните, что
React.memoдобавляет свои накладные расходы на сравнение пропсов - Всегда включайте пользовательскую функцию сравнения, когда пропсы являются не мемоизированными объектами или массивами, чтобы предотвратить ненужные перерендеры
- Используйте
React.memoв комбинации сuseCallbackдля колбэк-пропсов
- Используйте
-
Работа с асинхронным кодом:
- Считайте, что любое API может упасть: планируйте fallback-логику заранее (например, “попробовать повторить” или “показать offline-режим”).
- Учитывайте количество параллельных запросов и используйте rate limit или throttling
-
Ключи (keys) в списках
- Никогда не использовать индекс массива в качестве
key, за исключением если данные статические. - Используйте уникальные идентификаторы (
item.id), чтобы React мог эффективно определять, что изменилось.
- Никогда не использовать индекс массива в качестве
-
Дробление React компонентов
Цель: Разбивать сложные компоненты на более мелкие, сфокусированные части для улучшения читаемости, тестируемости и переиспользования кода.
Когда дробить компонент:
- Размер: Когда компонент превышает 200-300 строк кода.
- Сложность: Когда компонент выполняет более одной основной функции.
- Переиспользование: Когда логика или UI-блоки повторяются в разных местах.
- Тестирование: Когда тестирование компонента становится сложным из-за множества ответственностей.
Стратегии дробления:
-
Выделение презентационных компонентов:
// ❌ Большой компонент со смешанной логикойconst UserProfile = ({ userId }) => {const [user, setUser] = useState(null);const [loading, setLoading] = useState(true);// 50+ строк логики получения данных...return (<div>{/* 100+ строк JSX для отображения профиля */}<div className="user-header"><img src={user.avatar} alt={user.name} /><h1>{user.name}</h1><p>{user.email}</p></div><div className="user-stats">{/* Сложная логика отображения статистики */}</div><div className="user-actions">{/* Множество кнопок и действий */}</div></div>);};// ✅ Разбитый на логические компонентыconst UserProfile = ({ userId }) => {const { user, loading, error } = useUser(userId);if (loading) return <LoadingSpinner />;if (error) return <ErrorMessage error={error} />;return (<div><UserHeader user={user} /><UserStats user={user} /><UserActions user={user} /></div>);};const UserHeader = ({ user }) => (<div className="user-header"><UserAvatar src={user.avatar} alt={user.name} /><UserInfo name={user.name} email={user.email} /></div>); -
Извлечение кастомных хуков:
// ❌ Логика состояния внутри компонентаconst OrderForm = () => {const [formData, setFormData] = useState({});const [errors, setErrors] = useState({});const [isSubmitting, setIsSubmitting] = useState(false);const validateForm = () => { /* validation logic */ };const handleSubmit = async () => { /* submit logic */ };const handleFieldChange = (field, value) => { /* change logic */ };// JSX...};// ✅ Логика вынесена в кастомный хукconst OrderForm = () => {const {formData,errors,isSubmitting,handleFieldChange,handleSubmit} = useOrderForm();return (<form onSubmit={handleSubmit}><OrderFormFieldsdata={formData}errors={errors}onChange={handleFieldChange}/><SubmitButton isLoading={isSubmitting} /></form>);}; -
Разделение по функциональности:
// ❌ Компонент с множественными ответственностямиconst Dashboard = () => {// Логика для графиков// Логика для таблиц// Логика для фильтров// Логика для экспортаreturn (<div>{/* Множество различных UI блоков */}</div>);};// ✅ Разделенный на специализированные компонентыconst Dashboard = () => {const { data, filters, setFilters } = useDashboardData();return (<div><DashboardFilters filters={filters} onChange={setFilters} /><DashboardCharts data={data} /><DashboardTable data={data} /><DashboardExport data={data} /></div>);};
Правила дробления:
-
Один компонент — одна ответственность:
- Каждый компонент должен иметь четко определенную роль.
- Если при описании компонента используется союз “и”, возможно, его стоит разделить.
-
Логические границы:
- Выделяйте компоненты по смысловым блокам (header, content, footer).
- Создавайте компоненты для переиспользуемых UI-паттернов.
-
Уровни абстракции:
// Высокий уровень - контейнерconst ReservationPage = () => (<div><ReservationForm /><ReservationSummary /></div>);// Средний уровень - логические блокиconst ReservationForm = () => (<form><VehicleSelector /><DateRangeSelector /><CustomerDetails /></form>);// Низкий уровень - конкретные поляconst VehicleSelector = () => (<div><VehicleTypeSelect /><VehicleList /></div>);
Антипаттерны (чего избегать):
-
Чрезмерное дробление:
// ❌ Слишком мелкое дроблениеconst UserName = ({ name }) => <span>{name}</span>;const UserEmail = ({ email }) => <span>{email}</span>;// ✅ Разумное объединениеconst UserInfo = ({ name, email }) => (<div><span className="name">{name}</span><span className="email">{email}</span></div>); -
Передача множества пропсов:
// ❌ Слишком много пропсовconst UserCard = ({id, name, email, avatar, role, isActive,createdAt, lastLogin, permissions, settings}) => { /* ... */ };// ✅ Группировка связанных данныхconst UserCard = ({ user, permissions, settings }) => { /* ... */ }; -
Разрыв логически связанного кода:
// ❌ Тесно связанная логика в разных компонентахconst ParentComponent = () => {const [selectedItem, setSelectedItem] = useState(null);return (<div><ItemList onSelect={setSelectedItem} /><ItemDetails item={selectedItem} /></div>);};// ✅ Сохранение связанной логики вместеconst ItemManager = () => {const { selectedItem, selectItem, clearSelection } = useItemSelection();return (<div><ItemList selectedId={selectedItem?.id} onSelect={selectItem} /><ItemDetails item={selectedItem} onClose={clearSelection} /></div>);};
Рекомендации по структуре:
-
Именование файлов:
components/UserProfile/index.js // Основной компонентUserHeader.js // ПодкомпонентыUserStats.jsUserActions.js -
Мемоизация при необходимости:
// ✅ Мемоизация дорогих подкомпонентовconst ExpensiveChart = React.memo(({ data, config }) => {// Тяжелые вычисления для отрисовки графикаreturn <Chart data={processedData} />;});// ✅ Правильное использование в родительском компонентеconst Dashboard = () => {const chartConfig = useMemo(() => ({type: 'line',animate: true}), []);return (<div><ExpensiveChart data={data} config={chartConfig} /></div>);};
Чек-лист перед дроблением:
- Компонент выполняет более одной четко определенной задачи?
- Есть ли повторяющиеся блоки UI или логики?
- Сложно ли тестировать компонент из-за его размера?
- Можно ли выделить переиспользуемые части?
- Улучшит ли дробление читаемость кода?
Помните: Дробление должно делать код проще, а не сложнее. Если новые компоненты используются только в одном месте и не упрощают понимание кода, возможно, дробление преждевременно.
-
Использование композиции вместо props drilling
Props drilling происходит, когда данные передаются через множество промежуточных компонентов, которые сами не используют эти данные, а только пробрасывают их дальше. Это создает ненужные зависимости и усложняет поддержку кода.
Решения:
-
Композиция через children:
// ❌ Props drilling (пробрасывание пропсов)type User = { name: string; email: string };const App = () => {const user = { name: "Alice", email: "alice@example.com" };return <UserProfile user={user} />;};const UserProfile = ({ user }: { user: User }) => (<div className="profile"><UserHeader user={user} /></div>);const UserHeader = ({ user }: { user: User }) => (<div className="user-header"><UserInfo name={user.name} email={user.email} /></div>);const UserInfo = ({ name, email }: { name: string; email: string }) => (<div><span>{name}</span><span>{email}</span></div>);// ✅ Композиция (composition через children)const App = () => {const user = { name: "Alice", email: "alice@example.com" };return (<UserProfile><UserHeader><UserInfo name={user.name} email={user.email} /></UserHeader></UserProfile>);};const UserProfile = ({ children }: { children: React.ReactNode }) => (<div className="profile">{children}</div>);const UserHeader = ({ children }: { children: React.ReactNode }) => (<div className="user-header">{children}</div>);const UserInfo = ({ name, email }: { name: string; email: string }) => (<div><span>{name}</span><span>{email}</span></div>); -
Композиция через слоты (named children):
// ✅ Использование именованных слотов для сложных макетовtype DashboardProps = {header: React.ReactNode;sidebar: React.ReactNode;content: React.ReactNode;footer: React.ReactNode;};const Dashboard = ({ header, sidebar, content, footer }: DashboardProps) => (<div className="dashboard"><div className="dashboard-header">{header}</div><div className="dashboard-body"><div className="dashboard-sidebar">{sidebar}</div><div className="dashboard-content">{content}</div></div><div className="dashboard-footer">{footer}</div></div>);// Использованиеconst App = () => {const user = getCurrentUser();return (<Dashboardheader={<Header title="Dashboard" user={user} />}sidebar={<Sidebar user={user} />}content={<MainContent data={fetchData()} />}footer={<Footer />}/>);}; -
Render props паттерн:
// ✅ Render props для переиспользуемой логикиtype DataFetcherProps<T> = {url: string;children: (data: T | null, loading: boolean, error: string | null) => React.ReactNode;};const DataFetcher = <T,>({ url, children }: DataFetcherProps<T>) => {const [data, setData] = useState<T | null>(null);const [loading, setLoading] = useState(true);const [error, setError] = useState<string | null>(null);useEffect(() => {fetchData(url).then(setData).catch(setError).finally(() => setLoading(false));}, [url]);return children(data, loading, error);};// Использованиеconst UserList = () => (<DataFetcher<User[]> url="/api/users">{(users, loading, error) => {if (loading) return <LoadingSpinner />;if (error) return <ErrorMessage error={error} />;return <UserTable users={users || []} />;}}</DataFetcher>); -
Context API для глобального состояния:
// ✅ Context для избежания props drilling глобальных данныхconst UserContext = createContext<User | null>(null);const UserProvider = ({ children }: { children: React.ReactNode }) => {const [user, setUser] = useState<User | null>(null);useEffect(() => {fetchCurrentUser().then(setUser);}, []);return (<UserContext.Provider value={user}>{children}</UserContext.Provider>);};const useUser = () => {const user = useContext(UserContext);if (!user) throw new Error('useUser must be used within UserProvider');return user;};// Использование в любом вложенном компонентеconst UserInfo = () => {const user = useUser();return <span>{user.name}</span>;};const App = () => (<UserProvider><Dashboard><UserInfo /> {/* Нет необходимости передавать user через props */}</Dashboard></UserProvider>);
Когда использовать каждый подход:
- Children composition: Для простых макетов и контейнеров, которые не нуждаются в данных
- Named slots: Для сложных макетов с четко определенными секциями
- Render props: Для переиспользуемой логики с разным UI
- Context: Для глобальных данных (пользователь, тема, настройки языка)
Антипаттерны:
// ❌ Злоупотребление Context для локальных данныхconst ButtonColorContext = createContext('blue');// ✅ Простая передача через props для локальных данныхconst Button = ({ color = 'blue' }: { color?: string }) => (<button style={{ backgroundColor: color }}>Click me</button>); -
5. Node.js / Backend: Рекомендации по Стилю
-
Обработка ошибок:
- Используйте централизованную обработку ошибок через кастомный класс
ApiError(см.lib/errorCodes). Все основные ошибки должны создаваться через этот класс с указанием кода из объектаcodes. - Для клиентских ошибок используйте класс
ClientError. - Все критические ошибки отправляйте в Sentry (
Sentry.captureException(error)) и логируйте в консоль для локальной отладки. - API возвращают структурированный JSON-ответ с полем
error, содержащим сообщение и код ошибки. - На фронте ошибки отображаются пользователю через снэкбары (
useErrorMessage), с учетом кода ошибки (например, истекшая подписка, не найдено и т.д.). - Валидация входных данных проводится до выполнения основной логики. При ошибке валидации возвращается ошибка с соответствующим кодом и сообщением.
- Для сложных сценариев используйте
Sentry.addBreadcrumbдля добавления цепочки событий. - Пример обработки ошибки в резолвере:
try {// ... основной код} catch (e) {const error = new ApiError(codes.problemError, 'Error resolving problem', e);Sentry.captureException(error);console.error('e', e);throw error;}
- Используйте централизованную обработку ошибок через кастомный класс
-
Конфигурация и переменные окружения:
- Все секреты и адреса внешних сервисов лежат в
.env(и не коммитятся в репозиторий). - Используйте утилиты валидации или просто проверяйте наличие переменной окружения перед использованием.
- Все секреты и адреса внешних сервисов лежат в
-
Логирование:
- Использовать console.error для логирования ошибок и debug для всего остального.
- Логировать все внешние запросы, входящие параметры (без чувствительных данных), а также результаты критических операций (оплата, создание заказа).
-
Валидация:
- Ни в коем случае не полагайтесь только на фронтенд-валидаторы — всегда проверяйте данные на сервере.
- Валидируйте формы на клиенте так чтобы пользователю было понятно как это исправить.
6. Тестирование
6.1. Философия Тестирования
-
Модель “Пирамида Тестов”
-
Unit Testing (60–70% покрытия):
- Frontend: Vitest + React Testing Library. Покрывать логику компонентов, утилиты (snapshot только там, где всё очень стабильно).
- Backend: Vitest. Покрыть сервисный слой. Мокать БД-запросы через заглушки (vi.mock).
-
Integration Testing (20–30%):
- Тесты, проверяющие взаимодействие нескольких модулей. Можно использовать in-memory БД (например, SQLite) или тестовую БД PostgreSQL.
- Во фронтенде — проверка запросов к мок-API и рендер компонента на основании ответа.
-
End-to-End Testing (10%):
- Cypress для ключевых пользовательских сценариев (регистрация, авторизация, заказ).
- Запускать e2e-тесты на каждом QA-запуске перед релизом, не на каждый коммит (чтобы сохранить скорость CI).
-
-
Test-Driven Development (TDD):
- Для новых модулей рекомендуется начинать с тестов. Не обязательно строго следовать TDD, но хотя бы писать тесты до реализации фичи, чтобы сразу понять, какую логику надо покрыть.
- Следовать циклу Red-Green-Refactor: Написать падающий тест → Заставить его пройти → Улучшить качество кода.
6.2. Организация и Структура Тестов
-
Организация Файлов:
- Тестовые файлы могут располагаться рядом с тестируемым файлом или в директории
_tests_в корне проекта. Помещайте тест в отдельную папку тестов, если его нельзя разместить рядом с тестируемым файлом.
- Тестовые файлы могут располагаться рядом с тестируемым файлом или в директории
-
Структура Тестов (AAA паттерн):
- Всегда следовать паттерну Arrange, Act, Assert:
import { describe, it, expect, vi } from 'vitest';describe('UserService', () => {describe('createUser', () => {it('should create user with valid data', () => {// Arrangeconst userData = { name: 'John', email: 'john@example.com' };// Actconst result = userService.createUser(userData);// Assertexpect(result).toHaveProperty('id');expect(result.name).toBe('John');});});});
- Всегда следовать паттерну Arrange, Act, Assert:
6.3. Стандарты и Лучшие Практики Тестирования
-
Именование Тестов:
- Используйте описательные имена тестов, объясняющие сценарий и ожидаемый результат:
// ✅ Хорошоit('should return formatted currency when given valid amount and locale')// ❌ Плохоit('currency test')
- Используйте описательные имена тестов, объясняющие сценарий и ожидаемый результат:
-
Изоляция и Независимость Тестов:
- Тесты не должны зависеть от баз данных, файловых систем или сетевых вызовов
- Используйте
vi.mock()для симуляции зависимостей для быстрых, детерминированных тестов - Каждый тест должен выполняться независимо и в любом порядке
- Очищайте состояние между тестами
-
Пример моков:
import { vi } from 'vitest';// Мок внешних зависимостейvi.mock('@power-rent/lib/debug', () => ({ default: () => () => {} }));vi.mock('@sentry/nextjs', () => ({ captureException: vi.fn() }));// Мок сервисов в тестахconst mockContext = {services: {clientService: {findById: vi.fn().mockResolvedValue(mockData)}},flags: { someFlag: true }};
6.4. Тестирование Различных Слоев
-
Тестирование GraphQL Резолверов:
import { describe, it, expect, vi } from 'vitest';import { toGlobalId } from 'graphql-relay';import clientResolver from '../server/graphql/resolvers/query/client.resolver';describe('client resolver', () => {it('should return client when found', async () => {// Arrangeconst clientData = { id: 'c1', name: 'Test Client' };const mockContext = {services: { clientService: { findById: vi.fn().mockResolvedValue(clientData) } },flags: { faunaMigrationClients: true }};const args = { id: toGlobalId('Client', 'c1') };// Actconst result = await clientResolver(args, mockContext);// Assertexpect(mockContext.services.clientService.findById).toHaveBeenCalledWith('c1');expect(result).toEqual({ node: clientData, cursor: 'c1' });});}); -
Тестирование React Компонентов:
import { render, screen, fireEvent } from '@testing-library/react';import { describe, it, expect, vi } from 'vitest';import UserCard from '../components/UserCard';describe('UserCard', () => {it('should call onClick when clicked', () => {// Arrangeconst mockOnClick = vi.fn();const user = { id: '1', name: 'John Doe', email: 'john@example.com' };// Actrender(<UserCard user={user} onClick={mockOnClick} />);fireEvent.click(screen.getByText('John Doe'));// Assertexpect(mockOnClick).toHaveBeenCalledWith('1');});}); -
Тестирование Утилитарных Функций:
import { describe, it, expect } from 'vitest';import { formatCurrency } from '../lib/formatters';describe('formatCurrency', () => {it('should format currency correctly for different locales', () => {expect(formatCurrency(1234.56, 'USD', 'en-US')).toBe('$1,234.56');expect(formatCurrency(1234.56, 'EUR', 'de-DE')).toBe('1.234,56 €');});it('should handle zero and negative values', () => {expect(formatCurrency(0, 'USD', 'en-US')).toBe('$0.00');expect(formatCurrency(-100, 'USD', 'en-US')).toBe('-$100.00');});});
6.5. Команды и Workflow Тестирования
-
Команды для Разработки:
yarn test run- Запустить Vitest unit тестыyarn test run --coverage --coverage.reporter=html- Запустить тесты с отчетом о покрытииyarn test- Запустить тесты в режиме наблюдения
-
Рекомендации по Покрытию:
- Стремиться к 80-90% покрытию для критических путей
- Фокусироваться на качестве, а не количестве - значимые тесты важнее метрик покрытия
- Документировать исключения для непокрытого кода с обоснованными причинами
6.6. Обработка Ошибок и Граничные Случаи
-
Всегда тестируйте сценарии ошибок:
it('should handle API errors gracefully', async () => {const mockService = vi.fn().mockRejectedValue(new Error('API Error'));await expect(serviceCall(mockService)).rejects.toThrow('API Error');}); -
Тестируйте граничные условия:
- Null/undefined входные данные
- Пустые массивы/объекты
- Максимальные/минимальные значения
- Неверные типы данных
-
Асинхронное Тестирование:
it('should handle async operations', async () => {const expected = { some: 'data' };const actual = await asyncFunction();expect(actual).toEqual(expected);});
6.7. Производительность и Поддерживаемость
-
Быстрое Выполнение Тестов:
- Тесты должны выполняться за миллисекунды, а не секунды
- Vitest запускает тесты параллельно по умолчанию
- Избегайте тяжелых операций настройки в beforeEach/afterEach
-
Поддержка Тестов:
- Обновляйте тесты при изменении реализации
- Удаляйте устаревшие или избыточные тесты
- Поддерживайте актуальность тестовых данных и fixtures
- Регулярно проверяйте производительность тестов
6.8. Pre-commit и Code Review
-
Перед Коммитом:
- Имена тестов четко описывают сценарий
- Тесты следуют AAA паттерну
- Нет внешних зависимостей
- Тесты изолированы и независимы
- Граничные случаи покрыты
- Сценарии ошибок протестированы
- Тесты выполняются быстро (< 100мс каждый)
- Покрытие кода соответствует стандартам проекта
-
Чек-лист Code Review:
- Добавлены ли юнит-тесты/интеграционные тесты для новой логики?
- Покрывают ли тесты основную бизнес-логику и граничные случаи?
- Читаемы ли тесты и легко ли их поддерживать?
- Проходят ли все тесты в CI?
7. Code Review: Чек-лист для Pull Request
-
Общее:
- Понятен ли scope изменений? Линк на тикет, описание изменений.
- Нет ли “магических” чисел/строк? (Вынести в константы или конфиг)
-
Структура и Архитектура:
- Соответствуют ли изменения принятому паттерну?
- Нет ли дублирования кода? Можно ли выделить общую логику?
-
Типизация:
- Являются ли все функции и переменные типизированными?
- Нет ли
$FlowFixMeили@ts-ignoreбез веской причины?
-
Безопасность и Валидация:
- Проверяются ли пользователи на права доступа (Authorization)?
- Валидируются ли входные данные?
- Обработаны ли возможные ошибки (ошибки БД, ошибки HTTP, таймауты)?
-
Тесты:
- Добавлены ли юнит-тесты / интеграционные тесты для новой логики?
- Запущены ли тесты и проходит ли CI?
-
Производительность:
- Нет ли излишних дорогостоящих операций в рендере компонентов (например, тяжёлых вычислений без мемоизации или наоборот мемоизации без необходимости)?
- При необходимости использован
React.memo,useMemo,useCallback.
-
Логирование и Отладка:
- Нет ли ненужных
console.log/debuggerв продакшен-коде? - В коде бэкенда использован
debug, а неconsole.log.
- Нет ли ненужных
-
Документация:
- Комментарии только там, где это реально помогает (не “я тут делаю X”, а “причина, по которой мы делаем X”).
- Обновлена ли документация в соответствии с изменениями?
-
Стиль кода:
- Пройдены ли ESLint и Flow без ошибок?
- Соблюдены ли соглашения об именовании (CamelCase, PascalCase, UPPER_SNAKE_CASE)?
Важно: PR считается “готовым” к слиянию только после того, как минимум один человек одобрил изменения (peer review).
8. Документирование
Приветствуется использование AI для генерации документации.
-
Комментарии в Коде:
- Писать комментарии если метод сложный или неочевидный.
- Не оставлять “мертвые” комментарии (старый код, закомментированный) — лучше удалить ненужный участок.
9. Рекомендации по Рефакторингу Существующего Кода
-
Выявить “горячие точки” (hotspots):
- Определите модули, где чаще всего появляются баги (по истории Git, таскам в трекере).
- Начните с них: напишите простые интеграционные/юнит-тесты, чтобы иметь уверенность при рефакторинге.
-
Небольшие шаги (Small Commits):
- Реорганизуйте код постепенно: каждый PR делает одну-чётко определённую правку.
- После каждого шага запускайте тесты и проверяйте, что ничего не сломалось.
-
Добавление Тестов к Старому Коду:
- Прежде чем менять логику, напишите тесты, которые покрывают текущее поведение (даже если оно багованное).
- Затем корректируйте поведение, параллельно редактируя тест, чтобы он протестировал “правильный” результат.
-
Изолированное Рефакторинг:
- Выделяйте повторяющиеся утилиты из компонентов/сервисов в
utils/. - Вынесите “большие” бизнес-правила в отдельные слои (например, сформировать правило расчёта комиссии в
services/commissionService.ts, а не вcontrollers/orderController.ts).
- Выделяйте повторяющиеся утилиты из компонентов/сервисов в
-
Удаление Неиспользуемого Кода:
- Если в проекте лежат файлы или функции, которые никем не импортируются, безопасно их удаляйте.
- Это уменьшает поверхность для багов.
10. Менторство и Обучение
-
Парное программирование (Pair Programming):
- Запланируйте регулярные сессии, где сеньор работает с джуниором над задачей.
- Это выгодно и для процесса разработки (снижается риск багов), и для роста джуниора.
-
Code Review как Обучающий Процесс:
- Комментарии при ревью — не только “исправь это”, но и “почему так лучше”.
- Сеньор должен оставаться доступным для вопросов: лучше потратить 15 минут на объяснение, чем потом разбор огромного потока багов.
11. Использование ИИ для программирования
Инструменты ИИ для программирования могут значительно повысить производительность при правильном использовании. Однако они требуют тщательной интеграции в рабочий процесс для поддержания качества кода и безопасности.
11.1. Лучшие практики составления промптов
-
Будьте конкретными и детальными:
- Создавайте точные промпты, которые четко формулируют ваши требования.
- Включайте конкретные ограничения, библиотеки, требования к производительности и edge кейсы.
- Плохо: “Создай форму входа”
- Хорошо: “Создай React-компонент формы входа с валидацией email и пароля, используя formik, который отображает сообщения об ошибках и обрабатывает состояния загрузки”
-
Разбивайте сложные задачи:
- Разделяйте большие задачи на меньшие, логические шаги.
- Запрашивайте одну функцию или компонент за раз, а не целые модули.
- Это приводит к лучшей, более сфокусированной генерации кода.
-
Указывайте ожидаемые входные и выходные данные:
- Четко определяйте параметры функций, типы возвращаемых значений и ожидаемое поведение.
- Включайте примеры ожидаемых входных/выходных данных, когда это полезно.
11.2. Рабочий процесс интеграции кода
-
Никогда не принимайте код вслепую:
- Всегда читайте и понимайте сгенерированный код перед использованием.
- Проверяйте, что логика соответствует вашим требованиям и стандартам.
- Проверяйте наличие потенциальных уязвимостей безопасности или проблем с производительностью.
-
Следуйте процессу итеративного улучшения (Cursor):
- Предоставьте контекст и создайте начальный промпт
- Просмотрите сгенерированный код
- Уточните промпт на основе результатов
- Протестируйте и проверьте код
- Внесите необходимые ручные корректировки
- Сохраните рабочую версию перед созданием следующего промпта, чтобы предотвратить повреждение рабочей версии
- Повторяйте, пока не будете удовлетворены
11.3. Обеспечение качества
-
Обязательный код-ревью:
- Весь код, сгенерированный ИИ, должен проходить тот же процесс проверки, что и код, написанный человеком.
- Качество сгенерированного кода может отличаться от промпта к промпту, поэтому избегайте мысли “О, этот код выглядит так хорошо, я просто приму его”.
-
Комплексное тестирование:
- Напишите модульные тесты для всех функций и компонентов, сгенерированных ИИ.
- Убедитесь, что покрытие тестами соответствует стандартам проекта.
- Особое внимание уделите edge кейсам, которые ИИ может пропустить.
-
Требования к документации:
- Документируйте, что код был сгенерирован ИИ.
- Добавляйте комментарии, объясняющие сложную логику или неочевидные решения.
- Обновляйте документацию проекта, чтобы отразить новый функционал.
11.4. Соображения безопасности
-
Проверяйте сторонние зависимости:
- Тщательно проверяйте любые библиотеки или фреймворки, предложенные ИИ.
- Проверяйте активную поддержку, уязвимости безопасности и доверие сообщества.
- Убедитесь, что предложенные пакеты действительно существуют и являются легитимными.
-
Будьте внимательны к галлюцинациям ИИ:
- ИИ может генерировать правдоподобно выглядящий, но неправильный код или предлагать несуществующие библиотеки.
- Всегда проверяйте предложения ИИ по официальной документации.
- Тщательно тестируйте перед деплоем.
11.5. Для чего хорош ИИ
-
Быстрое прототипирование и шаблоны:
- Генерация начальной структуры кода и шаблонов.
- Создание повторяющихся паттернов кода (CRUD-операции, валидация форм).
- Настройка конфигурационных файлов и базовой структуры проекта.
-
Объяснение кода и документация:
- Генерация комментариев и документации для существующего кода.
- Объяснение сложных алгоритмов или устаревшего кода.
- Создание README-файлов и документации API.
-
Рефакторинг и оптимизация:
- Предложение улучшений существующего кода.
- Преобразование кода между различными паттернами или фреймворками.
- Выявление потенциальных проблем с производительностью.
11.6. Чего следует избегать
-
Не используйте ИИ для:
- Критически важных реализаций безопасности (аутентификация, шифрование).
- Сложной бизнес-логики без тщательной проверки.
- Создания новой архитектуры с нуля.
-
Избегайте чрезмерной зависимости:
- Не позволяйте ИИ заменять ваше понимание фундаментальных концепций.
- Продолжайте учиться и следить за технологическими трендами.
- Используйте ИИ как инструмент для улучшения, а не замены ваших навыков программирования.
11.7. Руководство для команды
-
Обмен знаниями:
- Делитесь эффективными промптами и техниками с членами команды.
- Документируйте уроки, извлеченные из разработки с помощью ИИ.
-
Постоянное улучшение:
- Регулярно оценивайте влияние инструментов ИИ на качество кода и скорость разработки.
- Следите за новыми инструментами и техниками ИИ.
Помните: ИИ - это мощный помощник, но человеческий опыт, суждение и проверка остаются необходимыми для создания надежного, безопасного и поддерживаемого программного обеспечения.
11.8. Примеры хороших и плохих промптов
Пример 1: Создание компонента
❌ Bad prompt:
Создать компонент карточки пользователя✅ Good prompt:
Создать функциональный React-компонент UserCard, который:- Принимает объект пользователя со свойствами: id, name, email, avatar, role- Использует Flow/TypeScript для типизации пропсов- Отображает аватар пользователя (с запасным вариантом инициалов, если аватар отсутствует)- Показывает имя, email и роль с соответствующим стилем- Имеет обработчик onClick, который передает id пользователя- Должен быть адаптивным для мобильных устройствПример 2: Интеграция API
❌ Bad prompt:
Сделать API-запрос для получения пользователей✅ Good prompt:
Создать пользовательский React-хук useUsers, который:- Получает пользователей с эндпоинта /api/users- Возвращает объект { users, loading, error, refetch }- Использует TypeScript с правильной типизацией для интерфейса User- Обрабатывает состояния загрузки и ошибок- Реализует обработку ошибок с помощью try-catch- Использует fetch API (не axios)- Включает правильную очистку для предотвращения утечек памяти- Возвращает пустой массив как начальное состояние для usersПример 3: Валидация формы
❌ Bad prompt:
Добавить валидацию в форму входа✅ Good prompt:
Добавить валидацию в существующую React-форму входа с использованием Formik:- Поле email: обязательное, правильный формат email, максимум 100 символов- Поле пароля: обязательное, минимум 8 символов, должно содержать хотя бы одну цифру- Отображать ошибки валидации под каждым полем красным текстом- Отключать кнопку отправки, пока форма невалидна или отправляется- Показывать состояние загрузки во время отправки- Очищать ошибки, когда пользователь начинает печатать- Использовать наш существующий компонент TextField для согласованности- Возвращать схему валидации как отдельный экспортируемый объектExample 4: Utility Function
❌ Bad prompt:
Функция форматирования даты✅ Good prompt:
Создать TypeScript-утилиту formatDate, которая:- Принимает на вход объект Date или строку в формате ISO- Возвращает отформатированную строку в формате "MMM DD, YYYY" (например, "15 янв. 2024")- Обрабатывает некорректные даты, возвращая "Invalid Date"- Использует встроенный Intl.DateTimeFormat для интернационализации- Имеет корректные TypeScript-типы для параметров и возвращаемого значения- Включает JSDoc-комментарии с примерами использования- Экспортируется как именованный экспорт из utils/dateHelpers.tsExample 5: Database Query
❌ Bad prompt:
Получить данные пользователя из базы данных✅ Good prompt:
Создать функцию Prisma-запроса getUserWithReservations, которая:- Принимает userId в качестве строкового параметра- Возвращает данные пользователя вместе с связанными бронированиями- Использует Prisma include для получения бронирований с деталями транспортных средств- Обрабатывает случай, когда пользователь не существует (возвращает null)- Включает корректный TypeScript-тип возвращаемого значения- Сортирует бронирования по createdAt в порядке убывания- Ограничивает количество бронирований последними 10 записями- Использует try-catch для обработки ошибок- Следует нашему существующему шаблону сервиса в services/UserService.tsExample 6: Refactoring Request
❌ Bad prompt:
Улучшить этот код✅ Good prompt:
Рефакторинг React-компонента для улучшения производительности и читаемости:
[прикрепите файл к контексту или укажите относительный путь к файлу]
Требования:- Вынести повторяющуюся логику в пользовательские хуки- Реализовать правильную мемоизацию где необходимо- Разбить на более мелкие подкомпоненты, если компонент слишком сложный- Добавить корректные Flow/TypeScript типы- Следовать нашим соглашениям об именовании (camelCase для функций, PascalCase для компонентов)- Сохранить существующую функциональность без изменений- Добавить комментарии для сложной логики- Обеспечить правильную типизацию всех пропсовExample 7: Testing
❌ Bad prompt:
Написать тесты для этого компонента✅ Good prompt:
Написать комплексные тесты Vitest для компонента UserCard с использованием React Testing Library:
[прикрепите файл к контексту или укажите относительный путь к файлу]
Необходимые тестовые случаи:- Корректно отображает информацию о пользователе- Показывает инициалы вместо аватара, если аватар не предоставлен- Вызывает обработчик onClick с правильным id пользователя- Корректно обрабатывает отсутствующие свойства пользователя- Тестирует адаптивное поведение, если применимо- Использует правильные описания тестов и организует их в блоки describe- Мокает все внешние зависимости- Следуй существующим паттернам тестирования из других тестов компонентов- Используй Flow/TypeScript для тестовых файловКлючевые Паттерны для Эффективных Промптов:
-
Начните с действия: “Создайте React-компонент…”, “Напишите функцию, которая…”, “Рефакторинг кода для…”
-
Включите контекст: Упомяните фреймворк, язык программирования, существующие паттерны и структуру файлов
-
Перечислите конкретные требования: Используйте маркированные списки для ясности и полноты
-
Укажите ограничения: Требования к производительности, поддержка браузеров, требования к доступности
-
Определите ожидаемое поведение: Включите edge кейсы и обработку ошибок
-
Упомяните существующие паттерны: Ссылайтесь на похожий код в проекте для обеспечения согласованности
-
Будьте явными в типах: Указывайте типы возвращаемых значений Flow/TypeScript или типы параметров
-
Включите примеры: Показывайте ожидаемые входные/выходные данные, когда это может помочь
12. Лучшие Практики Feature Flags
Feature flags — это мощная техника разработки программного обеспечения, которая позволяет командам контролировать видимость и поведение фич во время выполнения без развертывания кода.
⚠️ ВАЖНО: Каждая новая фича или изменение core функциональности ДОЛЖНО быть покрыто feature flag. Это обязательное требование для всех изменений кода, которые влияют на пользовательскую функциональность или основное поведение системы.
Это требование обеспечивает:
- Безопасный и контролируемый выпуск функций
- Возможность быстро отключить проблемные функции
- Постепенный выпуск для определенных сегментов пользователей
- Легкий откат в случае проблем
Исключения из этого правила должны быть одобрены тим лидом и задокументированы в описании pull request.
12.1. Основные Принципы
- Делайте флаги короткоживущими:
- Большинство feature flags должны быть временными и удаляться после успешного выпуска
- Относитесь к feature flags как к техническому долгу, который требует очистки
12.2. Как использовать
Наша система feature flags использует Statsig с двумя разными пакетами в зависимости от контекста:
@statsig/react-bindingsдля клиентских React-компонентов@flags-sdk/statsigдля серверных API-маршрутов и GraphQL-резолверов
12.2.1. Использование на Клиенте (React-компоненты)
Для клиентских компонентов используйте React-хуки Statsig:
Feature Gates (Булевы флаги):
// @flowimport { useGateValue } from '@statsig/react-bindings';
export default function MyComponent() { const isNewFeatureEnabled = useGateValue('new_checkout_flow');
return ( <div> {isNewFeatureEnabled ? ( <NewCheckoutFlow /> ) : ( <LegacyCheckoutFlow /> )} </div> );}Динамическая Конфигурация:
// @flowimport { useConfig } from '@statsig/react-bindings';
export default function ConfigurableComponent() { const { config, isLoading } = useConfig('ui_configuration');
if (isLoading) return <LoadingSpinner />;
const buttonColor = config?.value?.buttonColor || 'blue'; const maxItems = config?.value?.maxItems || 10;
return ( <div> <Button color={buttonColor}>Нажми меня</Button> <ItemsList limit={maxItems} /> </div> );}12.2.2. Использование на Сервере (API-маршруты и GraphQL)
Для серверного использования в API-маршрутах и GraphQL-резолверах используйте flags SDK:
В API-маршрутах:
// @flowimport { Statsig } from '@flags-sdk/statsig';import { getFeatureFlags } from '@power-rent/server/graphql/helpers/getFeatureFlags';
export default async function handler(req, res) { const stableId = req.cookies['stable-id'] || req.headers['x-stable-id'] || ''; const tenantId = req.body.tenantId || req.headers['x-tenant-id'] || '';
// Метод 1: Использование централизованного помощника (предпочтительно) const flags = await getFeatureFlags({ stableId, tenantId }); if (flags.newBillingSystem) { // Использовать новую логику биллинга }
// Метод 2: Прямое использование Statsig const user = { userID: tenantId, customIDs: { stableID: stableId }, };
const isEnabled = Statsig.checkGateWithExposureLoggingDisabledSync( user, 'new_api_endpoint' );
if (isEnabled) { return res.json({ message: 'Новая логика API' }); }
return res.json({ message: 'Устаревшая логика API' });}В GraphQL-резолверах:
// @flowconst resolvers = { Query: { getUsers: async (parent, args, context) => { const { stableId, tenantId, flags } = context;
if (flags.newUserQuery) { return getUsersFromPostgres(args); }
return getUsersFromFauna(args); }, },};12.2.3. Идентификация Пользователя
Система использует подход stable ID для последовательной оценки feature flags:
- Stable ID хранится в cookie браузера
- Если stable ID не существует, генерируется новый с помощью
crypto.randomBytes()в middleware - Stable ID обеспечивает пользователям последовательный опыт работы с feature flags в разных сессиях
- Дополнительный контекст пользователя (tenant ID, пользовательские атрибуты) может быть добавлен для таргетинга
12.2.4. Когда Использовать Какой Подход
useGateValue() vs useGate():
// ✅ Используйте useGateValue() для простых булевых проверокconst isEnabled = useGateValue('simple_feature');if (isEnabled) { return <NewFeature />;}
// ✅ Используйте useGate() когда нужны метаданные или состояния загрузкиconst { gate, isLoading, error } = useGate('complex_feature');if (isLoading) return <Spinner />;if (error) return handleError(error);if (gate?.value) { logAnalytics('feature_used', { ruleID: gate.ruleID }); return <NewFeature />;}Feature Gates vs Dynamic Configs:
// ✅ Feature Gates - Простые переключатели вкл/выклconst showNewButton = useGateValue('new_button_design');
// ✅ Dynamic Configs - Несколько параметров, которые изменяются вместеconst { config } = useConfig('checkout_settings');const maxItems = config?.value?.maxItems || 5;const timeout = config?.value?.timeout || 30000;const buttonColor = config?.value?.buttonColor || 'blue';Клиентская vs Серверная Реализация:
// ✅ Клиентская: UI изменения, функции UXconst showNewNav = useGateValue('new_navigation');
// ✅ Серверная: Маршрутизация базы данных, поведение API, бизнес-логикаconst flags = await getFeatureFlags({ stableId, tenantId });if (flags.useNewPaymentFlow) { return processPaymentV2(paymentData);}Когда Использовать Каждый Тип Хука:
| Хук | Случай Использования | Пример |
|---|---|---|
useGateValue() | Простые булевы переключатели функций | Показать/скрыть UI элементы |
useGate() | Нужны состояния загрузки или метаданные | Сложные функции с аналитикой |
useConfig() | Несколько связанных настроек | Настройки темы, API endpoints |
12.2.5. Лучшие Практики Использования
✅ Делайте:
// Используйте описательные имена флаговconst isNewDashboardEnabled = useGateValue('new_dashboard_v2');
// Обрабатывайте состояния загрузки когда доступноconst { config, isLoading } = useConfig('dynamic_config');if (isLoading) return <LoadingSpinner />;
// Предоставляйте fallback-значенияconst maxRetries = config?.value?.maxRetries ?? 3;
// Используйте серверные флаги для маршрутизации базы данныхconst flags = await getFeatureFlags({ stableId, tenantId });if (flags.usePostgres) { return queryPostgres();}❌ Не делайте:
// Не создавайте сложные вложенные условияif (useGate('feature_a')) { if (useGate('sub_feature_b')) { if (useGate('micro_feature_c')) { // Сложная вложенная логика } }}
// ✅ Используйте один составной флаг или конфиг вместо этогоconst { config } = useFeatureConfig('feature_a_configuration');const shouldShowFeatureA = config?.enabledFeatures?.includes('feature_a');❌ Не делайте:
// Не добавляйте новые функции без флагаnewFeatureThatSeemsToBeSafe(data);12.3 Соглашения об Именовании Флагов
✅ Хорошие Примеры:
new_checkout_flow- Четкое описание функцииai_recommendations_enabled- Описывает функциональностьbilling_api_v2_rollout- Указывает цель выпускаdebug_mode_internal- Показывает целевую аудиторию
❌ Плохие Примеры:
flag1- Неописательноexperiment- Слишком неопределенноtemp_flag- Неясная цельjohns_test- Личная ссылка
12.4 Документация и Рекомендации для Команды
- Документируйте цель флага и план выпуска:
/*** Цель: Постепенный выпуск новой примерной функции* План выпуска:* - Неделя 1: Только внутренние пользователи (0.1%)* - Неделя 2: Бета-клиенты (5%)* - Неделя 3: Все премиум пользователи (25%)* - Неделя 4: Полный выпуск (100%)** Дата очистки: 2025-07-01* Владелец: @github-user-name* Тикет: TOP-123*/export const exampleFlag = flag<boolean>({key: 'example_flag',...});