Coding Conventions
Coding Conventions
This guide covers the coding standards and best practices for contributing to Global Watch. Following these conventions ensures consistency across the codebase and makes code reviews more efficient.
TypeScript Conventions
Strict Type Safety
Global Watch enforces strict TypeScript. Never use any without explicit justification.
// ❌ WRONG - Using any
const data: any = getData();
// ✅ CORRECT - Using proper types
import type { Database } from '~/lib/database.types';
type ProjectRow = Database['public']['Tables']['projects']['Row'];
const data: ProjectRow = getData();
// ✅ CORRECT - Using unknown with type guards
function process(data: unknown) {
if (typeof data === 'object' && data !== null) {
// Type-safe processing
}
}Type Imports
Always use import type for type-only imports:
// ✅ CORRECT
import type { Project } from '~/types';
import { createProject } from '~/lib/projects';
// ❌ WRONG
import { Project, createProject } from '~/lib/projects';Explicit Return Types
Always specify return types for exported functions:
// ✅ CORRECT
export function calculateArea(geometry: Geometry): number {
// ...
}
// ❌ WRONG - Missing return type
export function calculateArea(geometry: Geometry) {
// ...
}Component Organization
File Structure
Organize components following this pattern:
feature/
├── page.tsx # Page component
├── layout.tsx # Layout wrapper
├── loading.tsx # Loading state
├── error.tsx # Error boundary
├── _components/ # Route-specific components
│ ├── feature-header.tsx
│ └── feature-list.tsx
└── _lib/
├── server/ # Server-only code
│ ├── feature-page.loader.ts
│ └── feature-server-actions.ts
└── schemas/ # Zod validation schemas
└── feature.schema.tsServer vs Client Components
Use Server Components by default. Only add 'use client' when necessary:
// Server Component (default) - for data fetching
async function ProjectList() {
const projects = await getProjects();
return <ProjectListClient projects={projects} />;
}
// Client Component - for interactivity
'use client';
function ProjectListClient({ projects }: { projects: Project[] }) {
const [filter, setFilter] = useState('');
// Interactive logic
}Component Naming
- PascalCase for component names
- Descriptive names that indicate purpose
- Suffix with context when needed (e.g.,
ProjectListClient)
// ✅ CORRECT
export function ProjectCard({ project }: ProjectCardProps) {}
export function TeamMemberList({ members }: TeamMemberListProps) {}
// ❌ WRONG
export function Card({ data }: any) {}
export function List({ items }: any) {}File Naming Conventions
General Rules
| Type | Convention | Example |
|---|---|---|
| Components | kebab-case.tsx | project-card.tsx |
| Utilities | kebab-case.ts | date-utils.ts |
| Types | kebab-case.ts | project.types.ts |
| Tests | *.test.ts(x) | project-card.test.tsx |
| Server Actions | *-server-actions.ts | project-server-actions.ts |
| Loaders | *-page.loader.ts | project-page.loader.ts |
| Schemas | *.schema.ts | project.schema.ts |
Directory Naming
- Use
_components/for route-specific components (underscore prefix) - Use
_lib/for route-specific utilities - Use
kebab-casefor all directories
Import Organization
Organize imports in this order:
// 1. React/Next.js imports
import { use, useState } from 'react';
import { notFound } from 'next/navigation';
// 2. External packages
import { z } from 'zod';
import { useQuery } from '@tanstack/react-query';
// 3. Internal packages (@kit/*, @fw/*)
import { Button } from '@kit/ui/button';
import { EntitySettings } from '@fw/entity-settings';
// 4. Local imports (relative paths)
import { ProjectCard } from './_components/project-card';
import type { Project } from './types';Error Handling
Result Type Pattern
Use the Result type for explicit error handling:
import { Result } from '~/core/shared/result';
// ✅ CORRECT - Return Result type
async function findProject(id: string): Promise<Result<Project, ProjectNotFoundError>> {
const { data, error } = await supabase
.from('projects')
.select('*')
.eq('id', id)
.maybeSingle();
if (error || !data) {
return Result.err(new ProjectNotFoundError(id));
}
return Result.ok(Project.fromPersistence(data));
}
// ✅ CORRECT - Handle Result
const result = await findProject(projectId);
if (!result.ok) {
console.error(result.error.message);
return;
}
// TypeScript knows result.value exists
console.log(result.value.name);Custom Error Types
Create specific error types for different failure modes:
export class ProjectError extends Error {
constructor(message: string, public readonly code: string) {
super(message);
this.name = 'ProjectError';
}
}
export class ProjectNotFoundError extends ProjectError {
constructor(projectId: string) {
super(`Project not found: ${projectId}`, 'PROJECT_NOT_FOUND');
}
}Internationalization (i18n)
Always Use Translations
Never hardcode user-facing text. Always use translation keys:
// ❌ WRONG - Hardcoded text
<Button>Save</Button>
<p>No images available</p>
// ✅ CORRECT - Using translations
<Button>{t('common.save')}</Button>
<p>{t('map.toolbar.noImagesAvailable')}</p>Translation Files
Add translations to all supported languages:
apps/web/public/locales/en/- Englishapps/web/public/locales/pt-BR/- Portugueseapps/web/public/locales/ar/- Arabic
Testing Requirements
Test Coverage
- 70% minimum overall coverage
- 90%+ coverage for critical components
- Write both unit tests and property-based tests
Test File Location
Co-locate tests with source files:
components/
├── project-card.tsx
└── project-card.test.tsxTest Naming
Use descriptive test names:
// ✅ CORRECT
describe('ProjectCard', () => {
it('should display project name and area', () => {});
it('should show archived badge when project is archived', () => {});
it('should call onEdit when edit button is clicked', () => {});
});
// ❌ WRONG
describe('ProjectCard', () => {
it('works', () => {});
it('test 1', () => {});
});Git Conventions
Commit Message Format
Use conventional commits:
type(scope): Brief description
[Optional body with more details]
[Optional footer]Types:
| Type | Description |
|---|---|
feat | New feature |
fix | Bug fix |
style | Styling changes |
refactor | Code refactoring |
docs | Documentation |
chore | Maintenance tasks |
test | Adding tests |
Examples:
feat(projects): add project archiving functionality
fix(auth): resolve session timeout issue
docs(api): update webhook documentation
refactor(billing): extract billing strategy patternBranch Naming
Use descriptive branch names:
# Feature branches
feature/project-archiving
feature/team-invitations
# Bug fixes
fix/session-timeout
fix/billing-calculation
# Documentation
docs/api-referencePerformance Guidelines
Bundle Size
- Import directly from source, avoid barrel files
- Use dynamic imports for heavy components
// ✅ CORRECT - Direct import
import PanelLeftIcon from 'lucide-react/dist/esm/icons/panel-left';
// ❌ WRONG - Barrel import
import { PanelLeftIcon } from 'lucide-react';Re-renders
- Use functional setState for stable callbacks
- Memoize context values
// ✅ CORRECT - Stable callback
const toggle = useCallback(() => {
setOpen((prev) => !prev);
}, []);
// ❌ WRONG - Creates new function on state change
const toggle = useCallback(() => {
setOpen(!open);
}, [open]);Lazy State Initialization
// ✅ CORRECT - Function called once
const [state, setState] = useState(() => expensiveComputation());
// ❌ WRONG - Computed on every render
const [state, setState] = useState(expensiveComputation());Code Review Checklist
Before submitting a PR, verify:
- TypeScript has no errors (
pnpm typecheck) - Lint passes (
pnpm lint:fix) - Code is formatted (
pnpm format:fix) - Tests pass (
pnpm --filter web test:run) - No
anytypes without justification - All text uses i18n translations
- Components follow naming conventions
- Imports are properly organized
- Error handling uses Result type
- Documentation is updated if needed
Next Steps
- Setup Guide - Configure your environment
- Architecture Overview - Understand the system design
- Testing Documentation - Learn about testing strategies