OneApp Docs
Guides

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 namingisLoading in one file, loading in another, loadingState in 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/can prefix (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 — Use unknown with type guards
  • ✅ Always add explicit return types for exports
  • ✅ Prefer satisfies over type assertions
  • ✅ Use branded types for IDs (UserId, ProductId)

That's it! ESLint and Prettier enforce most conventions automatically.

Naming conventions

Files and directories

TypeConventionExampleWhen to Use
ComponentsPascalCaseButton.tsx, UserProfile.tsxReact components
UtilitiescamelCaseformatDate.ts, apiClient.tsHelper functions
Tests*.test.tsxButton.test.tsxVitest tests
TypesPascalCaseUser.ts, ApiResponse.tsType definitions
Packageskebab-casepackages/my-packageInternal packages
Directorieskebab-caseuser-profile/, api-client/Folders

Variables and functions

Good examples
// ✅ 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) {
  /* ... */
}
Bad examples
// ❌ 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 retrieved

Boolean 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 constant

React 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:

Correct import order
// 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:

Bad - using any
// ❌ Loses all type safety
function process(data: any) {
  return data.value; // No autocomplete, no type checking
}
Good - using unknown
// ✅ 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:

Bad - inferred return types
// ❌ Return type inferred (unclear, fragile)
export function getUser(id: string) {
  return db.users.findUnique({ where: { id } });
}
Good - explicit return types
// ✅ 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:

Using satisfies
// ✅ 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
Using type assertion (avoid)
// ❌ 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:

packages/types/src/brands.ts
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;
}
Usage
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:

packages/ui/src/Card.tsx
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 ComponentNameProps interface
  • ✅ Add JSDoc with description and example
  • ✅ Explicit React.ReactElement return type
  • ✅ Destructure props in parameter list

Server vs Client Components

Server Component (default)
// No directive needed - server by default in Next.js App Router
export function ServerComponent() {
  return <div>Rendered on server;
}
Client Component (when needed)
"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:

Well-documented function
/**
 * 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)
  • @param for each parameter
  • @returns for return value
  • @throws for errors (if applicable)
  • @example for 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:

prettier.config.js
import config from "@repo/config/prettier";

export default config;

ESLint rules

ESLint enforces code quality:

eslint.config.mjs
import reactConfig from "@repo/config/eslint/react";

export default [...reactConfig];

Key rules enforced:

  • No any types
  • No unused variables
  • Explicit return types for exports
  • Proper import order
  • No console.log in production
  • Exhaustive switch statements
  • No magic numbers

Next steps

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 readonly

File 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 styles

Utility file structure

utils/
├── formatDate.ts        # Single responsibility per file
├── formatDate.test.ts   # Co-located tests
├── formatCurrency.ts
├── formatCurrency.test.ts
└── index.ts             # Barrel export

Package 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.md

Custom ESLint rules

Adding rules

eslint.config.mjs
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

eslint.config.mjs
export default [
  ...reactConfig,
  {
    ignores: ["dist/", ".next/", "coverage/", "**/*.config.js"]
  }
];

Custom Prettier config

Override Prettier rules (avoid unless necessary):

prettier.config.js
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 error

Private fields

// Use underscore prefix for private fields
class MyClass {
  private _internalState: string;

  public get state(): string {
    return this._internalState;
  }
}

On this page