Skip to main content

Coding Style Guide

This document outlines the coding standards and best practices for our TypeScript, React, and Next.js monorepo. Following these guidelines ensures code consistency, maintainability, and security across all applications and packages.

Naming Conventions

Consistent naming conventions improve code readability and maintainability across the entire monorepo. Follow these guidelines for all TypeScript, React, Next.js, TailwindCSS, shadcn, and CVA code.

General Rules

TypeConventionExample
Components, Interfaces, TypesPascalCaseUserProfile, TCard, IFormProps
Variables, FunctionscamelCaseuserName, fetchUserData, isValid
ConstantsALL_CAPSAPI_ENDPOINT, MAX_RETRIES
Private Class Members_prefixed (ok) or
#hash-syntax (preferred)
private _apiKey: string or
#apiKey: string
Enum MembersPascalCaseUserRole.Admin, Status.Pending
Boolean Variablesis/has prefixisLoading, hasError, canEdit

File Naming

File TypeConventionExample
React ComponentsPascalCase.tsxBranchSearchForm.tsx, UserCard.tsx
Next.js Pageskebab-case or [param]page.tsx, [id]/page.tsx
Next.js Layoutslayout.tsxlayout.tsx, [code]/layout.tsx
Utility Fileskebab-case.tsbranch-utilities.ts, date-helpers.ts
Type DefinitionsPascalCase.tsUserTypes.ts, ApiResponse.ts
Test Files*.test.tsx/tsUserCard.test.tsx, utils.test.ts
Story Files*.stories.tsxButton.stories.tsx

Summary of Naming Conventions

ContextConventionExample
TypeScript TypesPascalCase with T prefixTUserData, TApiResponse
TypeScript InterfacesPascalCase with I prefixIUserProfile, IFormProps
React ComponentsPascalCaseUserCard, SearchBar
FunctionscamelCasefetchUserData, validateEmail
Event HandlerscamelCase with handle prefixhandleClick, handleSubmit
ConstantsALL_CAPSAPI_BASE_URL, MAX_ITEMS
EnumsPascalCase with E prefixERole, EDataLayer
BooleanscamelCase with is/has/canisValid, hasError, canEdit
Files (Components)PascalCase.tsxUserProfile.tsx
Files (Utils)kebab-case.tsdate-utils.ts
CVA VariantscamelCase with Variants suffixbuttonVariants, cardVariants
Tailwind ClassesUtility classesflex, items-center, bg-blue-500
CSS Classes (BEM)kebab-caseuser-card, user-card__title

TypeScript Guidelines

Core Principles

Key Rules:

  • ✅ Use interfaces for data structures
  • ✅ Use type aliases for unions, intersections, and utilities
  • ❌ Avoid using 'any' type
  • ✅ Use specific types or generics (use generic type constraints if possible)
  • ✅ Use const and readonly
  • ✅ Prefer immutable operations
  • ✅ Use optional chaining
  • ✅ Use nullish coalescing
  • ✅ Use async/await over Promise chains
  • ❌ Avoid Promise chains with .then()/.catch()
  1. Always use TypeScript for all new code

    • Enable strict mode in all tsconfig.json files
    • Follow the project's TypeScript configuration
    • Prefer type safety and inference over explicit typing when clear
  2. Type Definitions

    // ✅ GOOD: Use interfaces for data structures
    interface UserProfile {
    firstName: string;
    lastName: string;
    email: string;
    phoneNumber?: string;
    }

    // ✅ GOOD: Use type aliases for unions, intersections, and utilities
    type UserRole = 'admin' | 'user' | 'guest';
    type ApiResponse<T> = {
    data: T;
    status: number;
    error?: string;
    };

    // ❌ AVOID: Using 'any' type
    function processData(data: any) { } // Don't do this

    // ✅ GOOD: Use specific types or generics with constraints (if possible)
    // Here, T is constained to be an object type:
    function processData<T extends object>(data: T): T { }
  3. Immutability

    // ✅ GOOD: Use const and readonly
    const API_ENDPOINT = 'https://api.example.com';

    interface Config {
    readonly apiKey: string;
    readonly timeout: number;
    }

    // ✅ GOOD: Prefer immutable operations
    const updatedArray = [...originalArray, newItem];
    const updatedObject = { ...originalObject, newProp: value };
  4. Modern TypeScript Features

    // ✅ GOOD: Use optional chaining
    const userName = user?.profile?.name;

    // ✅ GOOD: Use nullish coalescing
    const displayName = userName ?? 'Anonymous';

    // ✅ GOOD: Use async/await over Promise chains
    async function fetchUserData(userId: string) {
    try {
    const response = await fetch(`/api/users/${userId}`);
    const data = await response.json();
    return data;
    } catch (error) {
    SchLogger.error(error);
    throw error;
    }
    }

    // ❌ AVOID: Promise chains with .then()/.catch()
    function fetchUserData(userId: string) {
    return fetch(`/api/users/${userId}`)
    .then(response => response.json())
    .catch(error => console.error(error));
    }

Function Guidelines

Key Rules:

  • ✅ Named functions for reusability and testability
  1. Prefer Named Functions

    // ✅ GOOD: Named functions for reusability and testability
    async function submitLeadForm(formData: FormData): Promise<void> {
    // Implementation
    }

    // ✅ ACCEPTABLE: Arrow functions for callbacks
    const handleClick = () => {
    // Simple callback logic
    };
  2. JSDoc Requirements

    All functions must have JSDoc comments with at minimum:

    • @description - What the function does
    • @param - Each parameter with type and description
    • @returns - What the function returns
    /**
    * Retrieves search results for a given search term
    * @description Fetches search results using the provided URL and query parameter
    * @param {string} url - The base endpoint to fetch data from
    * @param {{ arg: { searchTerm: string } }} params - Object containing the search term
    * @returns {Promise<Array<{ label: string; value: string }> | null>} Search results or null
    */
    export async function fetchSuggestions(
    url: string,
    { arg }: { arg: { searchTerm: string } }
    ): Promise<Array<{ label: string; value: string }> | null> {
    if (!searchTerm) {
    return null;
    }

    try {
    const response = await fetch(`${url}?query=${encodeURIComponent(searchTerm)}`);
    if (!response.ok) {
    return null;
    }
    const data = await response.json();
    return data;
    } catch (error) {
    SchLogger.error(error);
    return null;
    }
    }

Input Validation

Key Rules:

  • ✅ Define schemas for validation

All structured input must be validated using Zod:

import { z } from 'zod';
import validator from 'validator';

// ✅ GOOD: Define schemas for validation
const UserEntrySchema = z.object({
firstName: z.string().min(1, { message: 'Please enter a valid first name.' }),
lastName: z.string().min(1, { message: 'Please enter a valid last name.' }),
email: z.string().email({ message: 'Please enter a valid email address.' }),
phoneNumber: z.string().refine(validator.isMobilePhone, {
message: 'Please enter a valid phone number.'
}),
});

// At the end of your Zod schema file, be sure to export your schema's
// inferred TypeScript type like so:
type UserEntry = z.infer<typeof UserEntrySchema>;

// Validate and parse data
const result = UserEntrySchema.safeParse(inputData);

if (!result.success) {
// Handle validation errors
const errors = result.error.issues;
return { errors };
}

React Guidelines

Component Structure

  1. Use Functional Components with Hooks

    'use client';

    import { useState, useEffect, JSX } from 'react';

    interface UserCardProps {
    userId: string;
    displayName: string;
    children?: React.ReactNode;
    }

    /**
    * A React functional component that displays user information
    * @description Renders a card with user details and handles user interactions
    * @param {UserCardProps} props - Component properties
    * @returns {JSX.Element} The rendered user card
    */
    export default function UserCard({
    userId,
    displayName,
    children
    }: UserCardProps): JSX.Element {
    const [isActive, setIsActive] = useState(false);

    useEffect(() => {
    // Effect logic
    }, [userId]);

    return (
    <div className="user-card">
    <h2>{displayName}</h2>
    {children}
    </div>
    );
    }
  2. Component Best Practices

    • Keep components small and focused on a single responsibility
    • Use TypeScript interfaces for props
    • Use React.FC type for components with children
    • Return type should be explicitly JSX.Element
    • Always mark client components with 'use client' directive
Spread Syntax Usage

Use the spread syntax (...) sparingly in React components. It can cause problems with types and testing, particularly when components use the React type that allows extending HTML elements. When spreading props internally, automated tests may presume the component supports attributes it doesn't actually handle, leading to type inconsistencies and testing failures.

// ❌ AVOID: Excessive spread usage
interface ButtonProps extends React.HTMLAttributes<HTMLButtonElement> {
label: string;
}

function Button({ label, ...props }: ButtonProps) {
return <button {...props}>{label}</button>; // Tests may fail
}

// ✅ GOOD: Explicit prop passing
interface ButtonProps {
label: string;
onClick?: () => void;
className?: string;
}

function Button({ label, onClick, className }: ButtonProps) {
return (
<button onClick={onClick} className={className}>
{label}
</button>
);
}
Returning Nothing from Components

When a component needs to return nothing, use return null instead of return <></>. While <></> (empty fragments) technically work, they still imply a small overhead for React to handle during rendering. Using null is more explicit and efficient.

// ❌ AVOID: Empty fragments add unnecessary overhead
function ConditionalComponent({ show }: { show: boolean }) {
if (!show) {
return <></>;
}
return <div>Content</div>;
}

// ✅ GOOD: Return null explicitly
function ConditionalComponent({ show }: { show: boolean }) {
if (!show) {
return null;
}
return <div>Content</div>;
}
  1. Hooks Rules

    Key Rules:

    • ✅ Hooks at top level
    • ❌ Avoid conditional hooks
    // ✅ GOOD: Hooks at top level
    function UserProfile() {
    const [user, setUser] = useState(null);
    const [loading, setLoading] = useState(false);

    // More component logic
    }

    // ❌ AVOID: Conditional hooks
    function UserProfile() {
    if (someCondition) {
    const [user, setUser] = useState(null); // Don't do this!
    }
    }
  2. State Management

    Key Rules:

    • ✅ Avoid global state unless using scoped context providers
    // ✅ GOOD: Avoid global state unless using scoped context providers
    const ThemeContext = createContext<ThemeContextType | undefined>(undefined);

    export function ThemeProvider({ children }: { children: React.ReactNode }) {
    const [theme, setTheme] = useState('light');

    return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
    {children}
    </ThemeContext.Provider>
    );
    }

Styling

Key Rules:

  • ✅ Tailwind utility classes
  1. Use TailwindCSS for Component Styling

    // ✅ GOOD: Tailwind utility classes
    export default function Button({ text }: { text: string }): JSX.Element {
    return (
    <button className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">
    {text}
    </button>
    );
    }
  2. Class Variance Authority (CVA) for Variants

    import { cva } from 'class-variance-authority';

    const buttonVariants = cva(
    'px-4 py-2 rounded font-medium transition-colors',
    {
    variants: {
    intent: {
    primary: 'bg-blue-500 text-white hover:bg-blue-600',
    secondary: 'bg-gray-200 text-gray-800 hover:bg-gray-300',
    },
    size: {
    sm: 'text-sm px-3 py-1.5',
    md: 'text-base px-4 py-2',
    lg: 'text-lg px-6 py-3',
    },
    },
    defaultVariants: {
    intent: 'primary',
    size: 'md',
    },
    }
    );

React Component Documentation

/**
* A React functional component that generates a branch search form
* @description Renders a search interface with Google Places autocomplete
* @param {BranchSearchFormProps} props - Component properties
* @param {React.ReactNode} props.children - Child components to render
* @param {TCard} props.marqueeContent - Content for the hero section
* @param {TFeatureFlag} props.flags - Feature flags for conditional rendering
* @returns {JSX.Element} The rendered search form
*/
export default function BranchSearchForm({
children,
marqueeContent,
flags,
}: BranchSearchFormProps): JSX.Element {
// Implementation
}

Next.js Guidelines

File-Based Routing

  1. App Router Structure

    app/
    [code]/
    page.tsx # Dynamic route
    layout.tsx # Shared layout
    loading.tsx # Loading UI
    error.tsx # Error boundary
    not-found.tsx # 404 page
  2. Server vs Client Components

    // ✅ Server Component (default, no directive)
    export default async function ServerPage() {
    const data = await fetchData();
    return <div>{data}</div>;
    }

    // ✅ Client Component (requires directive)
    'use client';

    import { useState } from 'react';

    export default function ClientComponent() {
    const [count, setCount] = useState(0);
    return <button onClick={() => setCount(count + 1)}>{count}</button>;
    }

Middleware

import { type NextRequest, NextResponse } from 'next/server';

export const config = {
matcher: [
'/((?!_next|api|favicon\\.ico).*)',
],
};

/**
* Middleware function for handling redirects, flags, and headers
* @description Processes requests before they reach the page
* @param {NextRequest} request - The incoming request
* @returns {Promise<NextResponse>} The response with modified headers/redirects
*/
export async function middleware(request: NextRequest) {
const requestHeaders = new Headers(request.headers);

// Add custom headers
requestHeaders.set('x-custom-header', 'value');

const response = NextResponse.next({
request: {
headers: requestHeaders,
},
});

return response;
}

Server Actions

'use server';

import { z } from 'zod';
import { headers } from 'next/headers';

/**
* Server action to handle form submission
* @description Validates and processes form data server-side
* @param {Record<string, any>} extraData - Additional hidden data
* @param {any} initialState - Initial state object
* @param {FormData} formData - Form data from client
* @returns {Promise<{ parse?: any; errors?: any }>} Result of the operation
*/
export default async function submitFormAction(
extraData: Record<string, any>,
initialState: any,
formData: FormData,
) {
// Define validation schema
const schema = z.object({
firstName: z.string().min(1),
lastName: z.string().min(1),
email: z.string().email(),
});

// Parse and validate
const parse = schema.safeParse({
firstName: formData.get('firstName'),
lastName: formData.get('lastName'),
email: formData.get('email'),
});

if (!parse.success) {
return { errors: parse.error.issues };
}

// Process valid data
const data = parse.data;
// ... perform server-side operations

return { parse };
}

Next.js Configuration

/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
trailingSlash: false,
experimental: {
serverActions: {
allowedOrigins: ['schwab.com', '*.schwab.com'],
},
},
async headers() {
return [
{
source: '/:path*',
headers: [
{
key: 'Strict-Transport-Security',
value: 'max-age=63072000; includeSubDomains; preload',
},
{
key: 'X-Content-Type-Options',
value: 'nosniff',
},
],
},
];
},
};

module.exports = nextConfig;

Zod Validation Guidelines

Core Principles

Zod is our TypeScript-first schema validation library. Use it for all structured input validation, especially for:

  • API request/response validation
  • Form data validation
  • Environment variable validation
  • External data sources (JSON, user input, third-party APIs)

Schema Naming Conventions

Key Rules:

  • ✅ Schema names use PascalCase with 'Schema' suffix
  • ✅ Infer TypeScript types from schemas
// ✅ GOOD: Schema names use PascalCase with 'Schema' suffix
const UserProfileSchema = z.object({
firstName: z.string(),
lastName: z.string(),
email: z.string().email(),
age: z.number().int().positive().optional(),
});

// ✅ GOOD: Infer TypeScript types from schemas
type TUserProfile = z.infer<typeof UserProfileSchema>;

Best Practices

Key Rules:

  • ✅ Transform data during validation
  • ✅ Reuse and extend schemas
  1. Always use .safeParse() for user input - Returns { success: boolean, data?, error? } instead of throwing
  2. Provide clear error messages - Use custom error messages for better user experience
  3. Validate at boundaries - Validate data when it enters your system (API routes, form handlers, external sources)
  4. Reuse schemas - Define schemas once and reuse them across your application
  5. Use .transform() for data normalization - Clean and normalize data during validation
// ✅ GOOD: Transform data during validation
const EmailSchema = z.string()
.email()
.transform((email) => email.toLowerCase().trim());

// ✅ GOOD: Reuse and extend schemas
const BaseUserSchema = z.object({
firstName: z.string(),
lastName: z.string(),
});

const UserWithEmailSchema = BaseUserSchema.extend({
email: z.string().email(),
});

Security Standards

Input Validation and Sanitization

Key Rules:

  • ✅ Always validate and sanitize user input
  • ✅ Use parameterized queries
import { z } from 'zod';
import validator from 'validator';

// ✅ GOOD: Always validate and sanitize user input
const SafeInputSchema = z.object({
comment: z.string()
.min(1)
.transform((val) =>
SfmcUtils.cleanData(val, SfmcUtils.regexCleaningPatterns.comment)
),
email: z.string().email(),
phone: z.string().refine(validator.isMobilePhone),
});

// ✅ GOOD: Use parameterized queries
const user = await db.query('SELECT * FROM users WHERE id = $1', [userId]);

// ❌ NEVER: Concatenate SQL or build queries from user input
const user = await db.query(`SELECT * FROM users WHERE id = ${userId}`); // Don't!

Security Rules

DO NOT:

  • ❌ Use eval, new Function(), or dynamic require() with user input
  • ❌ Expose process.env directly to client-side code
  • ❌ Log full request bodies or headers that may contain PII
  • ❌ Hardcode secrets or API keys in code
  • ❌ Commit .env files to version control
  • ❌ Disable TLS checks, even temporarily
  • ❌ Render raw user data without escaping (prevents XSS)

DO:

  • ✅ Sanitize and escape all user input
  • ✅ Validate all input strictly using typed parsers
  • ✅ Use parameterized queries (prevent injection)
  • ✅ Store secrets in secure environment variables
  • ✅ Redact PII from logs by default
  • ✅ Use allow-lists over deny-lists for validation

Example: Secure Data Handling

'use server';

import { headers } from 'next/headers';

/**
* Secure server action with proper data handling
* @description Validates input and protects sensitive data
* @param {FormData} formData - Form data from client
* @returns {Promise<{ success: boolean }>} Operation result
*/
export async function secureAction(formData: FormData) {
// ✅ Get IP from headers securely
const ipAddress = (await headers())
.get('x-forwarded-for')
?.split(',')[0] || 'unknown';

// ✅ Validate input with Zod
const schema = z.object({
email: z.string().email(),
message: z.string()
.min(1)
.transform(val => sanitizeInput(val)),
});

const result = schema.safeParse({
email: formData.get('email'),
message: formData.get('message'),
});

if (!result.success) {
// ✅ Log errors without exposing sensitive data
SchLogger.error('Validation failed', {
ip: ipAddress,
errors: result.error.issues
});
return { success: false };
}

// ✅ Process validated data
const { email, message } = result.data;

// ... handle securely

return { success: true };
}

Error Handling

Try-Catch for Async Operations

Key Rules:

  • ✅ Wrap async operations in try-catch
// ✅ GOOD: Wrap async operations in try-catch
async function fetchUserProfile(userId: string): Promise<UserProfile | null> {
try {
const response = await fetch(`/api/users/${userId}`);

if (!response.ok) {
throw new ApiError('Failed to fetch user', response.status);
}

const data = await response.json();
return data;
} catch (error) {
SchLogger.error('Error fetching user profile', { userId, error });
return null;
}
}

React Error Boundaries

'use client';

import { Component, type ReactNode } from 'react';

interface ErrorBoundaryProps {
children: ReactNode;
fallback?: ReactNode;
}

interface ErrorBoundaryState {
hasError: boolean;
error?: Error;
}

/**
* Error boundary component to catch React rendering errors
* @description Catches errors in child components and displays fallback UI
*/
export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
constructor(props: ErrorBoundaryProps) {
super(props);
this.state = { hasError: false };
}

static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { hasError: true, error };
}

componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
SchLogger.error('Component error caught', { error, errorInfo });
}

render() {
if (this.state.hasError) {
return this.props.fallback || <div>Something went wrong</div>;
}

return this.props.children;
}
}

Contextual Error Logging

Key Rules:

  • ✅ Always log errors with context
// ✅ GOOD: Always log errors with context
import { SchLogger } from '@schwab/utilities/SchLogger';
...
...
...
try {
await processPayment(orderId, amount);
} catch (error) {
SchLogger.error('Payment processing failed', {
orderId,
amount,
error,
timestamp: new Date().toISOString(),
});
throw new PaymentError('Unable to process payment', { orderId });
}

Accessibility Guidelines

Accessibility is a core requirement for all code. Your code should achieve WCAG v2.2 Level A (minimum) and AA (medium) compliance.

Core Principles (POUR)

We follow the WCAG POUR principles: Perceivable, Operable, Understandable, and Robust.

Perceivable: Provide text alternatives for non-text content (e.g., alt attributes, captions).

// ✅ Good: Image with alt text
<img src="/logo.png" alt="Company Logo" />

// ❌ Bad: Missing alt text
<img src="/logo.png" />

Operable: Ensure full keyboard navigation and visible focus indicators.

/* ✅ Good: Visible focus indicator */
button:focus-visible {
outline: 2px solid blue;
outline-offset: 2px;
}

Understandable: Use clear labels, predictable navigation, and descriptive link text.

// ✅ Good: Descriptive link text
<a href="/docs/intro">Read our introduction guide</a>

// ❌ Bad: Non-descriptive link text
<a href="/docs/intro">Click here</a>

Robust: Code should work with assistive technologies (proper roles, states, and values).

// ✅ Good: Proper ARIA attributes
<button aria-expanded={isOpen} aria-controls="menu">
Menu
</button>

Accessibility Checklist

Semantic HTML

Use native elements whenever possible (<button>, <nav>, <header>).

// ✅ Good: Using semantic HTML
<nav>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
</ul>
</nav>

// ❌ Bad: Using divs for everything
<div onClick={navigate}>
<div>Home</div>
</div>

ARIA

Apply ARIA roles and attributes only when semantics are missing.

// ✅ Good: ARIA only when needed
<div role="alert" aria-live="polite">
Form submitted successfully
</div>

// ✅ Better: Use semantic HTML when possible
<button>Submit</button> // No ARIA needed

Keyboard Support

All interactive elements must be reachable and operable via keyboard. Maintain visible focus styles.

// ✅ Good: Keyboard accessible custom component
function CustomButton({ onClick, children }) {
return (
<div
role="button"
tabIndex={0}
onClick={onClick}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
onClick(e);
}
}}
>
{children}
</div>
);
}

Color & Contrast

Text contrast ratio: 4.5:1 (normal), 3:1 (large text).

/* ✅ Good: Sufficient contrast */
.text {
color: #333333; /* Dark gray */
background-color: #ffffff; /* White */
/* Contrast ratio: 12.6:1 */
}

/* ❌ Bad: Insufficient contrast */
.text-bad {
color: #cccccc; /* Light gray */
background-color: #ffffff; /* White */
/* Contrast ratio: 1.6:1 - fails WCAG */
}

Forms

Provide explicit labels for inputs.

// ✅ Good: Label associated with input
<label htmlFor="email">Email Address</label>
<input type="email" id="email" name="email" />

// ✅ Also good: Wrapping label
<label>
Email Address
<input type="email" name="email" />
</label>

// ❌ Bad: No label
<input type="email" placeholder="Email" />

Media

Respect prefers-reduced-motion for animations and autoplay videos.

import useDocusaurusContext from '@docusaurus/useDocusaurusContext';

function AnimatedComponent() {
const prefersReducedMotion =
window.matchMedia('(prefers-reduced-motion: reduce)').matches;

return (
<div
style={{
transition: prefersReducedMotion ? 'none' : 'transform 0.3s ease'
}}
>
Content
</div>
);
}

Next.js

Use <Link> for navigation.

import Link from 'next/link';

// ✅ Good: Using Next.js Link
<Link href="/docs/intro">
Get Started
</Link>

// ❌ Bad: Using regular anchor
<a href="/docs/intro">Get Started</a>