Skip to content

Coding Conventions

Last Updated: 2025-01-22

This guide outlines the coding standards and conventions used throughout the AccessALI Customer Portal codebase.


Overview

Consistent coding conventions ensure code quality, maintainability, and team collaboration. This document covers naming conventions, file organization, TypeScript patterns, and best practices specific to Next.js 15 and our architecture.


File Naming

General Rules

  • Use kebab-case for all files and directories
  • Use descriptive, meaningful names
  • Keep names concise but clear
  • Match file names to their primary export when applicable

Specific Patterns

File Type Convention Example
Pages page.tsx app/dashboard/page.tsx
Layouts layout.tsx app/(dashboard)/layout.tsx
Server Actions feature-name.ts actions/dashboard.ts
API Routes route.ts api/health/route.ts
Repositories entity-repository.ts repositories/user-repository.ts
Use Cases feature-name.ts use-cases/dashboard.ts
Components component-name.tsx components/dashboard-header.tsx
UI Components component-name.tsx components/ui/button.tsx
Types types.ts or feature-types.ts lib/types.ts
Utils util-name.ts lib/utils.ts
Constants feature-constants.ts lib/constants/dashboard.ts
Tests *.test.ts or *.test.tsx __tests__/dashboard.test.ts

Examples

Good:
- dashboard-header.tsx
- user-repository.ts
- property-details.ts
- api-error-handler.ts

Bad:
- DashboardHeader.tsx (should use kebab-case)
- userRepository.ts (should use kebab-case)
- PropertyDetails.ts (should use kebab-case)
- apiErrorHandler.ts (should use kebab-case)

Import Patterns

Absolute Imports

Always use absolute imports with the @/ alias:

// Good - Absolute imports
import { prisma } from '@/lib/prisma'
import { Button } from '@/components/ui/button'
import { getDashboardData } from '@/app/actions/dashboard'
import { getUserProfile } from '@/lib/repositories/user-repository'

// Bad - Relative imports
import { prisma } from '../../lib/prisma'
import { Button } from '../../../components/ui/button'

Import Organization

Organize imports in the following order:

  1. External dependencies (React, Next.js, etc.)
  2. Internal absolute imports
  3. Types and interfaces
  4. Relative imports (if absolutely necessary)
  5. CSS/styles
// 1. External dependencies
import { Suspense } from 'react'
import { redirect } from 'next/navigation'
import { z } from 'zod'

// 2. Internal absolute imports
import { auth } from '@/lib/auth'
import { getDashboardData } from '@/app/actions/dashboard'
import { Button } from '@/components/ui/button'
import { DashboardHeader } from '@/components/dashboard-header'

// 3. Types
import type { DashboardData } from '@/lib/types'
import type { User } from '@prisma/client'

// 4. Styles (if needed)
import './styles.css'

TypeScript Conventions

Type Definitions

// Use type for object shapes and unions
type DashboardData = {
  user: UserProfile
  properties: PropertyListItem[]
}

// Use interface for objects that might be extended
interface UserProfile {
  id: string
  firstName: string
  lastName: string
}

// Use const assertions for constant objects
const MILESTONE_ORDER = {
  RESERVATION: 1,
  REQUIRED_DOCS: 2,
  PAYMENTS: 3,
} as const

Type Naming

// Good - Clear, descriptive names
type PropertyWithMilestones = Property & {
  milestones: Milestone[]
}

type UserProfileWithCounts = User & {
  _count: {
    properties: number
  }
}

// Avoid - Unclear abbreviations
type PropWM = Property & { m: Milestone[] }  // Bad
type UPC = User & { c: { p: number } }       // Bad

Avoid any

// Bad - Using any
function processData(data: any) {
  return data.value
}

// Good - Proper typing
function processData(data: { value: string }) {
  return data.value
}

// Good - Unknown with type guard
function processData(data: unknown) {
  if (typeof data === 'object' && data !== null && 'value' in data) {
    return (data as { value: string }).value
  }
  throw new Error('Invalid data')
}

Use Type Inference

// Good - Let TypeScript infer
const properties = await getUserProperties(userId)
// Type is inferred as PropertyListItem[]

// Avoid - Redundant type annotation
const properties: PropertyListItem[] = await getUserProperties(userId)

Component Patterns

Server Components (Default)

// Server Component - No "use client" directive
export default async function DashboardPage() {
  // Can fetch data directly
  const data = await getDashboardData()

  return (
    <div>
      <h1>Dashboard</h1>
      <DashboardContent data={data} />
    </div>
  )
}

Client Components

// Client Component - Explicit "use client"
'use client'

import { useState } from 'react'
import { Button } from '@/components/ui/button'

export function InteractiveComponent() {
  const [count, setCount] = useState(0)

  return (
    <Button onClick={() => setCount(count + 1)}>
      Count: {count}
    </Button>
  )
}

Component Props

// Define props interface
interface DashboardHeaderProps {
  userName: string
  propertyCount: number
  onRefresh?: () => void
}

// Use props interface
export function DashboardHeader({
  userName,
  propertyCount,
  onRefresh,
}: DashboardHeaderProps) {
  return (
    <header>
      <h1>Welcome, {userName}</h1>
      <p>{propertyCount} properties</p>
    </header>
  )
}

Server Actions

File Structure

// app/actions/feature-name.ts
'use server'

import { auth } from '@/lib/auth'
import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'

/**
 * Action description with JSDoc
 *
 * @param formData - Form data or parameters
 * @returns Result or redirects
 */
export async function actionName(formData: FormData) {
  // 1. Authenticate
  const session = await auth()
  if (!session?.user?.id) {
    throw new Error('Unauthorized')
  }

  // 2. Validate input
  const validated = schema.parse({
    field: formData.get('field'),
  })

  // 3. Execute business logic
  const result = await useCaseFunction(session.user.id, validated)

  // 4. Revalidate cache
  revalidatePath('/path')

  // 5. Return result or redirect
  return result
}

Error Handling

export async function createPayment(formData: FormData) {
  try {
    const session = await auth()
    if (!session?.user?.id) {
      throw new Error('Unauthorized')
    }

    const result = await createPaymentUseCase(session.user.id, data)
    revalidatePath('/payments')

    return { success: true, data: result }
  } catch (error) {
    const err = error instanceof Error ? error : new Error(String(error))
    logger.error('Error in createPayment', err)

    return {
      success: false,
      error: err.message,
    }
  }
}

Repository Layer

Function Naming

// CRUD operations
getUserById(id: string)           // GET single
getUserByEmail(email: string)     // GET by field
getUserProperties(userId: string) // GET collection
createUser(data: CreateData)      // CREATE
updateUser(id: string, data)      // UPDATE
deleteUser(id: string)            // DELETE

// Special operations
validatePropertyOwnership(userId, propertyId)
calculateMilestoneProgress(propertyId)
checkMilestoneDependencies(milestoneId)

Error Handling Pattern

export async function getUserById(id: string) {
  try {
    const user = await prisma.user.findUnique({
      where: { id },
    })

    if (!user) {
      throw new NotFoundError('User', id)
    }

    return user
  } catch (error) {
    // Re-throw custom errors unchanged
    if (error instanceof NotFoundError) {
      throw error
    }

    // Wrap database errors
    logger.error('Database error in getUserById', error)
    throw new DatabaseError('get user by id', error)
  }
}

Naming Conventions

Variables

// Use camelCase
const userName = 'John Doe'
const propertyCount = 5
const isActive = true

// Boolean variables - use is/has/can prefix
const isLoading = false
const hasPermission = true
const canEdit = false

// Arrays - use plural
const properties = []
const milestones = []
const documents = []

// Constants - use UPPER_SNAKE_CASE
const MAX_UPLOAD_SIZE = 10 * 1024 * 1024
const DEFAULT_PAGE_SIZE = 10
const API_BASE_URL = 'https://api.example.com'

Functions

// Use camelCase for functions
function getUserProfile() {}
function calculateProgress() {}
function validateInput() {}

// Use descriptive verb prefixes
function getUser()       // Retrieve data
function setUser()       // Set/assign value
function createUser()    // Create new entity
function updateUser()    // Modify existing
function deleteUser()    // Remove entity
function validateUser()  // Check validity
function calculateTotal()// Compute value
function isValid()       // Boolean check
function hasPermission() // Boolean check

Classes

// Use PascalCase
class UserRepository {}
class NotFoundError extends Error {}
class PaymentService {}

// Use descriptive names
class DatabaseConnection {}  // Good
class DBConn {}              // Bad - unclear abbreviation

Comments and Documentation

JSDoc for Public Functions

/**
 * Get user profile by ID
 *
 * Retrieves user profile data including property count.
 * Excludes sensitive fields like password.
 *
 * @param userId - The user's unique identifier
 * @returns User profile with counts, or null if not found
 * @throws {DatabaseError} If database operation fails
 *
 * @example
 * ```typescript
 * const profile = await getUserProfile('user-123')
 * if (profile) {
 *   console.log(`${profile.firstName} ${profile.lastName}`)
 *   console.log(`Properties: ${profile._count.properties}`)
 * }
 * ```
 */
export async function getUserProfile(
  userId: string
): Promise<UserProfileWithCounts | null> {
  // Implementation
}

Inline Comments

// Use comments to explain "why", not "what"

// Good - Explains reasoning
// Use deterministic hash to ensure same property gets same image
const hash = generateHash(propertyId)

// Bad - States the obvious
// Set count to 0
const count = 0

TODO Comments

// TODO: Add pagination support
// TODO: Implement caching layer
// FIXME: Handle edge case when user has no properties
// NOTE: This query is expensive, consider optimization

Environment Variables

Naming

// Use UPPER_SNAKE_CASE
DATABASE_URL
NEXTAUTH_SECRET
USE_MOCK_SAP

// Group by feature
GOOGLE_CLIENT_ID
GOOGLE_CLIENT_SECRET

REDIS_URL
REDIS_PASSWORD

Usage

// Good - With fallback
const apiUrl = process.env.API_URL || 'http://localhost:3000'
const useMock = process.env.USE_MOCK_SAP === 'true'

// Bad - No fallback (could be undefined)
const apiUrl = process.env.API_URL

Error Messages

User-Facing Errors

// Good - Clear, actionable
throw new Error('Email is required')
throw new Error('Password must be at least 8 characters')
throw new Error('Property not found')

// Bad - Vague or technical
throw new Error('Validation failed')
throw new Error('Database error')
throw new Error('Error 500')

Developer Errors

// Include context
throw new DatabaseError('create user', originalError)
throw new NotFoundError('Property', propertyId)
throw new UnauthorizedError('Property', propertyId, userId)

Testing Conventions

Test File Names

// Same name as source file with .test suffix
user-repository.ts        user-repository.test.ts
dashboard.ts             dashboard.test.ts
button.tsx               button.test.tsx

Test Structure

import { getUserProfile } from '@/lib/repositories/user-repository'
import { prisma } from '@/lib/prisma'

// Mock dependencies
jest.mock('@/lib/prisma')

describe('getUserProfile', () => {
  // Clear mocks before each test
  beforeEach(() => {
    jest.clearAllMocks()
  })

  it('should return user profile with property count', async () => {
    // Arrange
    const mockUser = {
      id: 'user-123',
      firstName: 'John',
      lastName: 'Doe',
      _count: { properties: 2 },
    }
    ;(prisma.user.findUnique as jest.Mock).mockResolvedValue(mockUser)

    // Act
    const result = await getUserProfile('user-123')

    // Assert
    expect(result).toEqual(mockUser)
    expect(prisma.user.findUnique).toHaveBeenCalledWith({
      where: { id: 'user-123' },
      select: expect.any(Object),
    })
  })

  it('should return null when user not found', async () => {
    ;(prisma.user.findUnique as jest.Mock).mockResolvedValue(null)

    const result = await getUserProfile('non-existent')

    expect(result).toBeNull()
  })
})

Code Formatting

Prettier Configuration

The project uses Prettier for automatic code formatting:

{
  "semi": false,
  "singleQuote": true,
  "tabWidth": 2,
  "trailingComma": "es5",
  "printWidth": 100,
  "arrowParens": "always"
}

ESLint Rules

Key ESLint rules enforced:

  • No unused variables
  • No console.log (use logger)
  • Explicit return types for exported functions
  • Consistent import order

Line Length

  • Maximum 100 characters per line
  • Break long lines for readability
// Good - Readable
const user = await prisma.user.findUnique({
  where: { id: userId },
  select: { id: true, firstName: true, lastName: true },
})

// Bad - Too long
const user = await prisma.user.findUnique({ where: { id: userId }, select: { id: true, firstName: true, lastName: true } })

Git Commit Messages

Format

type(scope): brief description

Longer description if needed.

- Bullet point details
- Another detail

Refs: #123

Types

  • feat: New feature
  • fix: Bug fix
  • docs: Documentation
  • style: Formatting
  • refactor: Code restructuring
  • test: Tests
  • chore: Maintenance

Examples

feat(dashboard): add property search functionality

Implemented real-time search with debouncing for property lookup.
Returns top 3 results matching brand, project name, or unit number.

- Added searchProperties server action
- Created search input component
- Added 300ms debounce for performance

Refs: #45

Best Practices Checklist

  • Use absolute imports (@/ alias)
  • Follow kebab-case for file names
  • Add JSDoc for public functions
  • Use TypeScript strict mode
  • Avoid any type
  • Use Server Components by default
  • Validate all inputs with Zod
  • Handle errors properly
  • Use repository pattern for data access
  • Add tests for new features
  • Use logger instead of console.log
  • Revalidate cache after mutations
  • Check authentication in server actions
  • Use environment variables for configuration
  • Follow component composition patterns


Next Steps

  1. Review the Development Workflow guide
  2. Understand Error Handling patterns
  3. Learn about Testing strategies
  4. Explore API Reference documentation