Conventions
Code Conventions
Solo Kit follows consistent conventions across all packages and applications. These conventions ensure code readability, maintainability, and team collaboration.
π File Naming Conventions
Routes and Pages
Next.js App Router follows kebab-case:
β
Good
app/user-profile/page.tsx
app/api/auth-callback/route.ts
app/admin/user-management/page.tsx
β Avoid
app/userProfile/page.tsx
app/user_profile/page.tsx
app/UserProfile/page.tsx
Components
Use PascalCase for all React components:
β
Good
components/UserProfile.tsx
components/AuthButton.tsx
components/DashboardLayout.tsx
β Avoid
components/userProfile.tsx
components/auth-button.tsx
components/dashboard_layout.tsx
Utilities and Functions
Use camelCase for utilities, hooks, and functions:
β
Good
lib/formatDate.ts
hooks/useAuth.ts
lib/validateEmail.ts
β Avoid
lib/format-date.ts
hooks/use_auth.ts
lib/ValidateEmail.ts
Constants and Configuration
Use SCREAMING_SNAKE_CASE for constants:
β
Good
lib/constants/API_ENDPOINTS.ts
config/DATABASE_CONFIG.ts
lib/constants/ERROR_MESSAGES.ts
β Avoid
lib/constants/apiEndpoints.ts
config/database-config.ts
lib/constants/errorMessages.ts
TypeScript Types and Interfaces
Use PascalCase with descriptive names:
β
Good
interface User {
id: string;
name: string;
email: string;
}
type AuthState = 'authenticated' | 'unauthenticated' | 'loading';
interface CreateUserRequest {
name: string;
email: string;
}
β Avoid
interface user { ... }
type authState = string;
interface createUserReq { ... }
ποΈ Directory Structure Conventions
Feature-Based Organization
Group by features, not by file types:
β
Good - Feature-based
components/features/
βββ auth/
β βββ LoginForm.tsx
β βββ SignupForm.tsx
β βββ AuthButton.tsx
βββ dashboard/
β βββ DashboardStats.tsx
β βββ DashboardLayout.tsx
β βββ StatsCard.tsx
βββ profile/
βββ UserProfile.tsx
βββ ProfileSettings.tsx
βββ AvatarUpload.tsx
β Avoid - File-type based
components/
βββ forms/
β βββ LoginForm.tsx
β βββ SignupForm.tsx
β βββ ProfileForm.tsx
βββ buttons/
β βββ AuthButton.tsx
β βββ SubmitButton.tsx
β βββ DeleteButton.tsx
βββ layouts/
βββ DashboardLayout.tsx
βββ ProfileLayout.tsx
Component Colocation
Keep related files together:
β
Good
components/features/dashboard/
βββ DashboardStats.tsx
βββ DashboardStats.test.tsx
βββ DashboardStats.stories.tsx
βββ dashboard-stats.module.css
β Avoid
components/DashboardStats.tsx
__tests__/DashboardStats.test.tsx
stories/DashboardStats.stories.tsx
styles/dashboard-stats.css
π¦ Import/Export Conventions
Import Order
Organize imports in this specific order:
β
Good
// 1. External libraries
import React from 'react';
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';
// 2. Internal packages (monorepo)
import { Button } from '@packages/ui';
import { formatDate } from '@packages/utils';
import { db } from '@packages/database';
// 3. Local app imports (absolute paths)
import { auth } from '@/lib/auth';
import { validateUser } from '@/lib/validation';
import { UserCard } from '@/components/features/user/UserCard';
// 4. Relative imports (last resort)
import { helper } from './utils';
import './styles.css';
β Avoid
import { helper } from './utils';
import React from 'react';
import { Button } from '@packages/ui';
import { auth } from '@/lib/auth';
Barrel Exports
Use index.ts files for clean imports:
// packages/ui/src/index.ts
export { Button } from './components/button';
export { Card } from './components/card';
export { Input } from './components/input';
export { Modal } from './components/modal';
// Usage
import { Button, Card, Input } from '@packages/ui';
Named vs Default Exports
Prefer named exports for better tree-shaking:
β
Good - Named exports
// components/UserProfile.tsx
export function UserProfile({ user }: UserProfileProps) {
return <div>{user.name}</div>;
}
// Usage
import { UserProfile } from '@/components/UserProfile';
β
Acceptable - Default exports for pages
// app/profile/page.tsx
export default function ProfilePage() {
return <UserProfile user={currentUser} />;
}
π― Component Conventions
Component Structure
Follow this component template:
// 1. Imports (following import order)
import React from 'react';
import { Button } from '@packages/ui';
import { cn } from '@/lib/utils';
// 2. Types and interfaces
interface UserCardProps {
user: {
id: string;
name: string;
email: string;
avatar?: string;
};
className?: string;
onEdit?: () => void;
}
// 3. Component implementation
export function UserCard({ user, className, onEdit }: UserCardProps) {
return (
<div className={cn('rounded-lg border p-4', className)}>
<div className="flex items-center space-x-3">
{user.avatar && (
<img
src={user.avatar}
alt={user.name}
className="h-10 w-10 rounded-full"
/>
)}
<div>
<h3 className="font-semibold">{user.name}</h3>
<p className="text-sm text-muted-foreground">{user.email}</p>
</div>
</div>
{onEdit && (
<Button onClick={onEdit} className="mt-3">
Edit Profile
</Button>
)}
</div>
);
}
// 4. Default props (if needed)
UserCard.defaultProps = {
className: '',
};
Component Props
Use descriptive prop names:
β
Good
interface ButtonProps {
children: React.ReactNode;
variant?: 'primary' | 'secondary' | 'destructive';
size?: 'sm' | 'md' | 'lg';
isLoading?: boolean;
onClick?: () => void;
}
β Avoid
interface ButtonProps {
children: React.ReactNode;
type?: string;
big?: boolean;
loading?: boolean;
click?: () => void;
}
Server vs Client Components
Be explicit about component type:
// Server Component (default - no directive)
import { db } from '@packages/database';
export async function UserList() {
const users = await db.select().from(usersTable);
return (
<div>
{users.map(user => (
<UserCard key={user.id} user={user} />
))}
</div>
);
}
// Client Component (explicit directive)
'use client';
import { useState } from 'react';
import { Button } from '@packages/ui';
export function InteractiveButton() {
const [count, setCount] = useState(0);
return (
<Button onClick={() => setCount(c => c + 1)}>
Clicked {count} times
</Button>
);
}
π§ TypeScript Conventions
Type Definitions
Place types close to usage:
// β
Good - Colocated types
// components/features/auth/LoginForm.tsx
interface LoginFormProps {
onSuccess?: () => void;
redirectTo?: string;
}
interface LoginFormData {
email: string;
password: string;
rememberMe?: boolean;
}
export function LoginForm({ onSuccess, redirectTo }: LoginFormProps) {
// Component implementation
}
// β
Good - Shared types in separate file
// lib/types/auth.types.ts
export interface User {
id: string;
email: string;
name: string;
role: 'user' | 'admin';
}
export interface AuthSession {
user: User;
token: string;
expiresAt: Date;
}
Utility Types
Use TypeScript utility types appropriately:
// User entity
interface User {
id: string;
email: string;
name: string;
createdAt: Date;
updatedAt: Date;
}
// API types using utility types
type CreateUserRequest = Omit<User, 'id' | 'createdAt' | 'updatedAt'>;
type UpdateUserRequest = Partial<Pick<User, 'name' | 'email'>>;
type UserResponse = Pick<User, 'id' | 'email' | 'name'>;
Generics
Use descriptive generic names:
β
Good
interface ApiResponse<TData = unknown> {
data: TData;
success: boolean;
message: string;
}
interface Repository<TEntity, TCreateInput, TUpdateInput> {
create(input: TCreateInput): Promise<TEntity>;
update(id: string, input: TUpdateInput): Promise<TEntity>;
findById(id: string): Promise<TEntity | null>;
}
β Avoid
interface ApiResponse<T = any> {
data: T;
success: boolean;
message: string;
}
π¨ Styling Conventions
Tailwind CSS Classes
Order Tailwind classes logically:
β
Good - Logical grouping
<div className="
flex items-center justify-between
w-full h-12 px-4 py-2
bg-white border border-gray-200 rounded-lg
text-sm font-medium text-gray-900
hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500
transition-colors duration-200
">
β Avoid - Random order
<div className="text-sm bg-white focus:ring-2 px-4 flex hover:bg-gray-50 border items-center w-full rounded-lg">
Class order pattern:
- Layout:
flex
,grid
,block
,inline
- Positioning:
relative
,absolute
,top-0
- Sizing:
w-full
,h-12
,min-h-screen
- Spacing:
p-4
,m-2
,space-x-2
- Background:
bg-white
,bg-gradient-to-r
- Border:
border
,border-gray-200
,rounded-lg
- Typography:
text-sm
,font-medium
,text-gray-900
- Interactive:
hover:
,focus:
,active:
- Transitions:
transition-colors
,duration-200
Component Variants
Use consistent variant patterns:
interface ButtonProps {
variant?: 'primary' | 'secondary' | 'destructive' | 'ghost' | 'outline';
size?: 'sm' | 'md' | 'lg';
}
const buttonVariants = {
variant: {
primary: 'bg-blue-600 text-white hover:bg-blue-700',
secondary: 'bg-gray-100 text-gray-900 hover:bg-gray-200',
destructive: 'bg-red-600 text-white hover:bg-red-700',
ghost: 'hover:bg-gray-100 text-gray-900',
outline: 'border border-gray-300 bg-white hover:bg-gray-50',
},
size: {
sm: 'px-2 py-1 text-xs',
md: 'px-4 py-2 text-sm',
lg: 'px-6 py-3 text-base',
},
};
ποΈ Database Conventions
Schema Naming
Use consistent table and column naming:
// β
Good - Snake case for database
export const users = pgTable('users', {
id: uuid('id').primaryKey().defaultRandom(),
email: text('email').notNull().unique(),
firstName: text('first_name').notNull(),
lastName: text('last_name').notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
});
export const userSessions = pgTable('user_sessions', {
id: uuid('id').primaryKey().defaultRandom(),
userId: uuid('user_id').references(() => users.id).notNull(),
token: text('token').notNull().unique(),
expiresAt: timestamp('expires_at').notNull(),
});
β Avoid - Mixed naming conventions
export const Users = pgTable('Users', {
ID: uuid('ID').primaryKey(),
Email: text('Email').notNull(),
firstName: text('firstName').notNull(),
// Inconsistent naming
});
Query Conventions
Use descriptive function names:
// β
Good - Descriptive names
export async function getUserById(userId: string) {
return db.select().from(users).where(eq(users.id, userId));
}
export async function createUser(userData: CreateUserData) {
return db.insert(users).values(userData).returning();
}
export async function updateUserEmail(userId: string, email: string) {
return db
.update(users)
.set({ email, updatedAt: new Date() })
.where(eq(users.id, userId));
}
β Avoid - Generic names
export async function getUser(id: string) { ... }
export async function createUser(data: any) { ... }
export async function updateUser(id: string, data: any) { ... }
π API Conventions
Server Actions
Use descriptive action names:
// β
Good - Clear action purpose
export const updateUserProfile = actionClientUser
.metadata({ name: 'updateUserProfile' })
.schema(updateUserProfileSchema)
.action(async ({ ctx: { userId }, parsedInput }) => {
// Implementation
});
export const deleteUserAccount = actionClientUser
.metadata({ name: 'deleteUserAccount' })
.schema(deleteUserAccountSchema)
.action(async ({ ctx: { userId } }) => {
// Implementation
});
β Avoid - Generic names
export const userAction = actionClientUser.schema(schema).action(...);
export const doUserStuff = actionClientUser.schema(schema).action(...);
API Routes
Use RESTful conventions:
// β
Good - RESTful naming
// app/api/users/route.ts - GET /api/users (list users)
// app/api/users/[id]/route.ts - GET /api/users/123 (get user)
// app/api/users/[id]/profile/route.ts - PATCH /api/users/123/profile
// Route handlers
export async function GET(request: NextRequest) {
// Get users
}
export async function POST(request: NextRequest) {
// Create user
}
export async function PATCH(
request: NextRequest,
{ params }: { params: { id: string } }
) {
// Update user
}
β Avoid - Non-RESTful naming
// app/api/getUserById/route.ts
// app/api/createUser/route.ts
// app/api/updateUserData/route.ts
π Error Handling Conventions
Error Types
Use structured error handling:
// lib/errors.ts
export class ValidationError extends Error {
constructor(
message: string,
public field: string,
public code: string = 'VALIDATION_ERROR'
) {
super(message);
this.name = 'ValidationError';
}
}
export class DatabaseError extends Error {
constructor(
message: string,
public operation: string,
public code: string = 'DATABASE_ERROR'
) {
super(message);
this.name = 'DatabaseError';
}
}
// Usage
if (!email) {
throw new ValidationError('Email is required', 'email', 'REQUIRED_FIELD');
}
Error Boundaries
Use consistent error boundary patterns:
// app/error.tsx
'use client';
interface ErrorPageProps {
error: Error & { digest?: string };
reset: () => void;
}
export default function Error({ error, reset }: ErrorPageProps) {
return (
<div className="flex flex-col items-center justify-center min-h-[400px]">
<h2 className="text-lg font-semibold text-gray-900 mb-2">
Something went wrong!
</h2>
<p className="text-sm text-gray-600 mb-4">
{error.message || 'An unexpected error occurred'}
</p>
<button
onClick={() => reset()}
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700"
>
Try again
</button>
</div>
);
}
π Documentation Conventions
JSDoc Comments
Document all exported functions:
/**
* Formats a date according to the user's locale preferences
* @param date - The date to format
* @param locale - Optional locale override (defaults to 'en-US')
* @param options - Intl.DateTimeFormat options
* @returns Formatted date string
*
* @example
* ```typescript
* formatDate(new Date(), 'en-US', { dateStyle: 'medium' })
* // Returns: "Jan 15, 2024"
* ```
*/
export function formatDate(
date: Date,
locale: string = 'en-US',
options: Intl.DateTimeFormatOptions = {}
): string {
return new Intl.DateTimeFormat(locale, options).format(date);
}
Component Documentation
Document component props and usage:
/**
* UserCard displays user information in a card format
*
* @example
* ```tsx
* <UserCard
* user={{ id: '1', name: 'John', email: 'john@example.com' }}
* onEdit={() => console.log('Edit clicked')}
* />
* ```
*/
interface UserCardProps {
/** User object containing id, name, email, and optional avatar */
user: {
id: string;
name: string;
email: string;
avatar?: string;
};
/** Additional CSS classes */
className?: string;
/** Callback fired when edit button is clicked */
onEdit?: () => void;
}
export function UserCard({ user, className, onEdit }: UserCardProps) {
// Implementation
}
π― Code Quality Conventions
Code Review Guidelines
Before submitting code:
- Follow naming conventions
- Add TypeScript types
- Include error handling
- Add JSDoc for exported functions
- Test responsive design
- Verify accessibility
- Check performance impact
Linting and Formatting
Use automated tools:
// .eslintrc.js
module.exports = {
extends: ['@packages/eslint-config/next'],
rules: {
// Naming conventions
'camelcase': ['error', { 'properties': 'never' }],
'@typescript-eslint/naming-convention': [
'error',
{
'selector': 'interface',
'format': ['PascalCase'],
'custom': {
'regex': '^I[A-Z]',
'match': false
}
}
]
}
};
π Next Steps
With consistent conventions in place:
- Common Commands - Master the development commands
- Troubleshooting - Solve common issues
- Database Guide - Deep dive into database patterns
- API Development - Build robust APIs
These conventions ensure your Solo Kit codebase remains maintainable, scalable, and collaborative as your team grows.