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:
- External dependencies (React, Next.js, etc.)
- Internal absolute imports
- Types and interfaces
- Relative imports (if absolutely necessary)
- 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 featurefix: Bug fixdocs: Documentationstyle: Formattingrefactor: Code restructuringtest: Testschore: 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
anytype - 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
Related Documentation¶
- Development Workflow - Daily development process
- Error Handling - Error handling patterns
- Testing - Testing strategies
- Project Structure - Directory organization
- Commit Conventions - Commit message standards
Next Steps¶
- Review the Development Workflow guide
- Understand Error Handling patterns
- Learn about Testing strategies
- Explore API Reference documentation