Coding Standards
Original work by ECC. Keep this credit when you share or change this skill.
Use these rules for TypeScript, JavaScript, React, and Node.js work.
When to Use This Skill
Use this skill when you:
- Start a project or module
- Write or review code
- Fix hard-to-read code
- Set naming or style rules
- Set up lint, format, or type checks
- Help a new person join a project
First Steps
Before you change code:
- Read the project rules.
- Check nearby code for local style.
- Find the files and tests that may change.
- Make the smallest safe change.
- Run the right checks.
Project rules come first. Do not change the whole code style unless the user asks.
Core Rules
Make Code Easy to Read
- Use clear names.
- Keep functions small and focused.
- Use the same style across nearby files.
- Write comments for reasons, limits, or risks.
- Do not use comments to repeat the code.
// Good
const searchQuery = 'election'
const isSignedIn = true
const totalRevenue = 1000
// Bad
const q = 'election'
const flag = true
const x = 1000Short names are fine for small, common scopes.
for (let i = 0; i < items.length; i += 1) {
console.log(items[i])
}Keep It Simple
- Use the simplest safe fix.
- Do not add layers without a real need.
- Do not tune speed before you find a slow part.
- Pick clear code over clever code.
Avoid Harmful Copying
Move shared code into one place when it holds the same rule.
Do not join code only because it looks alike. Two parts may need to change for different reasons. A small repeat can be safer than the wrong shared helper.
Build Only What Is Needed
- Do not add features for a guessed future need.
- Do not add settings with no current use.
- Add more parts only when the task needs them.
TypeScript and JavaScript
Names
Use:
camelCasefor values and functionsPascalCasefor classes, types, and React partsUPPER_SNAKE_CASEfor true fixed values- Names that show what a Boolean means, such as
isOpenorhasAccess
Name functions with an action.
async function fetchMarket(marketId: string) {}
function calculateTotal(prices: number[]) {}
function isValidEmail(email: string): boolean {
return email.includes('@')
}Avoid vague names.
async function market(id: string) {}
function process(data: unknown) {}Types
- Use exact types.
- Use
unknownfor data you have not checked. - Avoid
any. - Check data at system edges, such as API input and file input.
- Use one name style for fields inside the app.
interface Market {
id: string
name: string
status: 'active' | 'resolved' | 'closed'
createdAt: Date
}
function getMarket(id: string): Promise<Market> {
throw new Error('Not implemented')
}API JSON cannot hold a Date value. Use a date string at the API edge, then parse it if the app needs a Date.
interface MarketJson {
id: string
createdAt: string
}Do not hide a type error with a cast unless you have checked the value first.
Data Changes
Prefer new values when data is shared or used as state.
const updatedUser = {
...user,
name: 'New Name'
}
const updatedItems = [...items, newItem]Do not change React state or shared input in place.
user.name = 'New Name'
items.push(newItem)Local change is fine when the value is owned by one small scope and cannot leak out.
const results: string[] = []
for (const item of items) {
results.push(item.name)
}Functions
- Give each function one clear job.
- Return early for bad or empty cases.
- Keep side effects easy to find.
- Pass needed data as input.
- Do not read hidden global state when an input will work.
function getDisplayName(user?: User): string {
if (!user) return 'Guest'
if (!user.name.trim()) return 'Guest'
return user.name
}Handle empty lists, missing values, and bad input when they can occur.
Errors
- Check failed work.
- Add useful context.
- Keep the first error as the cause when possible.
- Do not show secrets or private data.
- Do not catch an error only to ignore it.
- Return safe messages to users.
- Log useful details on the server.
async function fetchData(url: string): Promise<unknown> {
try {
const response = await fetch(url)
if (!response.ok) {
throw new Error(`Request failed with status ${response.status}`)
}
return await response.json()
} catch (error) {
throw new Error('Could not fetch data', { cause: error })
}
}If the caller can handle the first error well, let it pass through. Do not wrap every error.
Async Work
Run work at the same time only when each task is independent.
const [users, markets, stats] = await Promise.all([
fetchUsers(),
fetchMarkets(),
fetchStats()
])Run work in order when one task needs the last result.
const user = await createUser(input)
await sendWelcomeEmail(user.email)Also:
- Use
Promise.allSettledwhen one failed task must not stop the rest. - Add a time limit or cancel signal for long network work.
- Limit group size when many tasks could flood a service.
- Do not leave a promise without
await,return, or clear error handling.
React
Components
- Use typed props.
- Keep parts small.
- Give each part one main job.
- Use plain HTML parts when they fit.
- Support keyboard use and clear labels.
interface ButtonProps {
children: React.ReactNode
onClick: () => void
disabled?: boolean
variant?: 'primary' | 'secondary'
}
export function Button({
children,
onClick,
disabled = false,
variant = 'primary'
}: ButtonProps) {
return (
<button
type="button"
onClick={onClick}
disabled={disabled}
className={`btn btn-${variant}`}
>
{children}
</button>
)
}State
Use a function when the new state uses the old state.
const [count, setCount] = useState(0)
setCount(previousCount => previousCount + 1)Do not store a value in state if it can be found from current props or state.
const fullName = `${firstName} ${lastName}`Hooks
- Call hooks only at the top level.
- Do not call hooks inside a loop, test, or nested function.
- List all used values in effect input lists.
- Clean up timers, events, and requests.
export function useDebounce<T>(value: T, delayMs: number): T {
const [debouncedValue, setDebouncedValue] = useState(value)
useEffect(() => {
const timerId = window.setTimeout(() => {
setDebouncedValue(value)
}, delayMs)
return () => window.clearTimeout(timerId)
}, [value, delayMs])
return debouncedValue
}Render States
Show loading, error, empty, and ready states when each can happen.
if (isLoading) return <Spinner />
if (error) return <ErrorMessage error={error} />
if (!data?.length) return <EmptyState />
return <DataList data={data} />For lists:
- Use stable keys from the data.
- Do not use the list index as a key when items can move.
- Do not change props during render.
API Rules
Routes
Use nouns for routes and HTTP methods for actions.
GET /api/markets
GET /api/markets/:id
POST /api/markets
PUT /api/markets/:id
PATCH /api/markets/:id
DELETE /api/markets/:id
GET /api/markets?status=active&limit=10&offset=0Use:
GETto readPOSTto createPUTto replacePATCHto change part of a valueDELETEto remove
Return the right status code. Common codes are 200, 201, 204, 400, 401, 403, 404, 409, and 500.
Response Shape
Use one response shape across the API.
interface ApiResponse<T> {
success: boolean
data?: T
error?: {
code: string
message: string
details?: unknown
}
meta?: {
total: number
page: number
limit: number
}
}Do not send stack traces, secret values, or raw database errors to the client.
Input Checks
Check route values, query values, headers, and body data. Treat all outside data as unsafe.
Also handle bad JSON before schema checks.
import { z } from 'zod'
const CreateMarketSchema = z.object({
name: z.string().trim().min(1).max(200),
description: z.string().trim().min(1).max(2000),
endDate: z.string().datetime(),
categories: z.array(z.string().trim().min(1)).min(1)
})
export async function POST(request: Request) {
let body: unknown
try {
body = await request.json()
} catch {
return Response.json(
{
success: false,
error: {
code: 'BAD_JSON',
message: 'The request body is not valid JSON'
}
},
{ status: 400 }
)
}
const result = CreateMarketSchema.safeParse(body)
if (!result.success) {
return Response.json(
{
success: false,
error: {
code: 'BAD_INPUT',
message: 'The request data is not valid',
details: result.error.flatten()
}
},
{ status: 400 }
)
}
const market = await createMarket(result.data)
return Response.json(
{ success: true, data: market },
{ status: 201 }
)
}Check access on the server. Hiding a button in React is not an access check.
Files
Follow the project layout first. A common layout is:
src/
โโโ app/
โ โโโ api/
โ โโโ markets/
โโโ components/
โ โโโ ui/
โ โโโ forms/
โ โโโ layouts/
โโโ hooks/
โโโ lib/
โ โโโ api/
โ โโโ utils/
โ โโโ constants/
โโโ types/
โโโ styles/Common file names:
components/Button.tsx
hooks/useAuth.ts
lib/formatDate.ts
types/market.tsKeep tests near the code or in the project test folder. Follow the style already in use.
Tests
Add or update tests for changed behavior.
Test:
- The normal case
- Empty or missing input
- Bad input
- Error paths
- Limit values
- Access rules when they apply
Do not test private steps. Test what callers can see.
A bug fix should have a test that fails before the fix and passes after it.
Tools and Checks
Use the tools the project already has.
Before you finish, run the checks that fit the change:
format
lint
type check
tests
buildDo not claim a check passed if you did not run it. If a check cannot run, say why.
Do not fix unrelated warnings unless the user asks.
Concrete Example
Task: Add a search helper for markets.
interface Market {
id: string
name: string
}
export function findMarkets(
markets: readonly Market[],
query: string
): Market[] {
const cleanQuery = query.trim().toLowerCase()
if (!cleanQuery) return []
return markets.filter(market =>
market.name.toLowerCase().includes(cleanQuery)
)
}import { describe, expect, it } from 'vitest'
import { findMarkets } from './findMarkets'
const markets = [
{ id: '1', name: 'City Budget' },
{ id: '2', name: 'Rain Tomorrow' }
]
describe('findMarkets', () => {
it('finds a market without case limits', () => {
expect(findMarkets(markets, 'CITY')).toEqual([markets[0]])
})
it('trims the query', () => {
expect(findMarkets(markets, ' rain ')).toEqual([markets[1]])
})
it('returns an empty list for an empty query', () => {
expect(findMarkets(markets, ' ')).toEqual([])
})
it('does not change the input list', () => {
findMarkets(markets, 'city')
expect(markets).toHaveLength(2)
})
})This example uses clear names, exact types, no shared data change, and tests for edge cases.
Final Review
Before you finish, check that:
- Names are clear.
- Types are exact.
- Outside data is checked.
- Errors are handled once and at the right level.
- Shared data is not changed in place.
- Async work is safe.
- React hooks follow hook rules.
- Loading, error, and empty states are covered.
- Tests cover the changed behavior.
- No secret or private data is shown.
- The code fits the local project style.