Files
syndarix/frontend/docs/CODING_STANDARDS.md
Felipe Cardoso 19ecd04a41 Add foundational API client, UI components, and state management setup
- Created `generate-api-client.sh` for OpenAPI-based TypeScript client generation.
- Added `src/lib/api` with Axios-based API client, error handling utilities, and placeholder for generated types.
- Implemented Zustand-based `authStore` for user authentication and token management.
- Integrated reusable UI components (e.g., `Dialog`, `Select`, `Textarea`, `Sheet`, `Separator`, `Checkbox`) using Radix UI and utility functions.
- Established groundwork for client-server integration, state management, and modular UI development.
2025-10-31 21:46:03 +01:00

28 KiB
Executable File

Frontend Coding Standards

Project: Next.js + FastAPI Template Version: 1.0 Last Updated: 2025-10-31 Status: Enforced


Table of Contents

  1. TypeScript Standards
  2. React Component Standards
  3. Naming Conventions
  4. File Organization
  5. State Management
  6. API Integration
  7. Form Handling
  8. Styling Standards
  9. Error Handling
  10. Testing Standards
  11. Accessibility Standards
  12. Performance Best Practices
  13. Security Best Practices
  14. Code Review Checklist

1. TypeScript Standards

1.1 General Principles

DO:

  • Enable strict mode in tsconfig.json
  • Define explicit types for all function parameters and return values
  • Use TypeScript's type inference where obvious
  • Prefer interface for object shapes, type for unions/primitives
  • Use generics for reusable, type-safe components and functions

DON'T:

  • Use any (use unknown if type is truly unknown)
  • Use as any casts (refactor to proper types)
  • Use @ts-ignore or @ts-nocheck (fix the underlying issue)
  • Leave implicit any types

1.2 Interfaces vs Types

Use interface for object shapes:

// ✅ Good
interface User {
  id: string;
  name: string;
  email: string;
  isActive: boolean;
}

interface UserFormProps {
  user?: User;
  onSubmit: (data: User) => void;
  isLoading?: boolean;
}

Use type for unions, intersections, and primitives:

// ✅ Good
type UserRole = 'admin' | 'user' | 'guest';
type UserId = string;
type UserOrNull = User | null;
type UserWithPermissions = User & { permissions: string[] };

1.3 Function Signatures

Always type function parameters and return values:

// ✅ Good
function formatUserName(user: User): string {
  return `${user.firstName} ${user.lastName}`;
}

async function fetchUser(id: string): Promise<User> {
  const response = await apiClient.get<User>(`/users/${id}`);
  return response.data;
}

// ❌ Bad
function formatUserName(user) {  // Implicit any
  return `${user.firstName} ${user.lastName}`;
}

1.4 Generics

Use generics for reusable, type-safe code:

// ✅ Good: Generic data table
interface DataTableProps<T> {
  data: T[];
  columns: Column<T>[];
  onRowClick?: (row: T) => void;
}

export function DataTable<T>({ data, columns, onRowClick }: DataTableProps<T>) {
  // Implementation
}

// Usage is fully type-safe
<DataTable<User> data={users} columns={userColumns} />

1.5 Unknown vs Any

Use unknown for truly unknown types:

// ✅ Good: Force type checking
function parseJSON(jsonString: string): unknown {
  return JSON.parse(jsonString);
}

const result = parseJSON('{"name": "John"}');
// Must narrow type before use
if (typeof result === 'object' && result !== null && 'name' in result) {
  console.log(result.name);
}

// ❌ Bad: Bypasses all type checking
function parseJSON(jsonString: string): any {
  return JSON.parse(jsonString);
}

1.6 Type Guards

Create type guards for runtime type checking:

// ✅ Good
function isUser(value: unknown): value is User {
  return (
    typeof value === 'object' &&
    value !== null &&
    'id' in value &&
    'email' in value
  );
}

// Usage
if (isUser(data)) {
  // TypeScript knows data is User here
  console.log(data.email);
}

1.7 Utility Types

Use TypeScript utility types:

// Partial - make all properties optional
type UserUpdate = Partial<User>;

// Pick - select specific properties
type UserPreview = Pick<User, 'id' | 'name' | 'email'>;

// Omit - exclude specific properties
type UserWithoutPassword = Omit<User, 'password'>;

// Record - create object type with specific keys
type UserRolePermissions = Record<UserRole, string[]>;

// Extract - extract from union
type AdminRole = Extract<UserRole, 'admin'>;

// Exclude - exclude from union
type NonAdminRole = Exclude<UserRole, 'admin'>;

2. React Component Standards

2.1 Component Structure

Standard component template:

// 1. Imports (React, external libs, internal, types, styles)
'use client';  // If client component

import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { Button } from '@/components/ui/button';
import { useUsers } from '@/lib/api/hooks/useUsers';

// 2. Types
interface UserListProps {
  initialFilters?: UserFilters;
  onUserSelect?: (user: User) => void;
  className?: string;
}

// 3. Component
export function UserList({
  initialFilters,
  onUserSelect,
  className
}: UserListProps) {
  // 3a. Hooks (state, effects, router, query)
  const router = useRouter();
  const [filters, setFilters] = useState(initialFilters);
  const { data, isLoading, error } = useUsers(filters);

  // 3b. Derived state
  const hasUsers = data && data.length > 0;
  const isEmpty = !isLoading && !hasUsers;

  // 3c. Effects
  useEffect(() => {
    // Side effects
  }, [dependencies]);

  // 3d. Event handlers
  const handleUserClick = (user: User) => {
    onUserSelect?.(user);
  };

  // 3e. Render conditions
  if (isLoading) return <LoadingSpinner />;
  if (error) return <ErrorMessage error={error} />;
  if (isEmpty) return <EmptyState message="No users found" />;

  // 3f. Main render
  return (
    <div className={className}>
      {/* JSX */}
    </div>
  );
}

2.2 Server Components vs Client Components

Server Components by default:

// ✅ Good: Server Component (default)
// app/(authenticated)/users/page.tsx
export default async function UsersPage() {
  // Could fetch data here, but we use client components with React Query
  return (
    <div>
      <PageHeader title="Users" />
      <UserList />
    </div>
  );
}

Client Components only when needed:

// ✅ Good: Client Component (interactive)
'use client';

import { useState } from 'react';
import { useUsers } from '@/lib/api/hooks/useUsers';

export function UserList() {
  const [search, setSearch] = useState('');
  const { data } = useUsers({ search });

  return (
    <div>
      <input value={search} onChange={(e) => setSearch(e.target.value)} />
      {/* Render users */}
    </div>
  );
}

When to use Client Components:

  • Using React hooks (useState, useEffect, etc.)
  • Event handlers (onClick, onChange, etc.)
  • Browser APIs (window, document, localStorage)
  • React Context consumers

2.3 Props

Always type props explicitly:

// ✅ Good: Explicit interface
interface ButtonProps {
  label: string;
  onClick: () => void;
  variant?: 'primary' | 'secondary';
  disabled?: boolean;
  className?: string;
  children?: React.ReactNode;
}

export function Button({
  label,
  onClick,
  variant = 'primary',
  disabled = false,
  className
}: ButtonProps) {
  // Implementation
}

Destructure props in function signature:

// ✅ Good
function UserCard({ user, onEdit }: UserCardProps) {
  return <div>{user.name}</div>;
}

// ❌ Bad
function UserCard(props: UserCardProps) {
  return <div>{props.user.name}</div>;
}

Allow className override:

// ✅ Good: Allow consumers to add classes
interface CardProps {
  title: string;
  className?: string;
}

export function Card({ title, className }: CardProps) {
  return (
    <div className={cn('default-classes', className)}>
      {title}
    </div>
  );
}

2.4 Composition Over Prop Drilling

Use composition patterns:

// ✅ Good: Composition
<Card>
  <CardHeader>
    <CardTitle>Users</CardTitle>
    <CardDescription>Manage system users</CardDescription>
  </CardHeader>
  <CardContent>
    <UserTable />
  </CardContent>
  <CardFooter>
    <Button>Create User</Button>
  </CardFooter>
</Card>

// ❌ Bad: Complex props
<Card
  title="Users"
  description="Manage system users"
  content={<UserTable />}
  footerAction={<Button>Create User</Button>}
/>

2.5 Named Exports vs Default Exports

Prefer named exports:

// ✅ Good: Named export
export function UserList() {
  // Implementation
}

// Easier to refactor, better IDE support, explicit imports
import { UserList } from './UserList';

// ❌ Avoid: Default export
export default function UserList() {
  // Implementation
}

// Can be renamed on import, less explicit
import WhateverName from './UserList';

Exception: Next.js pages must use default export

// pages and route handlers require default export
export default function UsersPage() {
  return <div>Users</div>;
}

2.6 Custom Hooks

Extract reusable logic:

// ✅ Good: Custom hook
function useDebounce<T>(value: T, delay: number): T {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const handler = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    return () => {
      clearTimeout(handler);
    };
  }, [value, delay]);

  return debouncedValue;
}

// Usage
function SearchInput() {
  const [search, setSearch] = useState('');
  const debouncedSearch = useDebounce(search, 500);

  const { data } = useUsers({ search: debouncedSearch });

  return <input value={search} onChange={(e) => setSearch(e.target.value)} />;
}

Hook naming: Always prefix with "use":

// ✅ Good
function useAuth() { }
function useUsers() { }
function useDebounce() { }

// ❌ Bad
function auth() { }
function getUsers() { }

3. Naming Conventions

3.1 Files and Directories

Type Convention Example
Components PascalCase UserTable.tsx, LoginForm.tsx
Hooks camelCase with use prefix useAuth.ts, useDebounce.ts
Utilities camelCase formatDate.ts, parseError.ts
Types camelCase user.ts, api.ts
Constants camelCase or UPPER_SNAKE_CASE constants.ts, API_ENDPOINTS.ts
Stores camelCase with Store suffix authStore.ts, uiStore.ts
Services camelCase with Service suffix authService.ts, adminService.ts
Pages (Next.js) lowercase page.tsx, layout.tsx, loading.tsx

3.2 Variables and Functions

Variables:

// ✅ Good: camelCase
const userName = 'John';
const isAuthenticated = true;
const userList = [];

// ❌ Bad
const UserName = 'John';  // PascalCase for variable
const user_name = 'John'; // snake_case

Functions:

// ✅ Good: camelCase, descriptive verb + noun
function getUserById(id: string): User { }
function formatDate(date: Date): string { }
function handleSubmit(data: FormData): void { }

// ❌ Bad
function User(id: string) { }      // Looks like a class
function get_user(id: string) { }  // snake_case
function gub(id: string) { }       // Not descriptive

Event Handlers:

// ✅ Good: handle + EventName
const handleClick = () => { };
const handleSubmit = () => { };
const handleInputChange = () => { };

// ❌ Bad
const onClick = () => { };  // Confusing with prop name
const submit = () => { };

Boolean Variables:

// ✅ Good: is/has/should prefix
const isLoading = true;
const hasError = false;
const shouldRedirect = true;
const canEdit = false;

// ❌ Bad
const loading = true;
const error = false;

3.3 Constants

Use UPPER_SNAKE_CASE for true constants:

// ✅ Good
const MAX_RETRY_ATTEMPTS = 3;
const API_BASE_URL = 'https://api.example.com';
const DEFAULT_PAGE_SIZE = 20;

// Constants object
const USER_ROLES = {
  ADMIN: 'admin',
  USER: 'user',
  GUEST: 'guest',
} as const;

3.4 Types and Interfaces

PascalCase for types and interfaces:

// ✅ Good
interface User { }
interface UserFormProps { }
type UserId = string;
type UserRole = 'admin' | 'user';

// ❌ Bad
interface user { }
interface user_form_props { }
type userId = string;

4. File Organization

4.1 Import Order

Organize imports in this order:

// 1. React and Next.js
import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import Image from 'next/image';

// 2. External libraries
import { useQuery } from '@tanstack/react-query';
import { toast } from 'sonner';
import { format } from 'date-fns';

// 3. Internal components (UI first, then features)
import { Button } from '@/components/ui/button';
import { Card } from '@/components/ui/card';
import { UserTable } from '@/components/admin/UserTable';

// 4. Internal hooks and utilities
import { useAuth } from '@/hooks/useAuth';
import { useUsers } from '@/lib/api/hooks/useUsers';
import { cn } from '@/lib/utils/cn';

// 5. Types
import type { User, UserFilters } from '@/types/user';

// 6. Styles (if any)
import styles from './Component.module.css';

4.2 Co-location

Group related files together:

components/admin/
├── UserTable/
│   ├── UserTable.tsx
│   ├── UserTable.test.tsx
│   ├── UserTableRow.tsx
│   ├── UserTableFilters.tsx
│   └── index.ts  (re-export)

Or flat structure for simpler components:

components/admin/
├── UserTable.tsx
├── UserTable.test.tsx
├── UserForm.tsx
├── UserForm.test.tsx

4.3 Barrel Exports

Use index.ts for clean imports:

// components/ui/index.ts
export { Button } from './button';
export { Card, CardHeader, CardContent } from './card';
export { Input } from './input';

// Usage
import { Button, Card, Input } from '@/components/ui';

5. State Management

5.1 State Placement

Keep state as local as possible:

// ✅ Good: Local state
function UserFilter() {
  const [search, setSearch] = useState('');
  return <input value={search} onChange={(e) => setSearch(e.target.value)} />;
}

// ❌ Bad: Unnecessary global state
// Don't put search in Zustand store

5.2 TanStack Query Usage

Standard query pattern:

// lib/api/hooks/useUsers.ts
export function useUsers(filters?: UserFilters) {
  return useQuery({
    queryKey: ['users', filters],
    queryFn: () => UserService.getUsers(filters),
    staleTime: 60000, // 1 minute
  });
}

// Component usage
function UserList() {
  const { data, isLoading, error, refetch } = useUsers({ search: 'john' });

  if (isLoading) return <LoadingSpinner />;
  if (error) return <ErrorMessage />;

  return <div>{/* Render data */}</div>;
}

Standard mutation pattern:

// lib/api/hooks/useUsers.ts
export function useUpdateUser() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: ({ id, data }: { id: string; data: UpdateUserDto }) =>
      UserService.updateUser(id, data),
    onSuccess: (_, { id }) => {
      // Invalidate queries to trigger refetch
      queryClient.invalidateQueries({ queryKey: ['users', id] });
      queryClient.invalidateQueries({ queryKey: ['users'] });
      toast.success('User updated successfully');
    },
    onError: (error: APIError[]) => {
      toast.error(error[0]?.message || 'Failed to update user');
    },
  });
}

// Component usage
function UserForm({ userId }: { userId: string }) {
  const updateUser = useUpdateUser();

  const handleSubmit = (data: UpdateUserDto) => {
    updateUser.mutate({ id: userId, data });
  };

  return (
    <form onSubmit={handleSubmit}>
      {/* Form fields */}
      <button type="submit" disabled={updateUser.isPending}>
        {updateUser.isPending ? 'Saving...' : 'Save'}
      </button>
    </form>
  );
}

Query key structure:

// ✅ Good: Consistent query keys
['users']                              // List all
['users', userId]                      // Single user
['users', { search: 'john', page: 1 }] // Filtered list
['organizations', orgId, 'members']    // Nested resource

// ❌ Bad: Inconsistent
['userList']
['user-' + userId]
['getUsersBySearch', 'john']

5.3 Zustand Store Pattern

Auth store example:

// stores/authStore.ts
import { create } from 'zustand';
import { persist } from 'zustand/middleware';

interface AuthStore {
  user: User | null;
  accessToken: string | null;
  isAuthenticated: boolean;

  setUser: (user: User | null) => void;
  setTokens: (accessToken: string, refreshToken: string) => void;
  clearAuth: () => void;
}

export const useAuthStore = create<AuthStore>()(
  persist(
    (set) => ({
      user: null,
      accessToken: null,
      isAuthenticated: false,

      setUser: (user) => set({ user, isAuthenticated: !!user }),

      setTokens: (accessToken, refreshToken) => {
        set({ accessToken });
        // Store refresh token separately (localStorage or cookie)
        localStorage.setItem('refreshToken', refreshToken);
      },

      clearAuth: () => {
        set({ user: null, accessToken: null, isAuthenticated: false });
        localStorage.removeItem('refreshToken');
      },
    }),
    {
      name: 'auth-storage',
      partialize: (state) => ({ user: state.user }), // Only persist user
    }
  )
);

// Usage with selector (performance optimization)
function UserAvatar() {
  const user = useAuthStore((state) => state.user);
  return <Avatar src={user?.avatar} />;
}

UI store example:

// stores/uiStore.ts
interface UIStore {
  sidebarOpen: boolean;
  theme: 'light' | 'dark' | 'system';

  setSidebarOpen: (open: boolean) => void;
  toggleSidebar: () => void;
  setTheme: (theme: 'light' | 'dark' | 'system') => void;
}

export const useUIStore = create<UIStore>()(
  persist(
    (set) => ({
      sidebarOpen: true,
      theme: 'system',

      setSidebarOpen: (open) => set({ sidebarOpen: open }),
      toggleSidebar: () => set((state) => ({ sidebarOpen: !state.sidebarOpen })),
      setTheme: (theme) => set({ theme }),
    }),
    { name: 'ui-storage' }
  )
);

6. API Integration

6.1 API Client Structure

Axios instance configuration:

// lib/api/client.ts
import axios from 'axios';
import { useAuthStore } from '@/stores/authStore';

export const apiClient = axios.create({
  baseURL: process.env.NEXT_PUBLIC_API_URL,
  timeout: 30000,
  headers: {
    'Content-Type': 'application/json',
  },
});

// Request interceptor
apiClient.interceptors.request.use(
  (config) => {
    const token = useAuthStore.getState().accessToken;
    if (token) {
      config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
  },
  (error) => Promise.reject(error)
);

// Response interceptor
apiClient.interceptors.response.use(
  (response) => response,
  async (error) => {
    if (error.response?.status === 401) {
      // Handle token refresh
      try {
        await refreshToken();
        return apiClient.request(error.config);
      } catch {
        useAuthStore.getState().clearAuth();
        window.location.href = '/login';
      }
    }
    return Promise.reject(parseAPIError(error));
  }
);

6.2 Error Handling

Parse API errors:

// lib/api/errors.ts
export interface APIError {
  code: string;
  message: string;
  field?: string;
}

export function parseAPIError(error: AxiosError): APIError[] {
  if (error.response?.data?.errors) {
    return error.response.data.errors;
  }

  return [{
    code: 'UNKNOWN',
    message: error.message || 'An unexpected error occurred',
  }];
}

// Error code mapping
export const ERROR_MESSAGES: Record<string, string> = {
  'AUTH_001': 'Invalid email or password',
  'USER_002': 'This email is already registered',
  'VAL_001': 'Please check your input',
  'ORG_001': 'Organization name already exists',
};

export function getErrorMessage(code: string): string {
  return ERROR_MESSAGES[code] || 'An error occurred';
}

6.3 Hook Organization

One hook file per resource:

// lib/api/hooks/useUsers.ts
export function useUsers(filters?: UserFilters) { }
export function useUser(userId: string) { }
export function useCreateUser() { }
export function useUpdateUser() { }
export function useDeleteUser() { }

// lib/api/hooks/useOrganizations.ts
export function useOrganizations() { }
export function useOrganization(orgId: string) { }
export function useCreateOrganization() { }
// ...

7. Form Handling

7.1 Form Pattern with react-hook-form + Zod

Standard form implementation:

// components/auth/LoginForm.tsx
'use client';

import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';

// 1. Define schema
const loginSchema = z.object({
  email: z.string().email('Invalid email address'),
  password: z.string().min(8, 'Password must be at least 8 characters'),
});

type LoginFormData = z.infer<typeof loginSchema>;

// 2. Component
export function LoginForm() {
  const form = useForm<LoginFormData>({
    resolver: zodResolver(loginSchema),
    defaultValues: {
      email: '',
      password: '',
    },
  });

  // 3. Submit handler
  const onSubmit = async (data: LoginFormData) => {
    try {
      await authService.login(data);
      router.push('/dashboard');
    } catch (error) {
      form.setError('root', {
        message: 'Invalid credentials',
      });
    }
  };

  // 4. Render
  return (
    <form onSubmit={form.handleSubmit(onSubmit)}>
      <Input
        {...form.register('email')}
        type="email"
        placeholder="Email"
        error={form.formState.errors.email?.message}
      />
      <Input
        {...form.register('password')}
        type="password"
        placeholder="Password"
        error={form.formState.errors.password?.message}
      />
      {form.formState.errors.root && (
        <ErrorMessage>{form.formState.errors.root.message}</ErrorMessage>
      )}
      <Button type="submit" disabled={form.formState.isSubmitting}>
        {form.formState.isSubmitting ? 'Logging in...' : 'Login'}
      </Button>
    </form>
  );
}

7.2 Form Validation

Complex validation with Zod:

const userSchema = z.object({
  email: z.string().email('Invalid email'),
  password: z
    .string()
    .min(8, 'Min 8 characters')
    .regex(/[A-Z]/, 'Must contain uppercase')
    .regex(/[0-9]/, 'Must contain number'),
  confirmPassword: z.string(),
  firstName: z.string().min(1, 'Required'),
  lastName: z.string().optional(),
  phoneNumber: z
    .string()
    .regex(/^\+?[1-9]\d{1,14}$/, 'Invalid phone number')
    .optional(),
}).refine((data) => data.password === data.confirmPassword, {
  message: 'Passwords do not match',
  path: ['confirmPassword'],
});

7.3 Form Accessibility

Always include labels and error messages:

<div>
  <Label htmlFor="email">Email</Label>
  <Input
    id="email"
    {...form.register('email')}
    aria-invalid={!!form.formState.errors.email}
    aria-describedby="email-error"
  />
  {form.formState.errors.email && (
    <span id="email-error" className="text-red-500 text-sm">
      {form.formState.errors.email.message}
    </span>
  )}
</div>

8. Styling Standards

8.1 Tailwind CSS Usage

Use utility classes:

// ✅ Good
<button className="px-4 py-2 bg-primary text-white rounded-md hover:bg-primary/90">
  Click me
</button>

// ❌ Bad: Inline styles
<button style={{ padding: '8px 16px', backgroundColor: 'blue' }}>
  Click me
</button>

Use cn() for conditional classes:

import { cn } from '@/lib/utils/cn';

<div className={cn(
  'base-classes',
  isActive && 'active-classes',
  isDisabled && 'disabled-classes',
  className  // Allow override
)} />

8.2 Responsive Design

Mobile-first approach:

<div className="
  w-full p-4           /* Mobile */
  sm:w-1/2 sm:p-6      /* Small screens */
  md:w-1/3 md:p-8      /* Medium screens */
  lg:w-1/4 lg:p-10     /* Large screens */
">
  Content
</div>

8.3 Dark Mode

Use dark mode classes:

<div className="
  bg-white text-black
  dark:bg-gray-900 dark:text-white
">
  Content
</div>

10. Testing Standards

10.1 Test File Organization

src/
├── components/
│   ├── UserTable.tsx
│   └── UserTable.test.tsx
└── lib/
    ├── utils/
    │   ├── formatDate.ts
    │   └── formatDate.test.ts

10.2 Test Naming

Use descriptive test names:

// ✅ Good
test('displays user list when data is loaded', async () => {});
test('shows loading spinner while fetching users', () => {});
test('displays error message when API request fails', () => {});
test('redirects to login when user is not authenticated', () => {});

// ❌ Bad
test('works', () => {});
test('test1', () => {});
test('renders', () => {});

10.3 Component Testing

Test user interactions, not implementation:

// UserTable.test.tsx
import { render, screen, userEvent } from '@testing-library/react';
import { UserTable } from './UserTable';

test('allows user to search for users', async () => {
  const user = userEvent.setup();
  render(<UserTable />);

  const searchInput = screen.getByPlaceholderText('Search users');
  await user.type(searchInput, 'john');

  expect(await screen.findByText('John Doe')).toBeInTheDocument();
  expect(screen.queryByText('Jane Smith')).not.toBeInTheDocument();
});

10.4 Accessibility Testing

Test with accessibility queries:

// Prefer getByRole over getByTestId
const button = screen.getByRole('button', { name: 'Submit' });
const heading = screen.getByRole('heading', { name: 'Users' });
const textbox = screen.getByRole('textbox', { name: 'Email' });

11. Accessibility Standards

11.1 Semantic HTML

// ✅ Good: Semantic
<header>
  <nav>
    <ul>
      <li><a href="/">Home</a></li>
    </ul>
  </nav>
</header>
<main>
  <article>
    <h1>Title</h1>
    <p>Content</p>
  </article>
</main>
<footer>...</footer>

// ❌ Bad: Div soup
<div className="header">
  <div className="nav">
    <div className="link">Home</div>
  </div>
</div>

11.2 ARIA Labels

Use ARIA when semantic HTML isn't enough:

<button aria-label="Close dialog">
  <X className="w-4 h-4" />
</button>

<div role="status" aria-live="polite">
  Loading...
</div>

11.3 Keyboard Navigation

Ensure all interactive elements are keyboard accessible:

<div
  role="button"
  tabIndex={0}
  onClick={handleClick}
  onKeyDown={(e) => {
    if (e.key === 'Enter' || e.key === ' ') {
      handleClick();
    }
  }}
>
  Click me
</div>

12. Performance Best Practices

12.1 Code Splitting

Dynamic imports for heavy components:

import dynamic from 'next/dynamic';

const HeavyChart = dynamic(() => import('./HeavyChart'), {
  loading: () => <LoadingSpinner />,
  ssr: false,
});

12.2 Memoization

Use React.memo for expensive renders:

export const UserCard = React.memo(function UserCard({ user }: UserCardProps) {
  return <div>{user.name}</div>;
});

Use useMemo for expensive calculations:

const sortedUsers = useMemo(() => {
  return users.sort((a, b) => a.name.localeCompare(b.name));
}, [users]);

12.3 Image Optimization

Always use Next.js Image component:

import Image from 'next/image';

<Image
  src="/avatar.jpg"
  alt="User avatar"
  width={40}
  height={40}
  loading="lazy"
/>

13. Security Best Practices

13.1 Input Sanitization

Never render raw HTML:

// ✅ Good: React escapes by default
<div>{userInput}</div>

// ❌ Bad: XSS vulnerability
<div dangerouslySetInnerHTML={{ __html: userInput }} />

13.2 Environment Variables

Never commit secrets:

// ✅ Good: Use env variables
const apiUrl = process.env.NEXT_PUBLIC_API_URL;

// ❌ Bad: Hardcoded
const apiUrl = 'https://api.example.com';

Public vs Private:

  • NEXT_PUBLIC_*: Exposed to browser
  • Other vars: Server-side only

14. Code Review Checklist

Before submitting PR:

  • All tests pass
  • No TypeScript errors
  • ESLint passes
  • Code follows naming conventions
  • Components are typed
  • Accessibility considerations met
  • Error handling implemented
  • Loading states implemented
  • No console.log statements
  • No commented-out code
  • Documentation updated if needed

Conclusion

These standards ensure consistency, maintainability, and quality across the codebase. Follow them rigorously, and update this document as the project evolves.

For specific patterns and examples, refer to:

  • ARCHITECTURE.md: System design and patterns
  • COMPONENT_GUIDE.md: Component usage and examples
  • FEATURE_EXAMPLES.md: Step-by-step implementation guides