Coding Conventions
Master the naming conventions, import order, TypeScript patterns, and code style rules that keep the OneApp monorepo consistent across 40+ packages and apps.
Already familiar with conventions?
Why conventions matter
Working without consistent conventions creates serious problems:
- Inconsistent naming —
isLoadingin one file,loadingin another,loadingStatein a third - Import chaos — Every file has different import order, mixing types with values
- Code review friction — 50% of review comments are about style instead of logic
- TypeScript errors — Using
any, missing return types, no type guards - Merge conflicts — Different formatting causes unnecessary git conflicts
- Onboarding delays — New developers don't know how to name files, variables, components
OneApp's conventions use enforced patterns via ESLint 9 (flat config), Prettier 3 (auto-format on save), TypeScript
strict mode (no any, explicit return types), consistent import order (external → internal → relative → types →
styles), and standardized naming (PascalCase components, camelCase functions, kebab-case files) — ensuring code looks
the same no matter who wrote it.
Production-ready with automated enforcement via git hooks (Husky 9), ESLint auto-fix on commit, Prettier formatting
on save, 40+ apps following same conventions, zero tolerance for any types, and comprehensive JSDoc requirements.
Use cases
Master conventions to:
- Write consistent code — Follow patterns used across entire monorepo
- Pass code reviews — Avoid style nitpicks, focus on logic
- Onboard quickly — Know exactly how to name files, imports, variables
- Prevent conflicts — Consistent formatting reduces merge conflicts
- Improve readability — Code looks uniform across all packages
- Catch errors early — TypeScript strict mode catches bugs at compile time
Quick Start
Essential conventions summary
Naming:
- Components: PascalCase (
Button.tsx,UserProfile.tsx) - Functions: camelCase (
formatDate,getUserById) - Files: kebab-case for packages (
my-package), match export for components - Booleans:
is/has/should/canprefix (isLoading,hasPermission) - Constants: UPPER_SNAKE_CASE (
MAX_RETRIES,API_BASE_URL)
Import order (enforced by ESLint):
// 1. External packages
import { useState } from "react";
// 2. Internal packages (@repo/*)
import { Button } from "@repo/ui";
// 3. Relative imports
import { Header } from "./Header";
// 4. Type imports
import type { User } from "@repo/types";
// 5. Styles
import styles from "./Page.module.css";TypeScript rules:
- ❌ Never use
any— Useunknownwith type guards - ✅ Always add explicit return types for exports
- ✅ Prefer
satisfiesover type assertions - ✅ Use branded types for IDs (
UserId,ProductId)
That's it! ESLint and Prettier enforce most conventions automatically.
Naming conventions
Files and directories
| Type | Convention | Example | When to Use |
|---|---|---|---|
| Components | PascalCase | Button.tsx, UserProfile.tsx | React components |
| Utilities | camelCase | formatDate.ts, apiClient.ts | Helper functions |
| Tests | *.test.tsx | Button.test.tsx | Vitest tests |
| Types | PascalCase | User.ts, ApiResponse.ts | Type definitions |
| Packages | kebab-case | packages/my-package | Internal packages |
| Directories | kebab-case | user-profile/, api-client/ | Folders |
Variables and functions
// ✅ Descriptive variable names
const isLoading = true;
const hasPermission = checkPermission();
const userCount = 42;
const currentUser = await getUser();
// ✅ Function naming
function formatDate(date: Date): string {
/* ... */
}
function getUserById(id: UserId): Promise<User | null> {
/* ... */
}
async function fetchUsers(): Promise<User[]> {
/* ... */
}
// ✅ Event handlers
function handleClick() {
/* ... */
}
function handleSubmit(e: FormEvent) {
/* ... */
}// ❌ Non-descriptive names
const loading = true; // Missing is/has prefix for boolean
const data = 42; // Too generic
const u = await getUser(); // Abbreviation
// ❌ Poor function naming
function format(d) {
/* ... */
} // Missing type, too generic
function get(id) {
/* ... */
} // Unclear what is being retrievedBoolean variables
Always use descriptive prefixes:
// Prefixes: is, has, should, can, will
const isActive = true;
const isLoading = false;
const isDisabled = !canEdit;
const hasChildren = items.length > 0;
const hasPermission = user.role === "admin";
const hasError = result.error !== null;
const shouldValidate = true;
const shouldShowModal = isOpen && !isLoading;
const canEdit = user.role === "admin" || user.role === "editor";
const canDelete = user.role === "admin";
const willRedirect = !!returnUrl;
const willExpire = expiresAt < Date.now();Why it matters: Boolean prefixes make code self-documenting and prevent confusion.
Constants
// Use UPPER_SNAKE_CASE for true constants
const MAX_RETRIES = 3;
const API_BASE_URL = "https://api.example.com";
const DEFAULT_PAGE_SIZE = 20;
const TIMEOUT_MS = 5000;
// ❌ Don't use for regular variables
// const USER_NAME = userName; // Bad - not a constantReact component props
// Use ComponentNameProps pattern
export interface ButtonProps {
children: ReactNode;
variant?: "primary" | "secondary";
onClick?: () => void;
}
export interface CardProps {
title: string;
description?: string;
className?: string;
}Import order
Strictly enforce this import order via ESLint:
// 1. External packages (React, Next.js, third-party libraries)
import { useState, useEffect } from "react";
import Link from "next/link";
import { z } from "zod/v4";
// 2. Internal packages (@repo/*)
import { Button, Card } from "@repo/ui";
import { formatDate, cn } from "@repo/utils";
import { auth } from "@repo/auth/server";
// 3. Relative imports (same package)
import { Header } from "./Header";
import { Footer } from "./Footer";
import { useAuth } from "../hooks/useAuth";
import { formatCurrency } from "../utils/formatCurrency";
// 4. Type imports (always separate, never mixed)
import type { User, UserId } from "@repo/types";
import type { HeaderProps } from "./Header";
import type { ReactNode } from "react";
// 5. Styles (always last)
import styles from "./Page.module.css";
import "./globals.css";Never mix type imports
Separate type imports from value imports:
// ❌ Bad - mixed imports
import { useState, type ReactNode } from "react";
// ✅ Good - separated
import { useState } from "react";
import type { ReactNode } from "react";This ensures proper tree-shaking and type checking.
TypeScript standards
No any types
Never use any — use unknown with type guards:
// ❌ Loses all type safety
function process(data: any) {
return data.value; // No autocomplete, no type checking
}// ✅ Type-safe with guard
function process(data: unknown) {
if (isValidData(data)) {
return data.value; // Autocomplete works, type-safe
}
throw new Error("Invalid data");
}
function isValidData(data: unknown): data is { value: string } {
return typeof data === "object" && data !== null && "value" in data && typeof data.value === "string";
}Explicit return types
Always add explicit return types for exported functions:
// ❌ Return type inferred (unclear, fragile)
export function getUser(id: string) {
return db.users.findUnique({ where: { id } });
}// ✅ Explicit return type (clear, stable)
export function getUser(id: UserId): Promise<User | null> {
return db.users.findUnique({ where: { id } });
}
// ✅ For complex types
export function processData(input: Input): AsyncResult<Output> {
// Implementation
}Why it matters: Explicit return types create a stable API. Changing implementation won't accidentally change the public API.
Prefer satisfies over as
Use satisfies to validate types while preserving literal types:
// ✅ Validates type while preserving literal types
const config = {
port: 3000,
host: "localhost",
env: "development"
} satisfies ServerConfig;
// config.port is typed as 3000 (literal), not number
// config.env is typed as "development" (literal), not string// ❌ Loses literal types
const config: ServerConfig = {
port: 3000,
host: "localhost",
env: "development"
};
// config.port is typed as number (less precise)
// config.env is typed as string (less precise)Branded types for IDs
Use branded types to prevent ID mix-ups:
import type { Brand } from "@repo/types";
export type UserId = Brand<string, "UserId">;
export type ProductId = Brand<string, "ProductId">;
export type OrderId = Brand<string, "OrderId">;
export function createUserId(id: string): UserId {
return id as UserId;
}
export function createProductId(id: string): ProductId {
return id as ProductId;
}import type { UserId, ProductId } from "@repo/types";
import { createUserId } from "@repo/types";
function getUser(id: UserId): Promise<User> {
/* ... */
}
function getProduct(id: ProductId): Promise<Product> {
/* ... */
}
const userId = createUserId("user_123");
const productId = createProductId("prod_456");
getUser(userId); // ✅ Works
getUser(productId); // ❌ Compiler error - prevents bugs!Component patterns
Function components
Use function components with explicit types:
import * as React from "react";
import { cn } from "@repo/utils";
import type { ReactNode } from "react";
/**
* Props for the Card component.
*/
export interface CardProps {
/** Card title */
title: string;
/** Optional description */
description?: string;
/** Card content */
children: ReactNode;
/** Additional CSS classes */
className?: string;
}
/**
* A card component for displaying grouped content.
*
* @example
* ```tsx
* <Card title="Welcome">
* <p>Card content here</p>
* </Card>
* ```
*/
export function Card({ title, description, children, className }: CardProps): React.ReactElement {
return (
<div className={cn("rounded-lg border p-4", className)}>
<h3 className="font-semibold">{title}</h3>
{description && <p className="text-gray-600">{description}</p>}
<div className="mt-4">{children}
);
}Key patterns:
- ✅ Export
ComponentNamePropsinterface - ✅ Add JSDoc with description and example
- ✅ Explicit
React.ReactElementreturn type - ✅ Destructure props in parameter list
Server vs Client Components
// No directive needed - server by default in Next.js App Router
export function ServerComponent() {
return <div>Rendered on server;
}"use client";
import { useState } from "react";
export function ClientComponent() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount((c) => c + 1)}>{count}</button>;
}Use Client Components only when you need:
- React state (
useState,useReducer) - Effects (
useEffect,useLayoutEffect) - Event handlers (
onClick,onChange,onSubmit) - Browser APIs (
window,document,localStorage) - React hooks (
useSearchParams,usePathname)
Default to Server Components for better performance.
Documentation
JSDoc comments
All exported functions, components, and types require JSDoc:
/**
* Formats a date for display in the user's locale.
*
* @param date - The date to format
* @param locale - The locale to use (default: "en-US")
* @returns Formatted date string
* @throws {Error} If date is invalid
*
* @example
* ```ts
* formatDate(new Date()); // "January 1, 2024"
* formatDate(new Date(), "de-DE"); // "1. Januar 2024"
* ```
*/
export function formatDate(date: Date, locale = "en-US"): string {
if (!(date instanceof Date) || isNaN(date.getTime())) {
throw new Error("Invalid date");
}
return date.toLocaleDateString(locale, {
year: "numeric",
month: "long",
day: "numeric"
});
}Required sections:
- Description (what the function does)
@paramfor each parameter@returnsfor return value@throwsfor errors (if applicable)@examplefor complex functions
Inline comments
Use sparingly, only for non-obvious code:
// ✅ Good - explains "why"
// Retry up to 3 times due to occasional network timeouts
for (let i = 0; i < 3; i++) {
await retry();
}
// Wait 100ms to debounce rapid clicks
await new Promise((resolve) => setTimeout(resolve, 100));
// ❌ Bad - states the obvious
// Loop through users
for (const user of users) {
/* ... */
}
// Create a variable
const name = "John";Code style
Formatting
Prettier handles formatting automatically:
- Indent: 2 spaces
- Quotes: Double quotes for strings
- Semicolons: Always
- Trailing commas: ES5 (objects, arrays)
- Line length: 100 characters
- Arrow functions: Parentheses when needed
Don't configure Prettier yourself — use @repo/config/prettier:
import config from "@repo/config/prettier";
export default config;ESLint rules
ESLint enforces code quality:
import reactConfig from "@repo/config/eslint/react";
export default [...reactConfig];Key rules enforced:
- No
anytypes - No unused variables
- Explicit return types for exports
- Proper import order
- No
console.login production - Exhaustive
switchstatements - No magic numbers
Next steps
- Learn git workflow: Git Workflow →
- Handle errors properly: Error Handling →
- Understand type safety: Type Safety →
For Developers: Advanced conventions and customization
Advanced TypeScript patterns
Discriminated unions
type Result<T> = { success: true; data: T } | { success: false; error: Error };
function handleResult<T>(result: Result<T>) {
if (result.success) {
console.log(result.data); // TypeScript knows data exists
} else {
console.error(result.error); // TypeScript knows error exists
}
}Utility types
// Make all properties optional
type PartialUser = Partial<User>;
// Make all properties required
type RequiredUser = Required<PartialUser>;
// Pick specific properties
type UserPreview = Pick<User, "id" | "name" | "email">;
// Omit specific properties
type UserWithoutPassword = Omit<User, "password">;
// Extract union members
type Status = "pending" | "approved" | "rejected";
type ApprovedStatus = Extract<Status, "approved">; // "approved"Const assertions
// Create readonly tuples
const colors = ["red", "green", "blue"] as const;
// Type: readonly ["red", "green", "blue"]
// Create readonly objects
const config = {
apiUrl: "https://api.example.com",
timeout: 5000
} as const;
// All properties are readonlyFile organization
Component file structure
Button/
├── Button.tsx # Component implementation
├── Button.test.tsx # Tests
├── Button.stories.tsx # Storybook stories
├── index.ts # Re-exports
└── styles.module.css # Component-specific stylesUtility file structure
utils/
├── formatDate.ts # Single responsibility per file
├── formatDate.test.ts # Co-located tests
├── formatCurrency.ts
├── formatCurrency.test.ts
└── index.ts # Barrel exportPackage structure
packages/my-package/
├── src/
│ ├── index.ts # Public exports
│ ├── client.ts # Client-only exports
│ ├── server.ts # Server-only exports
│ └── types.ts # Type-only exports
├── package.json
├── tsconfig.json
├── eslint.config.mjs
└── README.mdCustom ESLint rules
Adding rules
import reactConfig from "@repo/config/eslint/react";
export default [
...reactConfig,
{
rules: {
// Disable specific rules
"no-console": "off",
// Custom rules for this package
"import/no-default-export": "error"
}
}
];Ignoring files
export default [
...reactConfig,
{
ignores: ["dist/", ".next/", "coverage/", "**/*.config.js"]
}
];Custom Prettier config
Override Prettier rules (avoid unless necessary):
import config from "@repo/config/prettier";
export default {
...config,
// Override only if absolutely necessary
printWidth: 120, // Increase line length
semi: false // Remove semicolons
};Naming edge cases
Acronyms
// ✅ Treat as words
class ApiClient {}
class HttpRequest {}
const userId = "123";
// ❌ Don't use all caps unless constant
class API_CLIENT {} // Bad
const USER_ID = "123"; // Bad (not a constant)Numbers in names
// ✅ Numbers at end are fine
const variant2 = "secondary";
const attempt3 = retry();
// ❌ Don't start with numbers (invalid)
// const 2ndAttempt = retry(); // Syntax errorPrivate fields
// Use underscore prefix for private fields
class MyClass {
private _internalState: string;
public get state(): string {
return this._internalState;
}
}