Code Standards & Style
Consistency isn't about aesthetics, it's about reducing the cognitive load of reading someone else's code. When every file in the project follows the same patterns, a developer can focus on what the code does rather than deciphering how it's structured. Linters enforce the mechanical rules. This section covers the judgment calls that tooling can't make for you.
Why this matters
Code standards are how S&P's value of Teamwork shows up in the codebase. Every engineer on the team reads more code than they write. When naming, structure, and patterns are consistent, code review is faster, onboarding is smoother, and bugs are easier to spot because deviations from the pattern stand out. The goal is not uniformity for its own sake, it's a shared language that makes collaboration cheaper.
The standard
TypeScript configuration
Every project uses TypeScript with strict mode enabled and additional safety flags. This is the baseline tsconfig.json for new projects:
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"forceConsistentCasingInFileNames": true,
"isolatedModules": true,
"esModuleInterop": true,
"skipLibCheck": true
}
}
Why the extra flags beyond strict: true:
noUncheckedIndexedAccess: Forces you to handle theundefinedcase when accessing array elements or object keys by index. Catches a real class of runtime errors thatstrictalone misses.exactOptionalPropertyTypes: Distinguishes between "property is missing" and "property is present butundefined." This matters when dealing with API payloads and database records where the difference is semantically meaningful.
Existing projects that aren't on strict mode should migrate incrementally: enable one flag at a time, fix the resulting errors, and move on. Trying to flip everything at once on a large codebase creates a PR nobody can review.
Naming conventions
Consistent naming eliminates an entire category of "how do I find this file?" questions.
Files: kebab-case with type suffix
All files use kebab-case with a dot-separated type suffix that describes the file's role:
| Type | Pattern | Example |
|---|---|---|
| Utility | *.util.ts | date-format.util.ts |
| Test | *.spec.ts / *.test.ts | user.service.spec.ts |
| Type definitions | *.type.ts | user.type.ts |
| Constants | *.constant.ts | error-codes.constant.ts |
| Config | *.config.ts | database.config.ts |
| Migration | timestamp prefix | 1716652800000-create-users-table.ts |
For the full naming tables by discipline:
- NestJS file suffixes (
.service.ts,.controller.ts,.module.ts,.entity.ts,.dto.ts,.guard.ts, etc.): Backend Reference. Naming conventions - React file suffixes (
.component.tsx,.hook.ts,.context.tsx,.queries.ts, etc.): Frontend Reference. Naming conventions
Variables and functions: camelCase
const userCount = await this.userService.count();
function formatCurrency(amount: number, currency: string): string { ... }
Classes. PascalCase
class UserService { ... }
class CreateUserDto { ... }
class AuthGuard implements CanActivate { ... }
Constants and enum-like values. UPPER_SNAKE_CASE for module-level, camelCase for local
const MAX_RETRY_COUNT = 3;
const CACHE_TTL_SECONDS = 300;
function processItems(items: Item[]) {
const batchSize = 50;
// ...
}
Types and interfaces. PascalCase, no I prefix
type UserRole = 'admin' | 'editor' | 'viewer';
interface PaginationParams {
page: number;
limit: number;
}
Don't prefix interfaces with I (IUserService). It's a C# convention that adds noise without value in TypeScript, where the structural type system makes the interface/type distinction less meaningful.
Types over enums
Prefer as const objects over TypeScript enums. They produce better tree-shaking, have no runtime overhead beyond the object itself, and work more predictably with TypeScript's type inference.
// Prefer this
const USER_ROLE = {
ADMIN: 'admin',
EDITOR: 'editor',
VIEWER: 'viewer',
} as const;
type UserRole = (typeof USER_ROLE)[keyof typeof USER_ROLE];
// Not this
enum UserRole {
ADMIN = 'admin',
EDITOR = 'editor',
VIEWER = 'viewer',
}
For simple union types where you don't need a runtime object to iterate over, use a plain string union:
type Status = 'active' | 'inactive' | 'suspended';
Use as const objects when you need to iterate over the values, use them as lookup keys, or pass them around at runtime. Use string unions when the type is purely a compile-time constraint.
Import ordering
Organize imports in this order, separated by blank lines:
- Node built-ins:
node:path,node:fs,node:crypto - External packages:
@nestjs/common,react,zod - Internal aliases:
@/shared/,@/features/, workspace packages - Relative imports:
./user.service,../common/utils
import { readFile } from 'node:fs/promises';
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { PaginationDto } from '@/shared/dto/pagination.dto';
import { UserEntity } from './user.entity';
import { CreateUserDto } from './dto/create-user.dto';
This is a convention, not an enforced rule. The order is designed so that the most stable dependencies (Node, npm packages) appear first, and the most local dependencies (relative imports) appear last. When scanning a file, you can immediately see what external surface area it touches.
Error handling
Every language and framework has an idiomatic way to signal errors. Use it, don't invent a custom error system on top.
General principles:
- Use the framework's built-in error mechanism. NestJS has
HttpExceptionclasses, .NET has middleware exception handling, Go returns errors as values. Work with your framework, not around it. - Handle errors at the boundary, not everywhere. Register a global error handler (exception filter, middleware, recovery handler) for cross-cutting concerns like logging, error response shaping, and error tracking (Sentry). Don't scatter try/catch through every function.
- Reserve local error handling for recovery. Only catch locally when you can do something useful: retry a network call, fall back to a cached value, wrap a third-party library that throws unpredictably. If you're catching just to re-throw or log, the global handler already does that.
- Fail with context. Error messages should include enough information to diagnose the problem without reading the source code: what was expected, what was found, and which identifier was involved.
// Good: context in the error message
throw new NotFoundException(`User with id ${id} not found`);
// Bad, the person debugging this needs to read the stack trace
throw new NotFoundException('Not found');
Frontend: error boundaries + query error handling
React components should use error boundaries to catch render errors. Data fetching errors are handled through the query library's error state (React Query / SWR), not try/catch in components.
For framework-specific error handling patterns (NestJS exception filters, Go error wrapping, .NET middleware), see the discipline appendices.
Function and method design
Prefer early returns over deep nesting
// Prefer this
function processOrder(order: Order): Result {
if (!order.items.length) {
return { ok: false, error: 'Empty order' };
}
if (order.status !== 'pending') {
return { ok: false, error: 'Order already processed' };
}
const total = calculateTotal(order.items);
return { ok: true, data: { total } };
}
// Not this
function processOrder(order: Order): Result {
if (order.items.length) {
if (order.status === 'pending') {
const total = calculateTotal(order.items);
return { ok: true, data: { total } };
} else {
return { ok: false, error: 'Order already processed' };
}
} else {
return { ok: false, error: 'Empty order' };
}
}
Keep functions focused
A function should do one thing. If you're naming it validateAndSaveUser, that's two things: split them. The exception is thin orchestration functions that coordinate a sequence of focused steps.
Limit function parameters
More than three positional parameters is a code smell. Use an options object:
// Prefer this
function createReport(options: {
startDate: Date;
endDate: Date;
format: ReportFormat;
includeArchived?: boolean;
}): Report { ... }
// Not this
function createReport(
startDate: Date,
endDate: Date,
format: ReportFormat,
includeArchived?: boolean,
): Report { ... }
Backend patterns
These principles apply regardless of whether the project uses NestJS, Go, or .NET. Framework-specific conventions live in the Backend Reference.
Module boundaries. Organize backend code into cohesive modules (NestJS modules, Go packages, .NET projects/namespaces). Each module owns its data models, business logic, transport layer, and validation. Other modules consume it through its exported public API (never by reaching into internals. For NestJS module structure and directory layout, see Backend Reference) Project structure and modules.
Validate at the edge. Use separate input shapes (DTOs, request structs, input types) for create and update operations. Validate incoming data at the API boundary before it reaches business logic. Never trust client input. For NestJS DTO patterns with class-validator, see Backend Reference. DTOs and input validation.
Thin transport layer, thick domain layer. The transport layer (controllers, handlers, route functions) handles HTTP/gRPC/queue concerns: routing, request parsing, response shaping. Business logic lives in services or domain functions. If a handler is longer than a few lines beyond calling a service method, the logic probably belongs deeper.
React and Next.js patterns
Components follow a consistent internal layout: props interface, hooks, derived state, handlers, early returns, main render. Use named exports (not default exports) except where Next.js requires them for route files. Extract reusable logic into custom hooks (*.hook.ts). Collocate state with the component that owns it, don't lift state higher than necessary.
For the full component structure convention, code examples, hook extraction rules, and state colocation decision ladder, see Frontend Reference. Component patterns.
CSS and styling
Use Tailwind utility classes directly in JSX with cn() (from clsx + tailwind-merge) for conditional class composition. Use shadcn/ui as the component foundation: extend through composition, don't fork the generated source. Don't install a second component library alongside shadcn unless there's a specific capability gap.
For Tailwind class ordering conventions, cn() usage patterns, and shadcn customization approach, see Frontend Reference. Styling.
Database conventions
Database objects use snake_case: this is PostgreSQL convention and avoids quoting issues. Every table gets id (UUIDv7 primary key), created_at, and updated_at columns. UUIDv7 is preferred over UUIDv4 because it encodes a timestamp, giving natural ordering and better index locality. Foreign keys follow {referenced_table_singular}_id. Indexes are named explicitly (idx_{table}_{columns}).
Migrations are always forward-only in production. Never edit a migration that has been applied to a shared environment. Name files with a timestamp prefix and description: 1716652800000-create-user-accounts.ts.
For the full PostgreSQL conventions (table DDL examples, index naming, soft deletes, migration workflow, ORM setup, and database security), see Backend Reference. Database patterns.
Testing conventions
Testing structure, naming, and strategy are covered in detail in Testing Strategy. That section defines the testing trophy model, what to test at each level, and how to structure tests (AAA pattern, descriptive it blocks, colocation).
The one code-style convention to highlight here: use factory functions for test data instead of copy-pasting object literals across tests. This keeps tests readable and makes it easy to update the shape when the entity changes:
function buildUser(overrides: Partial<User> = {}): User {
return {
id: '019414d0-8b3a-7f1e-9a2b-0242ac120002',
email: 'test@example.com',
name: 'Test User',
role: 'viewer',
...overrides,
};
}
it('grants access to admin users', () => {
const admin = buildUser({ role: 'admin' });
expect(canAccessDashboard(admin)).toBe(true);
});
Critical thinking
When consistency is worth breaking
These standards optimize for the common case. There are legitimate reasons to deviate:
- Third-party integration code that needs to match an external API's naming conventions (snake_case JSON from a partner API, PascalCase from a .NET service). Keep the adapter layer thin and convert to your conventions at the boundary.
- Performance-critical paths where a less idiomatic pattern measurably improves throughput. Document why with a comment.
- Framework constraints where the framework's conventions override yours (Next.js default exports for pages, NestJS decorator patterns, Go's exported/unexported naming).
The test is: can someone reading this code understand why it deviates? If you need more than one sentence to explain, the deviation might not be worth it.
Types: strict enough, not paranoid
Strict TypeScript catches real bugs. But there's a point where the type gymnastics cost more to maintain than the bugs they prevent. If you're spending more time satisfying the type checker than writing business logic, the types are too complex. Use unknown and narrow rather than writing a ten-line generic. Use // @ts-expect-error with a comment when the type system genuinely can't model what you need, but only after you've tried the obvious solutions.
Consistency within a project beats consistency across projects
If you're joining a project that uses a different convention than this playbook (PascalCase files, default exports, enums instead of const objects), follow the project's convention. Consistency within a single codebase matters more than alignment with a company-wide standard. If you want to align the project with the playbook, do it as a dedicated refactoring PR, not as a side effect of a feature.
Checklist
Use this when writing or reviewing code:
- TypeScript strict mode is enabled with
noUncheckedIndexedAccessandexactOptionalPropertyTypes - Files use kebab-case with type suffix (
.service.ts,.component.tsx,.hook.ts) - Constants use
as constobjects instead of enums - No
Iprefix on interfaces - Imports follow the convention order: node built-ins, external, internal aliases, relative
- Functions use early returns instead of deep nesting
- Functions with more than three parameters use an options object
- Transport layer is thin: business logic lives in services/domain layer
- Input validation exists for every create/update operation at the API boundary
- React components use named exports (except Next.js pages/layouts)
- Database tables use snake_case with UUIDv7
id,created_at,updated_atcolumns - Test data uses factory functions, not copy-pasted object literals (see Testing Strategy for full conventions)
AI tips
- Use AI to enforce naming conventions in new code. Add naming rules to
.cursor/rules/so the AI generates files with the correct suffix (user.service.tsnotUserService.ts) and follows the variable naming patterns. - Generate DTOs from entities. Describe your entity and ask AI to generate the corresponding
CreateDto,UpdateDto, andResponseDtowith the rightclass-validatordecorators. Review the validation rules. AI sometimes misses domain-specific constraints. - Convert enums to const objects. If you're migrating a file from enums to
as constobjects, paste the enum and ask AI to generate the equivalent const object plus the derived type. It handles thetypeof/keyofboilerplate reliably. - Scaffold test factories. Give AI your entity type and ask it to generate a
buildUser()/buildOrder()factory function with sensible defaults. Then customize the defaults for your domain. - Review for naming consistency. Paste a module's file listing and ask AI to flag files that don't follow the naming convention. It's good at pattern-matching across a list of file names.
Resources
TypeScript:
- TypeScript Handbook (Strict Mode) What each strict flag does
- Total TypeScript. Advanced TypeScript patterns and tips
- Clean Code JavaScript. Adapted clean code principles for JS/TS
NestJS:
- NestJS documentation. Official docs
- Tao of Node. Node.js conventions and patterns
- Node.js Best Practices. Comprehensive best practices collection
React / Next.js:
- Tao of React. React conventions and patterns
- Bulletproof React. React project structure reference
- AirBnB JavaScript Style Guide. Widely-used JS style guide for reference
Frontend:
- Tailwind CSS documentation. Utility class reference
- shadcn/ui. Component library
- Radix UI. Headless accessible primitives
Database:
- PostgreSQL naming conventions. Official identifier rules