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
| Type | Convention | Example |
|---|---|---|
| Components, Interfaces, Types | PascalCase | UserProfile, TCard, IFormProps |
| Variables, Functions | camelCase | userName, fetchUserData, isValid |
| Constants | ALL_CAPS | API_ENDPOINT, MAX_RETRIES |
| Private Class Members | _prefixed (ok) or #hash-syntax (preferred) | private _apiKey: string or #apiKey: string |
| Enum Members | PascalCase | UserRole.Admin, Status.Pending |
| Boolean Variables | is/has prefix | isLoading, hasError, canEdit |
File Naming
| File Type | Convention | Example |
|---|---|---|
| React Components | PascalCase.tsx | BranchSearchForm.tsx, UserCard.tsx |
| Next.js Pages | kebab-case or [param] | page.tsx, [id]/page.tsx |
| Next.js Layouts | layout.tsx | layout.tsx, [code]/layout.tsx |
| Utility Files | kebab-case.ts | branch-utilities.ts, date-helpers.ts |
| Type Definitions | PascalCase.ts | UserTypes.ts, ApiResponse.ts |
| Test Files | *.test.tsx/ts | UserCard.test.tsx, utils.test.ts |
| Story Files | *.stories.tsx | Button.stories.tsx |
Summary of Naming Conventions
| Context | Convention | Example |
|---|---|---|
| TypeScript Types | PascalCase with T prefix | TUserData, TApiResponse |
| TypeScript Interfaces | PascalCase with I prefix | IUserProfile, IFormProps |
| React Components | PascalCase | UserCard, SearchBar |
| Functions | camelCase | fetchUserData, validateEmail |
| Event Handlers | camelCase with handle prefix | handleClick, handleSubmit |
| Constants | ALL_CAPS | API_BASE_URL, MAX_ITEMS |
| Enums | PascalCase with E prefix | ERole, EDataLayer |
| Booleans | camelCase with is/has/can | isValid, hasError, canEdit |
| Files (Components) | PascalCase.tsx | UserProfile.tsx |
| Files (Utils) | kebab-case.ts | date-utils.ts |
| CVA Variants | camelCase with Variants suffix | buttonVariants, cardVariants |
| Tailwind Classes | Utility classes | flex, items-center, bg-blue-500 |
| CSS Classes (BEM) | kebab-case | user-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()
-
Always use TypeScript for all new code
- Enable
strictmode in alltsconfig.jsonfiles - Follow the project's TypeScript configuration
- Prefer type safety and inference over explicit typing when clear
- Enable
-
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 { } -
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 }; -
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
-
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
}; -
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
-
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>
);
} -
Component Best Practices
- Keep components small and focused on a single responsibility
- Use TypeScript interfaces for props
- Use
React.FCtype for components with children - Return type should be explicitly
JSX.Element - Always mark client components with
'use client'directive
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>
);
}
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>;
}
-
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!
}
} -
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
-
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>
);
} -
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
-
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 -
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
- Always use
.safeParse()for user input - Returns{ success: boolean, data?, error? }instead of throwing - Provide clear error messages - Use custom error messages for better user experience
- Validate at boundaries - Validate data when it enters your system (API routes, form handlers, external sources)
- Reuse schemas - Define schemas once and reuse them across your application
- 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 dynamicrequire()with user input - ❌ Expose
process.envdirectly to client-side code - ❌ Log full request bodies or headers that may contain PII
- ❌ Hardcode secrets or API keys in code
- ❌ Commit
.envfiles 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>