Code Style Guide
Coding standards and best practices for Izri
๐ Overview
This guide defines the coding standards for Izri to ensure consistent, maintainable, and high-quality code across the project.
๐ง Tooling
Biome (Formatter & Linter)
We use Biome for code formatting and linting:
# Format code
pnpm format:all
# Check formatting
pnpm biome check .
# Fix issues
pnpm biome check --write .
TypeScript
# Type checking
pnpm type:check
# Per package
pnpm type:check:apps
pnpm type:check:packages
๐ TypeScript Style Guide
Type Annotations
// โ
Use explicit return types for functions
function getProject(id: string): Promise<Project | null> {
return db.query.projects.findFirst({ where: eq(projects.id, id) })
}
// โ
Use type inference for obvious cases
const count = projects.length
const isValid = true
// โ Avoid unnecessary annotations
const count: number = projects.length // Redundant
Interfaces vs Types
// โ
Use interfaces for object shapes
interface Project {
id: string
name: string
repository: string
}
// โ
Use types for unions, intersections, utilities
type Status = 'pending' | 'running' | 'completed' | 'failed'
type ProjectWithTests = Project & { testRuns: TestRun[] }
type PartialProject = Partial<Project>
Naming Conventions
// PascalCase for types, interfaces, classes, components
interface ProjectSettings {}
class RepositoryAnalyzer {}
function ProjectCard() {}
// camelCase for variables, functions, methods
const projectId = 'proj_123'
function fetchProjects() {}
// SCREAMING_SNAKE_CASE for constants
const API_BASE_URL = 'https://api.example.com'
const MAX_RETRIES = 3
// Use descriptive names
const user = await getUser(id) // โ
const u = await getU(i) // โ
Async/Await
// โ
Use async/await
async function createProject(data: ProjectData) {
const project = await db.insert(projects).values(data)
return project
}
// โ Avoid promise chains when possible
function createProject(data: ProjectData) {
return db.insert(projects).values(data)
.then(project => project)
}
// โ
Handle errors properly
try {
const project = await createProject(data)
} catch (error) {
logger.error({ error }, 'Failed to create project')
throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR' })
}
Optional Chaining & Nullish Coalescing
// โ
Use optional chaining
const email = user?.profile?.email
// โ
Use nullish coalescing
const name = user.name ?? 'Anonymous'
// โ Avoid long chains
const value = data?.items?.[0]?.properties?.value?.toString() // Too deep
โ๏ธ React Patterns
Component Structure
// โ
Functional components with TypeScript
interface ProjectCardProps {
project: Project
onDelete?: (id: string) => void
}
export function ProjectCard({ project, onDelete }: ProjectCardProps) {
// Hooks at the top
const navigate = useNavigate()
const [isDeleting, setIsDeleting] = useState(false)
// Event handlers
const handleDelete = async () => {
setIsDeleting(true)
try {
await onDelete?.(project.id)
navigate('/dashboard/projects')
} finally {
setIsDeleting(false)
}
}
// Render
return (
<Card>
<CardHeader>
<CardTitle>{project.name}</CardTitle>
</CardHeader>
<CardContent>
<Button onClick={handleDelete} disabled={isDeleting}>
Delete
</Button>
</CardContent>
</Card>
)
}
Hooks
// โ
Custom hooks start with 'use'
function useProject(projectId: string) {
const { data, isLoading } = trpc.projects.getProject.useQuery({ projectId })
return { project: data?.project, isLoading }
}
// โ
Extract complex logic into hooks
function useProjectForm() {
const form = useForm<ProjectForm>({
resolver: zodResolver(projectSchema)
})
const createProject = trpc.projects.createProject.useMutation()
const onSubmit = async (values: ProjectForm) => {
await createProject.mutateAsync(values)
}
return { form, onSubmit, isSubmitting: createProject.isLoading }
}
Component Composition
// โ
Compose small, focused components
function ProjectDetails({ project }: Props) {
return (
<div>
<ProjectHeader project={project} />
<ProjectStats project={project} />
<ProjectTestRuns projectId={project.id} />
</div>
)
}
// โ Avoid monolithic components
function ProjectDetails({ project }: Props) {
return (
<div>
{/* 500 lines of JSX */}
</div>
)
}
๐จ Styling Guidelines
Tailwind CSS
// โ
Use Tailwind utility classes
<div className="flex items-center gap-4 p-4 rounded-lg bg-white shadow-md">
<span className="text-lg font-semibold text-gray-900">Title</span>
</div>
// โ
Use responsive modifiers
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{/* Content */}
</div>
// โ
Use cn() for conditional classes
<div className={cn(
"base-classes",
isActive && "active-classes",
isPending && "pending-classes"
)}>
{/* Content */}
</div>
Component Variants (CVA)
import { cva, type VariantProps } from 'class-variance-authority'
const buttonVariants = cva(
// Base classes
"inline-flex items-center justify-center rounded-md font-medium transition-colors",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline: "border border-input bg-background hover:bg-accent"
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 px-3",
lg: "h-11 px-8"
}
},
defaultVariants: {
variant: "default",
size: "default"
}
}
)
๐ File Organization
Directory Structure
src/
โโโ components/
โ โโโ ui/ # shadcn/ui components
โ โโโ features/ # Feature-specific components
โ โโโ layouts/ # Layout components
โโโ hooks/ # Custom hooks
โโโ lib/ # Utilities and helpers
โโโ routes/ # React Router routes
โโโ types/ # Shared TypeScript types
File Naming
// Components: PascalCase
ProjectCard.tsx
UserProfile.tsx
// Utilities: camelCase
formatDate.ts
apiClient.ts
// Types: PascalCase
Project.ts
User.ts
// Hooks: camelCase (use prefix)
useProject.ts
useAuth.ts
๐ฏ Best Practices
Error Handling
// โ
Use try-catch for async operations
try {
const result = await riskyOperation()
return result
} catch (error) {
logger.error({ error }, 'Operation failed')
if (error instanceof SpecificError) {
// Handle specific error
}
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: 'Operation failed',
cause: error
})
}
Logging
// โ
Use structured logging
logger.info({ userId, projectId }, 'Project created')
// โ
Include context in error logs
logger.error({ error, userId, data }, 'Failed to create project')
// โ Avoid string concatenation
logger.info('User ' + userId + ' created project ' + projectId)
Performance
// โ
Use React.memo for expensive components
export const ExpensiveComponent = React.memo(({ data }: Props) => {
// Complex rendering logic
})
// โ
Use useMemo for expensive calculations
const sortedProjects = useMemo(
() => projects.sort((a, b) => a.name.localeCompare(b.name)),
[projects]
)
// โ
Use useCallback for event handlers passed to children
const handleDelete = useCallback(
(id: string) => {
deleteProject.mutate({ id })
},
[deleteProject]
)
๐งช Testing
Test File Naming
// Next to source file
Button.tsx
Button.test.tsx
// Or in __tests__ directory
Button.tsx
__tests__/Button.test.tsx
Test Structure
describe('ProjectCard', () => {
it('renders project name', () => {
const project = { name: 'Test Project', ...otherFields }
render(<ProjectCard project={project} />)
expect(screen.getByText('Test Project')).toBeInTheDocument()
})
it('calls onDelete when delete button clicked', async () => {
const onDelete = vi.fn()
const project = { id: 'proj_123', ...otherFields }
render(<ProjectCard project={project} onDelete={onDelete} />)
await userEvent.click(screen.getByRole('button', { name: /delete/i }))
expect(onDelete).toHaveBeenCalledWith('proj_123')
})
})
๐ Related Documentation
- Development Setup: Local setup guide
- Testing Guidelines: Testing standards
- Commit Conventions: Commit message format
Next: Commit Conventions โ