coding_style_guide_en
Coding Style Guide: Improving Quality and Reducing Bugs
Below is a condensed set of rules and recommendations. The goal is to establish unified standards, make code easier to read and understand, reduce regressions, and accelerate delivery of new features.
1. General Principles
-
“Clean Code” comes first.
- Each function and component should perform only one task (Single Responsibility).
- If code starts to duplicate or grow too large, consider extracting it into utilities, hooks, or common modules.
-
KISS Principle (“Keep It Stupid Simple”).
- Prefer understandable logic over “excessive” optimizations.
- Avoid complex “tricks”: it’s better to iterate through several lines than to rack your brain over an incomprehensible loop.
-
YAGNI (“You Ain’t Gonna Need It”).
- Don’t write code “for the future” if the specific task doesn’t require extensibility.
- Don’t create a universal, parameterized algorithm right away if you need a simple implementation.
-
The best time to find a bug is as early as possible.
- Write tests before or simultaneously with the feature (TDD/unit test attestation).
- Run linter/static analyzer/tests in CI on every change.
2. General Naming Rules
-
Files and directories:
- Components and services —
PascalCase(e.g.,UserCard.jsx,UserService.js). - Utilities, hooks, and regular modules —
camelCase(e.g.,formatDate.js,useAddress.js,validators.js). - Configs and constants —
kebab-case(e.g.,db-config.js).
- Components and services —
-
Variables and functions:
- Variables, functions, and arguments —
camelCase(e.g.,calculateTotalPrice). - Constants (usually easily recognizable) —
UPPER_SNAKE_CASE(e.g.,MAX_LOGIN_ATTEMPTS). - Interfaces, types, and enums —
PascalCase.
- Variables, functions, and arguments —
-
React components:
- Always use
PascalCasefor component names (e.g.,UserList,OrderForm).
- Always use
-
CSS/styles:
- For styled-components — place styles next to the component in the same file. Moving styles to a separate file makes sense only if they are reused.
3. Typing Standards
-
Don’t use
anywithout extreme necessity.- If the type is hard to determine immediately, you can use
mixed(Flow) orunknown(TS) and tighten the checking over time.
- If the type is hard to determine immediately, you can use
-
Explicit return types for functions/methods.
- Always specify
(): Promise<User>or(): numberinstead of leaving it to compiler inference. - This improves readability and helps avoid unexpected
voidorany.
- Always specify
-
Use
readonlyfor immutable fields.-
For example Flow:
type User {+id: string;name: string;email: string;} -
For example TS:
interface User {readonly id: string;name: string;email: string;}
-
-
Generics:
- When creating utilities or wrapper functions (e.g.,
fetchData<T>(url: string): Promise<T>). - Don’t create “unnecessary” generics if the specifics are clear.
- When creating utilities or wrapper functions (e.g.,
-
Types and interfaces:
- Flow: types are defined with the
typekeyword, whileinterfaceis intended for describing classes. - TS: For describing objects, entities, and DTOs, use
interface(e.g., interfaces for models, function parameters, component props). - For union types, intersections, primitive aliases, tuples, and complex/derived types, use
type. - Don’t mix both approaches in the same code block without necessity. For consistency, follow this rule throughout the entire project.
- Flow: types are defined with the
-
Enum vs Union Types:
-
Flow: enums are supported in later versions and are recommended for use.
-
TS: For a fixed set of values, use
enumoras const+ union of strings:export enum UserRole {ADMIN = 'admin',USER = 'user',GUEST = 'guest'}// orexport const UserRoles = ['admin', 'user', 'guest'] as const;export type UserRole = (typeof UserRoles)[number];
-
4. React: Components and Architectural Standards
-
Functional components + React Hooks.
- Don’t write class components (except when it’s really justified, but most often it’s redundant).
- Use
useState,useEffectonly where it’s really necessary. - For complex logic — extract to custom hooks.
-
Divide into “presentational” and “container” components (as needed).
- Attention: anti-pattern in Relay. In Relay, data is requested where it’s used, which avoids dead queries and requesting unused data.
- Presentational: responsible only for UI, receive data through props, don’t know about data source.
- Container: wrap data fetching logic (Redux, Context API, services) and pass it to presentational components.
-
Mandatory PropTypes specification through Flow or TypeScript.
-
Type all input props:
type UserCardProps = {user: User;onClick: (id: string) => void;};const UserCard = ({ user, onClick }: UserCardProps) => { /*…*/ };
-
-
Selectors and memoization:
- Use
useMemojudiciously and only when necessary:- For expensive calculations that would block the event loop for more than 100ms
- When the memoized value is used as a dependency in other hooks
- When passing complex objects or arrays as props to memoized child components
- When the calculation involves complex data transformations or filtering
- Avoid premature optimization - don’t use
useMemofor simple calculations or primitive values - Remember that
useMemoitself has a cost - it needs to store the previous value and compare dependencies - Use
useCallbackfor function memoization when:- Passing callbacks to memoized child components (React.memo)
- The callback is used as a dependency in other hooks (useEffect, useMemo)
- The function is expensive to create or contains complex logic
- Avoid
useCallbackfor:- Simple event handlers that don’t trigger re-renders
- Functions that are recreated on every render but don’t impact performance
- Functions that are only used within the component and not passed as props
- Remember that
useCallbackitself has overhead - it needs to store the function and compare dependencies - Always include all dependencies used inside the callback in the dependency array
- Consider using arrow functions for callbacks to maintain consistent
thisbinding - Apply
React.memojudiciously in these scenarios:- Components that receive the same props frequently but re-render unnecessarily
- List items in large data tables or virtualized lists
- Form components that update frequently but don’t need to re-render their siblings
- Components that are expensive to render (complex calculations, heavy DOM operations)
- Components that are rendered many times in a list
- Avoid
React.memowhen:- The component is simple and renders quickly
- Props change frequently anyway
- The component is used only once or twice
- The component needs to re-render on every parent update
- Remember that
React.memoadds its own overhead for prop comparison - Always include a custom comparison function when props are unmemoized objects or arrays to prevent unnecessary re-renders
- Use
React.memoin combination withuseCallbackfor callback props
- Use
-
Working with asynchronous code:
- Assume that any API can fail: plan fallback logic in advance (e.g., “try again” or “show offline mode”).
- Consider the number of parallel requests and use rate limiting or throttling.
-
Keys in lists
- Never use array index as
key, except when data is static. - Use unique identifiers (
item.id) so React can efficiently determine what changed.
- Never use array index as
-
React Component Splitting
Goal: Break down complex components into smaller, focused parts to improve readability, testability, and code reuse.
When to split a component:
- Size: When a component exceeds 200-300 lines of code.
- Complexity: When a component performs more than one main function.
- Reusability: When logic or UI blocks are repeated in different places.
- Testing: When testing a component becomes difficult due to multiple responsibilities.
Splitting strategies:
-
Extracting presentational components:
// ❌ Large component with mixed logicconst UserProfile = ({ userId }) => {const [user, setUser] = useState(null);const [loading, setLoading] = useState(true);// 50+ lines of data fetching logic...return (<div>{/* 100+ lines of JSX for profile display */}<div className="user-header"><img src={user.avatar} alt={user.name} /><h1>{user.name}</h1><p>{user.email}</p></div><div className="user-stats">{/* Complex statistics display logic */}</div><div className="user-actions">{/* Multiple buttons and actions */}</div></div>);};// ✅ Split into logical componentsconst 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>); -
Extracting custom hooks:
// ❌ State logic inside componentconst 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...};// ✅ Logic extracted to custom hookconst OrderForm = () => {const {formData,errors,isSubmitting,handleFieldChange,handleSubmit} = useOrderForm();return (<form onSubmit={handleSubmit}><OrderFormFieldsdata={formData}errors={errors}onChange={handleFieldChange}/><SubmitButton isLoading={isSubmitting} /></form>);}; -
Splitting by functionality:
// ❌ Component with multiple responsibilitiesconst Dashboard = () => {// Chart logic// Table logic// Filter logic// Export logicreturn (<div>{/* Multiple different UI blocks */}</div>);};// ✅ Split into specialized componentsconst Dashboard = () => {const { data, filters, setFilters } = useDashboardData();return (<div><DashboardFilters filters={filters} onChange={setFilters} /><DashboardCharts data={data} /><DashboardTable data={data} /><DashboardExport data={data} /></div>);};
Splitting rules:
-
One component — one responsibility:
- Each component should have a clearly defined role.
- If describing a component uses the conjunction “and”, it might need to be split.
-
Logical boundaries:
- Extract components by semantic blocks (header, content, footer).
- Create components for reusable UI patterns.
-
Abstraction levels:
// High level - containerconst ReservationPage = () => (<div><ReservationForm /><ReservationSummary /></div>);// Medium level - logical blocksconst ReservationForm = () => (<form><VehicleSelector /><DateRangeSelector /><CustomerDetails /></form>);// Low level - specific fieldsconst VehicleSelector = () => (<div><VehicleTypeSelect /><VehicleList /></div>);
Anti-patterns (what to avoid):
-
Over-splitting:
// ❌ Too granular splittingconst UserName = ({ name }) => <span>{name}</span>;const UserEmail = ({ email }) => <span>{email}</span>;// ✅ Reasonable combinationconst UserInfo = ({ name, email }) => (<div><span className="name">{name}</span><span className="email">{email}</span></div>); -
Passing too many props:
// ❌ Too many propsconst UserCard = ({id, name, email, avatar, role, isActive,createdAt, lastLogin, permissions, settings}) => { /* ... */ };// ✅ Grouping related dataconst UserCard = ({ user, permissions, settings }) => { /* ... */ }; -
Breaking logically connected code:
// ❌ Tightly coupled logic in different componentsconst ParentComponent = () => {const [selectedItem, setSelectedItem] = useState(null);return (<div><ItemList onSelect={setSelectedItem} /><ItemDetails item={selectedItem} /></div>);};// ✅ Keeping related logic togetherconst ItemManager = () => {const { selectedItem, selectItem, clearSelection } = useItemSelection();return (<div><ItemList selectedId={selectedItem?.id} onSelect={selectItem} /><ItemDetails item={selectedItem} onClose={clearSelection} /></div>);};
Structure recommendations:
-
File naming:
components/UserProfile/index.js // Main componentUserHeader.js // Sub-componentsUserStats.jsUserActions.js -
Memoization when needed:
// ✅ Memoizing expensive sub-componentsconst ExpensiveChart = React.memo(({ data, config }) => {// Heavy calculations for chart renderingreturn <Chart data={processedData} />;});// ✅ Proper usage in parent componentconst Dashboard = () => {const chartConfig = useMemo(() => ({type: 'line',animate: true}), []);return (<div><ExpensiveChart data={data} config={chartConfig} /></div>);};
Checklist before splitting:
- Does the component perform more than one clearly defined task?
- Are there repeating UI blocks or logic?
- Is the component difficult to test due to its size?
- Can reusable parts be extracted?
- Will splitting improve code readability?
Remember: Splitting should make code simpler, not more complex. If new components are only used in one place and don’t simplify code understanding, the splitting might be premature.
-
Using Composition Instead of Props Drilling
Props drilling occurs when data is passed through multiple intermediate components that don’t use the data themselves, only pass it down. This creates unnecessary dependencies and complicates code maintenance.
Solutions:
-
Composition through children:
// ❌ Props drillingtype 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 through childrenconst 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>); -
Composition through slots (named children):
// ✅ Using named slots for complex layoutstype 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>);// Usageconst App = () => {const user = getCurrentUser();return (<Dashboardheader={<Header title="Dashboard" user={user} />}sidebar={<Sidebar user={user} />}content={<MainContent data={fetchData()} />}footer={<Footer />}/>);}; -
Render props pattern:
// ✅ Render props for reusable logictype 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);};// Usageconst 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 for global state:
// ✅ Context to avoid props drilling for global dataconst 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;};// Usage in any nested componentconst UserInfo = () => {const user = useUser();return <span>{user.name}</span>;};const App = () => (<UserProvider><Dashboard><UserInfo /> {/* No need to pass user through props */}</Dashboard></UserProvider>);
When to use each approach:
- Children composition: For simple layouts and containers that don’t need data
- Named slots: For complex layouts with clearly defined sections
- Render props: For reusable logic with different UI
- Context: For global data (user, theme, language settings)
Anti-patterns:
// ❌ Overusing Context for local dataconst ButtonColorContext = createContext('blue');// ✅ Simple prop passing for local dataconst Button = ({ color = 'blue' }: { color?: string }) => (<button style={{ backgroundColor: color }}>Click me</button>); -
5. Node.js / Backend: Style Recommendations
-
Runtime errors handling: This doesn’t include input validation errors, which are described in point 13 of this document.
- Use centralized error handling through a custom
ApiErrorclass (seelib/errorCodes). All main errors should be created through this class with a code specified from thecodesobject. - For client errors, use the
ClientErrorclass. - Send all critical errors to Sentry (
Sentry.captureException(error)) and log to console for local debugging. - APIs return structured JSON response with an
errorfield containing message and error code. - On the frontend, errors are displayed to users through snackbars (
useErrorMessage), considering the error code (e.g., expired subscription, not found, etc.). - Input data validation is performed before executing main logic. On validation error, return an error with appropriate code and message.
- For complex scenarios, use
Sentry.addBreadcrumbto add event chains. - Example error handling in resolver:
try {// ... main code} catch (e) {const error = new ApiError(codes.problemError, 'Error resolving problem', e);Sentry.captureException(error);console.error('e', e);throw error;}
- Use centralized error handling through a custom
-
Configuration and environment variables:
- All secrets and external service addresses are stored in
.env(and not committed to repository). - Use validation utilities or simply check for environment variable presence before use.
- All secrets and external service addresses are stored in
-
Logging:
- Use console.error for logging errors and debug for everything else.
- Log all external requests, incoming parameters (without sensitive data), and results of critical operations (payment, order creation).
-
Validation:
- Never rely only on frontend validators — always check data on the server.
- Validate forms on the client so users understand how to fix issues.
6. Testing
6.1. Testing Philosophy
-
“Test Pyramid” Model
-
Unit Testing (60–70% coverage):
- Frontend: Vitest + React Testing Library. Cover component logic, utilities (snapshot only where everything is very stable).
- Backend: Vitest. Cover service layer. Mock DB queries through stubs (vi.mock).
-
Integration Testing (20–30%):
- Tests that check interaction between multiple modules. Can use in-memory DB (e.g., SQLite) or test PostgreSQL DB.
- On frontend — check requests to mock API and component render based on response.
-
End-to-End Testing (10%):
- Cypress for key user scenarios (registration, authorization, ordering).
- Run e2e tests on each QA run before release, not on every commit (to maintain CI speed).
-
-
Test-Driven Development (TDD):
- For new modules, it’s recommended to start with tests. It’s not mandatory to strictly follow TDD, but at least write tests before implementing the feature to immediately understand what logic needs to be covered.
- Follow the Red-Green-Refactor cycle: Write failing test → Make it pass → Improve code quality.
6.2. Test Organization and Structure
-
File Organization:
- Test files can be located next to the tested file or in the
_tests_directory at the project root. Put test to the dedicated tests folder if it can’t be placed next to the tested file.
- Test files can be located next to the tested file or in the
-
Test Structure (AAA Pattern):
- Always follow Arrange, Act, Assert pattern:
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');});});});
- Always follow Arrange, Act, Assert pattern:
6.3. Testing Standards and Best Practices
-
Test Naming:
- Use descriptive test names that explain the scenario and expected outcome:
// ✅ Goodit('should return formatted currency when given valid amount and locale')// ❌ Badit('currency test')
- Use descriptive test names that explain the scenario and expected outcome:
-
Test Isolation and Independence:
- Tests must not rely on databases, file systems, or network calls
- Use
vi.mock()to simulate dependencies for fast, deterministic tests - Each test should run independently and in any order
- Clean state between tests
-
Mocking example:
import { vi } from 'vitest';// Mock external dependenciesvi.mock('@power-rent/lib/debug', () => ({ default: () => () => {} }));vi.mock('@sentry/nextjs', () => ({ captureException: vi.fn() }));// Mock services in testsconst mockContext = {services: {clientService: {findById: vi.fn().mockResolvedValue(mockData)}},flags: { someFlag: true }};
6.4. Testing Different Layers
-
GraphQL Resolver Testing:
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 Component Testing:
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');});}); -
Utility Function Testing:
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. Testing Commands and Workflow
-
Development Commands:
yarn test run- Run Vitest unit testsyarn test run --coverage --coverage.reporter=html- Run tests with coverage reportyarn test- Run tests in watch mode
-
Coverage Guidelines:
- Target 80-90% coverage for critical paths
- Focus on quality over quantity - meaningful tests over coverage metrics
- Document exclusions for uncovered code with valid reasons
6.6. Error Handling and Edge Cases
-
Always test error scenarios:
it('should handle API errors gracefully', async () => {const mockService = vi.fn().mockRejectedValue(new Error('API Error'));await expect(serviceCall(mockService)).rejects.toThrow('API Error');}); -
Test boundary conditions:
- Null/undefined inputs
- Empty arrays/objects
- Maximum/minimum values
- Invalid data types
-
Async Testing:
it('should handle async operations', async () => {const expected = { some: 'data' };const actual = await asyncFunction();expect(actual).toEqual(expected);});
6.7. Performance and Maintainability
-
Fast Test Execution:
- Tests should run in milliseconds, not seconds
- Vitest runs tests concurrently by default
- Avoid heavy setup operations in beforeEach/afterEach
-
Test Maintenance:
- Update tests when implementation changes
- Remove obsolete or redundant tests
- Keep test data and fixtures current
- Review test performance regularly
6.8. Pre-commit and Code Review
-
Before Committing:
- Test names clearly describe the scenario
- Tests follow AAA pattern
- No external dependencies
- Tests are isolated and independent
- Edge cases are covered
- Error scenarios are tested
- Tests run quickly (< 100ms each)
- Code coverage meets project standards
-
Code Review Checklist:
- Are unit tests/integration tests added for new logic?
- Do tests cover the main business logic and edge cases?
- Are tests readable and maintainable?
7. Code Review: Pull Request Checklist
-
General:
- Is the scope of changes clear? Link to ticket, description of changes.
- Are there any “magic” numbers/strings? (Extract to constants or config)
-
Structure and Architecture:
- Do the changes follow the accepted pattern?
- Is there any code duplication? Can common logic be extracted?
-
Typing:
- Are all functions and variables typed?
- Are there any
$FlowFixMeor@ts-ignorewithout good reason?
-
Security and Validation:
- Are users checked for access rights (Authorization)?
- Is input data validated?
- Are possible errors handled (DB errors, HTTP errors, timeouts)?
-
Tests:
- Are unit tests / integration tests added for new logic?
- Are tests run and does CI pass?
-
Performance:
- Are there any unnecessary expensive operations in component renders (e.g., heavy calculations without memoization or conversely memoization without necessity)?
- When necessary,
React.memo,useMemo,useCallbackare used.
-
Logging and Debugging:
- Are there any unnecessary
console.log/debuggerin production code? - In backend code,
debugis used instead ofconsole.log.
- Are there any unnecessary
-
Documentation:
- Comments only where they really help (not “I’m doing X here”, but “the reason why we do X”).
- Is documentation updated according to changes?
-
Code style:
- Are ESLint and Flow passed without errors?
- Are naming conventions followed (CamelCase, PascalCase, UPPER_SNAKE_CASE)?
Important: PR is considered “ready” for merge only after at least one person has approved the changes (peer review).
8. Documentation
The use of AI for generating documentation is encouraged.
-
Comments in Code:
- Write comments if the method is complex or non-obvious.
- Don’t leave “dead” comments (old code, commented out) — better to delete unnecessary sections.
9. Recommendations for Refactoring Existing Code
-
Identify “hotspots”:
- Identify modules where bugs appear most often (from Git history, tracker tasks).
- Start with them: write simple integration/unit tests to have confidence when refactoring.
-
Small steps (Small Commits):
- Reorganize code gradually: each PR makes one clearly defined fix.
- After each step, run tests and check that nothing is broken.
-
Adding Tests to Old Code:
- Before changing logic, write tests that cover current behavior (even if it’s buggy).
- Then correct the behavior, simultaneously editing the test to test the “correct” result.
-
Isolated Refactoring:
- Extract repeating utilities from components/services to
utils/. - Move “large” business rules to separate layers (e.g., formulate commission calculation rule in
services/commissionService.ts, not incontrollers/orderController.ts).
- Extract repeating utilities from components/services to
-
Removing Unused Code:
- If there are files or functions in the project that are not imported by anyone, safely delete them.
- This reduces the surface area for bugs.
10. Mentoring and Learning
-
Pair Programming:
- Schedule regular sessions where a senior works with a junior on a task.
- This benefits both the development process (reduces bug risk) and junior’s growth.
-
Code Review as a Learning Process:
- Comments during review — not just “fix this”, but “why this way is better”.
- Senior should remain available for questions: it’s better to spend 15 minutes explaining than later dealing with a huge stream of bugs.
11. Using AI for coding
AI coding tools can significantly boost productivity when used correctly. However, they require careful integration into your workflow to maintain code quality and security.
11.1. Prompt Engineering Best Practices
-
Be specific and detailed:
- Craft precise prompts that clearly articulate your requirements.
- Include specific constraints, libraries, performance requirements, and edge cases.
- Bad: “Create a login form”
- Good: “Create a React login form component with email and password validation, using formik, that displays error messages and handles loading states”
-
Break down complex tasks:
- Divide large tasks into smaller, logical steps.
- Ask for one function or component at a time rather than entire modules.
- This leads to better, more focused code generation.
-
Specify expected inputs and outputs:
- Clearly define function parameters, return types, and expected behavior.
- Include examples of expected input/output when helpful.
11.2. Code Integration Workflow
-
Never copy-paste blindly:
- Always read and understand the generated code before using it.
- Verify that the logic aligns with your requirements and coding standards.
- Check for potential security vulnerabilities or performance issues.
-
Follow the iterative refinement process:
- Provide context and create initial prompt
- Review generated code
- Refine prompt based on results
- Test and validate the code
- Make manual adjustments as needed
- Stage the working version before making the next prompt to prevent corrupting the working version
- Repeat until satisfied
11.3. Quality Assurance
-
Mandatory code review:
- All AI-generated code must go through the same review process as human-written code.
- The quality of the generated code can differ from prompt to prompt, so avoid falling into the trap of thinking “Oh, this code looks so good, I’ll just accept it.”
-
Comprehensive testing:
- Write unit tests for all AI-generated functions and components.
- Ensure test coverage meets project standards.
- Pay special attention to edge cases that AI might miss.
-
Documentation requirements:
- Document that code was AI-generated.
- Add comments explaining complex logic or non-obvious decisions.
- Update project documentation to reflect new functionality.
11.4. Security Considerations
-
Validate third-party dependencies:
- Carefully review any libraries or frameworks suggested by AI.
- Check for active maintenance, security vulnerabilities, and community trust.
- Verify that suggested packages actually exist and are legitimate.
-
Be aware of AI hallucinations:
- AI can generate plausible-looking but incorrect code or suggest non-existent libraries.
- Always validate AI suggestions against official documentation.
- Test thoroughly before deployment.
11.5. What AI is Good For
-
Rapid prototyping and boilerplate:
- Generating initial code structure and templates.
- Creating repetitive code patterns (CRUD operations, form validation).
- Setting up configuration files and basic project structure.
-
Code explanation and documentation:
- Generating comments and documentation for existing code.
- Explaining complex algorithms or legacy code.
- Creating README files and API documentation.
-
Refactoring and optimization:
- Suggesting improvements to existing code.
- Converting code between different patterns or frameworks.
- Identifying potential performance issues.
11.6. What to Avoid
-
Don’t use AI for:
- Critical security implementations (authentication, encryption).
- Complex business logic without thorough validation.
- Building new architecture from scratch.
-
Avoid over-dependence:
- Don’t let AI replace your understanding of fundamental concepts.
- Continue learning and staying updated with technology trends.
- Use AI as a tool to enhance, not replace, your coding skills.
11.7. Team Guidelines
-
Knowledge sharing:
- Share effective prompts and techniques with team members.
- Document lessons learned from AI-assisted development.
-
Continuous improvement:
- Regularly evaluate the impact of AI tools on code quality and delivery speed.
- Stay updated with new AI tools and techniques.
Remember: AI is a powerful assistant, but human expertise, judgment, and validation remain essential for building reliable, secure, and maintainable software.
11.8. Good and bad prompts examples
Example 1: Component Creation
❌ Bad prompt:
Create a user card component✅ Good prompt:
Create a React functional component called UserCard that:- Receives user object with properties: id, name, email, avatar, role- Uses Flow/TypeScript for props typing- Displays user avatar (with fallback to initials if no avatar)- Shows name, email, and role with proper styling- Has an onClick handler that passes the user id- Should be responsive for mobile devicesExample 2: API Integration
❌ Bad prompt:
Make an API call to get users✅ Good prompt:
Create a custom React hook called useUsers that:- Fetches users from /api/users endpoint- Returns { users, loading, error, refetch } object- Uses TypeScript with proper typing for User interface- Handles loading and error states- Implements error handling with try-catch- Uses fetch API (not axios)- Includes proper cleanup to prevent memory leaks- Returns empty array as initial state for usersExample 3: Form Validation
❌ Bad prompt:
Add validation to login form✅ Good prompt:
Add validation to existing React login form using Formik:- Email field: required, valid email format, max 100 characters- Password field: required, minimum 8 characters, must contain at least one number- Display validation errors below each field in red text- Disable submit button while form is invalid or submitting- Show loading state during submission- Clear errors when user starts typing- Use our existing TextField component for consistency- Return validation schema as separate exported objectExample 4: Utility Function
❌ Bad prompt:
Format date function✅ Good prompt:
Create a TypeScript utility function formatDate that:- Takes a Date object or ISO string as input- Returns formatted string in "MMM DD, YYYY" format (e.g., "Jan 15, 2024")- Handles invalid dates by returning "Invalid Date"- Uses built-in Intl.DateTimeFormat for internationalization- Has proper TypeScript types for parameters and return value- Includes JSDoc comments with usage examples- Export as named export from utils/dateHelpers.tsExample 5: Database Query
❌ Bad prompt:
Get user data from database✅ Good prompt:
Create a Prisma query function getUserWithReservations that:- Takes userId as string parameter- Returns user data including related reservations- Uses Prisma's include to get reservations with vehicle details- Handles case when user doesn't exist (return null)- Includes proper TypeScript return type- Orders reservations by createdAt descending- Limits reservations to last 10 entries- Uses try-catch for error handling- Follows our existing service pattern in services/UserService.tsExample 6: Refactoring Request
❌ Bad prompt:
Improve this code✅ Good prompt:
Refactor this React component to improve performance and readability:
[attach file to the context or provide the file relative path]
Requirements:- Extract repeated logic into custom hooks- Implement proper memoization where needed- Split into smaller sub-components if too complex- Add proper Flow/TypeScript types- Follow our naming conventions (camelCase for functions, PascalCase for components)- Maintain existing functionality exactly- Add comments for complex logic- Ensure all props are properly typedExample 7: Testing
❌ Bad prompt:
Write tests for this component✅ Good prompt:
Write comprehensive Vitest tests for UserCard component using React Testing Library:
[attach file to the context or provide the file relative path]
Test cases needed:- Renders user information correctly- Shows fallback initials when no avatar provided- Calls onClick handler with correct user id- Handles missing user properties gracefully- Tests responsive behavior if applicable- Uses proper test descriptions and organize in describe blocks- Mock any external dependencies- Follow our existing test patterns from other component tests- Use TypeScript for test filesKey Patterns for Effective Prompts:
-
Start with action and object: “Create a React component…”, “Write a function that…”, “Refactor this code to…”
-
Include context: Mention the framework, language, existing patterns, and file structure
-
List specific requirements: Use bullet points for clarity and completeness
-
Specify constraints: Performance needs, browser support, accessibility requirements
-
Define expected behavior: Include edge cases and error handling
-
Mention existing patterns: Reference similar code in the project for consistency
-
Be explicit about types: Specify Flow/TypeScript return types or parameter types
-
Include examples: Show expected input/output when helpful
12. Feature Flags Best Practices
Feature flags are a powerful software development technique that allows teams to control feature visibility and behavior at runtime without code deployments.
⚠️ IMPORTANT: Every new feature or core functionality change MUST be covered with a feature flag. This is a mandatory requirement for all code changes that affect user-facing functionality or core system behavior.
This requirement ensures:
- Safe and controlled feature rollouts
- Ability to quickly disable problematic features
- Gradual rollout to specific user segments
- Easy rollback in case of issues
Exceptions to this rule must be approved by the technical lead and documented in the pull request description.
12.1. Core Principles
- Make flags short-lived:
- Most feature flags should be temporary and removed after successful rollout
- Treat feature flags like technical debt that needs cleanup
12.2. How to use
Our feature flag system uses Statsig with two different packages depending on the context:
@statsig/react-bindingsfor client-side React components@flags-sdk/statsigfor server-side API routes and GraphQL resolvers
12.2.1. Client-Side Usage (React Components)
For client-side components, use the Statsig React hooks:
Feature Gates (Boolean Flags):
// @flowimport { useGateValue } from '@statsig/react-bindings';
export default function MyComponent() { const isNewFeatureEnabled = useGateValue('new_checkout_flow');
return ( <div> {isNewFeatureEnabled ? ( <NewCheckoutFlow /> ) : ( <LegacyCheckoutFlow /> )} </div> );}Dynamic Configuration:
// @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}>Click me</Button> <ItemsList limit={maxItems} /> </div> );}12.2.2. Server-Side Usage (API Routes & GraphQL)
For server-side usage in API routes and GraphQL resolvers, use the server SDK:
In API Routes:
// @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'] || '';
// Method 1: Using the centralized helper const flags = await getFeatureFlags({ stableId, tenantId }); if (flags.newBillingSystem) { // Use new billing logic }
// Method 2: Direct Statsig usage const user = { userID: tenantId, customIDs: { stableID: stableId }, };
const isEnabled = Statsig.checkGateWithExposureLoggingDisabledSync( user, 'new_api_endpoint' );
if (isEnabled) { return res.json({ message: 'New API logic' }); }
return res.json({ message: 'Legacy API logic' });}In GraphQL Resolvers:
// @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. User Identification
The system uses a stable ID approach for consistent feature flag evaluation:
- A stable ID is stored in a browser cookie
- If no stable ID exists, a new one is generated using
crypto.randomBytes() - The stable ID ensures users get consistent feature flag experiences across sessions
- Additional user context (tenant ID, custom attributes) can be added for targeting
12.2.4. When to Use Which Approach
useGateValue() vs useGate():
// ✅ Use useGateValue() for simple boolean checksconst isEnabled = useGateValue('simple_feature');if (isEnabled) { return <NewFeature />;}
// ✅ Use useGate() when you need metadata or loading statesconst { 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 - Simple on/off switchesconst showNewButton = useGateValue('new_button_design');
// ✅ Dynamic Configs - Multiple parameters that change togetherconst { config } = useConfig('checkout_settings');const maxItems = config?.value?.maxItems || 5;const timeout = config?.value?.timeout || 30000;const buttonColor = config?.value?.buttonColor || 'blue';Client-Side vs Server-Side Implementation:
// ✅ Client-side: UI changes, user experience featuresconst showNewNav = useGateValue('new_navigation');
// ✅ Server-side: Database routing, API behavior, business logicconst flags = await getFeatureFlags({ stableId, tenantId });if (flags.useNewPaymentFlow) { return processPaymentV2(paymentData);}When to Use Each Hook Type:
| Hook | Use Case | Example |
|---|---|---|
useGateValue() | Simple boolean feature toggles | Show/hide UI elements |
useGate() | Need loading states or metadata | Complex features with analytics |
useConfig() | Multiple related settings | Theme settings, API endpoints |
12.2.5. Best Practices for Usage
✅ Do:
// Use descriptive flag namesconst isNewDashboardEnabled = useGateValue('new_dashboard_v2');
// Handle loading states when availableconst { config, isLoading } = useConfig('complex_feature');if (isLoading) return <LoadingSpinner />;
// Provide fallbacksconst maxRetries = config?.value?.maxRetries ?? 3;
// Use server-side flags for database routingconst flags = await getFeatureFlags({ stableId, tenantId });if (flags.usePostgres) { return queryPostgres();}❌ Don’t:
// Don't create complex nested conditionsif (useGateValue('a') && useGateValue('b') && useGateValue('c')) { // Too complex - use a single flag or config instead}❌ Don’t:
// Add new feature without a flagnewFeatureThatSeemsToBeSafe(data);12.3 Flag Naming Conventions
✅ Good Examples:
new_checkout_flow- Clear feature descriptionai_recommendations_enabled- Describes functionalitybilling_api_v2_rollout- Indicates rollout purposedebug_mode_internal- Shows target audience
❌ Bad Examples:
flag1- Non-descriptiveexperiment- Too vaguetemp_flag- Unclear purposejohns_test- Personal reference
12.4 Documentation and Team Guidelines
- Document flag purpose and rollout plan:
/*** Purpose: Gradual rollout of a new example feature* Rollout Plan:* - Week 1: Internal users only (0.1%)* - Week 2: Beta customers (5%)* - Week 3: All premium users (25%)* - Week 4: Full rollout (100%)** Cleanup Date: 2025-07-01* Owner: @github-user-name* Ticket: TOP-123*/export const exampleFlag = flag<boolean>({key: 'example_flag',...});
13. Handling Validation Errors
When handling user input validation failures, use dedicated error classes/helpers that prevent Sentry reporting and return appropriate HTTP status codes. Validation errors should be distinguished from system errors to keep monitoring clean and meaningful.
API Endpoints
Use the ValidationError class from @power-rent/lib/errors for all validation failures.
import { ValidationError } from '@power-rent/lib/errors';
try { if (!widgetId) { throw new ValidationError('No widgetId provided'); }
if (!fromDate.isValid || !toDate.isValid) { throw new ValidationError('Invalid date format'); }
if (fromDate.toMillis() < today.toMillis()) { throw new ValidationError('Invalid dateFrom, requests in past not allowed'); }
// ... business logic} catch (error) { if (error instanceof ValidationError) { res.status(400).json({ error: error.message }); return; } // Only system errors reach Sentry console.error(error); Sentry.captureException(error); res.status(500).json({ error: 'Internal server error' });}GraphQL Resolvers
Use the validationError helper from @power-rent/server/graphql/helpers/errors to return validation failures with optional error codes.
import { validationError } from '@power-rent/server/graphql/helpers/errors';
if (isExpired) { return validationError( `[${codes.subscriptionExpired}]: Your subscription has expired` );}
if (!isValidPhoneNumber) { return validationError( 'Invalid phone number', codes.invalidPhoneNumber );}Key Points:
- Never use generic
ErrororApiErrorfor validation failures - Don’t send ValidationErrors to Sentry (reducing noise)
- Always return HTTP 400 for api request validation errors, HTTP 500 for system errors
- Never throw on validation error inside resolvers
- Include clear error messages