Skip to Content

Code Style Guide

This guide documents the coding conventions and style rules for the TEA Platform codebase.

Tooling

Linting and Formatting

The project uses Biome  via Ultracite for linting and formatting:

# Check for issues pnpm exec ultracite check # Fix issues automatically pnpm exec ultracite fix

Always run before committing

CI will fail if linting errors are present. Run ultracite check before pushing.

Type Checking

TypeScript strict mode is enabled. Run type checking with:

npx tsc --noEmit

TypeScript Rules

No any Type

Never use any. Use proper types or unknown with type guards:

// Bad function process(data: any) { ... } // Good function process(data: unknown) { if (isValidData(data)) { ... } } // Good - use proper types function process(data: ProcessableData) { ... }

Use Defined Types

Import types from types/ or generate them from Prisma:

// Import from types directory import type { AssuranceCaseWithElements } from '@/types/cases' // Use Prisma generated types import type { AssuranceCase, AssuranceElement } from '@/src/generated/prisma'

Prefer Type Inference

Let TypeScript infer types where obvious:

// Unnecessary - type is inferred const count: number = items.length // Better const count = items.length // Do specify for function return types function getCount(items: Item[]): number { return items.length }

Naming Conventions

Files and Folders

TypeConventionExample
Componentskebab-caseflow-editor.tsx
Utilitieskebab-caseformat-date.ts
Typeskebab-casecase-types.ts
Server Actionskebab-casecase-actions.ts
Folderskebab-caseflow-editor/

Code Identifiers

TypeConventionExample
VariablescamelCasecaseData
FunctionscamelCasegetCaseById
ComponentsPascalCaseFlowEditor
Types/InterfacesPascalCaseAssuranceCase
ConstantsSCREAMING_SNAKE_CASEMAX_RETRY_COUNT
EnumsPascalCase (values too)ElementType.Goal

British English

Use British English spelling throughout the codebase:

UseDon’t Use
colourcolor
behaviourbehavior
optimiseoptimize
analyseanalyze
centrecenter
licence (noun)license (noun)

Exception: External API names and library identifiers should use their original spelling (e.g., backgroundColor in CSS-in-JS).

React Patterns

Server vs Client Components

Default to Server Components. Use Client Components only when necessary:

// Server Component (default) - no directive needed async function CasePage({ params }) { const data = await getCaseData(params.id) return <CaseView data={data} /> } // Client Component - needs 'use client' 'use client' function InteractiveEditor({ initialData }) { const [state, setState] = useState(initialData) // ... }

Component Structure

Organise components consistently:

'use client' // if needed import { useState } from 'react' import { cn } from '@/lib/utils' import type { ComponentProps } from './types' interface Props { // ... } export function ComponentName({ prop1, prop2 }: Props) { // Hooks first const [state, setState] = useState() // Event handlers const handleClick = () => { ... } // Render helpers const renderItem = (item: Item) => { ... } // Return JSX return ( <div> {/* ... */} </div> ) }

Props Interface

Define props interface above the component:

interface FlowEditorProps { initialNodes: Node[] initialEdges: Edge[] readOnly?: boolean onSave?: (data: CaseData) => void } export function FlowEditor({ initialNodes, initialEdges, readOnly = false, onSave, }: FlowEditorProps) { // ... }

Server Actions

Action Structure

Server Actions follow a consistent pattern:

'use server' import { z } from 'zod' import { prisma } from '@/lib/db' import { requireAuth } from '@/lib/auth' const createElementSchema = z.object({ caseId: z.string().uuid(), elementType: z.nativeEnum(ElementType), description: z.string().min(1), }) export async function createElement( input: z.infer<typeof createElementSchema> ) { const user = await requireAuth() const validated = createElementSchema.parse(input) // Check permissions const hasAccess = await checkCaseAccess(validated.caseId, user.id, 'EDIT') if (!hasAccess) { throw new Error('Permission denied') } // Perform action const element = await prisma.assuranceElement.create({ data: { ...validated, createdById: user.id, } }) return { success: true, data: element } }

Return Types

Actions should return consistent result objects:

// Success return { success: true, data: result } // Error return { success: false, error: 'Error message' }

Imports

Import Order

Organise imports in this order:

  1. React/Next.js
  2. External libraries
  3. Internal aliases (@/)
  4. Relative imports
  5. Types (with type keyword)
import { useState, useEffect } from 'react' import { useRouter } from 'next/navigation' import { toast } from 'sonner' import { z } from 'zod' import { prisma } from '@/lib/db' import { cn } from '@/lib/utils' import { LocalComponent } from './local-component' import type { CaseData } from '@/types/cases'

Path Aliases

Use the @/ alias for imports from project root:

// Good import { Button } from '@/components/ui/button' // Avoid import { Button } from '../../../components/ui/button'

Comments

When to Comment

  • Complex business logic
  • Non-obvious workarounds
  • TODO items with context

Comment Style

// Single line for brief notes /** * Multi-line for function documentation. * Explains what the function does and why. */ function complexFunction() { ... } // TODO(username): Description of what needs to be done // and why it's deferred

Avoid Obvious Comments

// Bad - states the obvious // Increment counter counter++ // Good - explains why // Skip the header row when processing CSV startIndex = 1

Testing

Test File Location

Place test files next to source files:

components/ flow-editor/ flow-editor.tsx flow-editor.test.tsx

Test Naming

describe('FlowEditor', () => { it('should render initial nodes', () => { ... }) it('should add node when toolbar button clicked', () => { ... }) it('should delete selected node on Delete key', () => { ... }) })

Further Reading


Code Style Guide | TEA Documentation