Skip to content

Instantly share code, notes, and snippets.

@nicholaswmin
Last active January 15, 2026 19:32
Show Gist options
  • Select an option

  • Save nicholaswmin/3c251f53c8e12d5b150d7a8535553aa5 to your computer and use it in GitHub Desktop.

Select an option

Save nicholaswmin/3c251f53c8e12d5b150d7a8535553aa5 to your computer and use it in GitHub Desktop.
rolling styleguide

es.next

style guide for modern JS
nicholaswmin

targets: node v25+, latest Win/MacOS/iOS Chrome/Safari

Contents

Foundation

Principles for project setup and dependency management.

Prefer ESM; avoid CommonJS

ES modules are the JavaScript standard.

  • Avoid .mjs; prefer .js with "type": "module".
  • Use node:* imports for built-ins.
// ✅ ES modules
import { users } from './data.js'
import data from './data.json' with { type: 'json' }
import config from './config.js'

export const validate = input => check(input)
export const run = input =>
  transform(input, { users, data, config })

// ❌ CommonJS
const { users } = require('./data')
const data = require('./data.json')
const config = require('./config')

const validate = input => check(input)
const run = input =>
  transform(input, { users, data, config })

module.exports = { validate, run }

Always start with a package.json

Minimal publishable package.json:

{
  "name": "@johndoe/foo",
  "version": "1.0.0",
  "description": "Does foo bar & maybe baz",
  "type": "module",
  "exports": "./index.js",
  "files": ["index.js"],
  "engines": { "node": ">=24" },
  "scripts": { "test": "node --test \"**/*.test.js\"" },
  "keywords": ["foo", "bar"],
  "author": "John Doe <johnk@doe.dom>",
  "license": "MIT"
}

Use catch-all imports for nicer imports

Export from src/<name>/index.js.
Import with #<name>.

Minimal setup:

{
  "type": "module",
  "imports": {
    "#*": "./src/*/index.js"
  }
}

Project structure:

foo-bar/
├── package.json
├── index.js
├── test/                      # integration tests
│   └── main.test.js
└── src/
    ├── bar/
    │   ├── index.js
    │   └── test/              # bar unit tests (optional)
    │       └── main.test.js
    └── baz/
        ├── index.js
        └── test/              # baz unit tests (optional)
            └── main.test.js

Import using pretty paths:

// index.js
import { foo } from '#bar'
import { baz } from '#baz'

// work

Avoid needless dependencies

Every dependency is a liability.
Prefer built-ins and writing small utilities.

// ✅ Use built-ins
const unique = [...new Set(items)]
const sleep = ms =>
  new Promise(resolve => setTimeout(resolve, ms))
const pick = (obj, keys) =>
  Object.fromEntries(
    keys
      .filter(k => Object.hasOwn(obj, k))
      .map(k => [k, obj[k]])
  )

However, avoid DIY in production for domains that are:

  • Too complex (game engines, CRDTs, date/time math)
  • Too critical (cryptography, validation)

Avoid invented fluff & fallbacks

Implement only what is necessary.
Do not add code based on speculation about future needs.

Avoid:

  • Fallbacks for cases like xyz that should not happen
  • "Legacy" modes that don't actually exist
  • Ad-hoc options or features added "just in case"
  • Premature optimizations for unmentioned problems

Exception: You have a known, explicit requirement about any of the above.

// ✅ Implements only the known requirement (export as CSV)
const exportData = records => {
  // omitted for brevity...

  return toCsv(records)
}

// ❌ Invented optionality
// Goal was to export CSV.
// Nobody asked for JSON support.
const exportData = (records, { format = 'csv' } = {}) => {
  // omitted for brevity...

  return format === 'json'
    ? JSON.stringify(records)
    : toCsv(records)
}

Code organization

Patterns for structuring functions and managing complexity.

Layer functions by purpose

  • Utility: Generic non-domain helpers
  • Domain: Domain-specific helpers
  • Orchestration: Usually the main of the module or program
// ✅ Utility functions
const gzip = file => compress(file, 'gzip', { lvl: 1 })
const delay = ms =>
  new Promise(resolve => setTimeout(resolve, ms))
const hasExt = ext => file =>
  file.filename.endsWith(`.${ext}`)

// ✅ Domain functions
const upload = (file, n = 0) =>
  s3.upload(file).then(result =>
    result.error
      ? n < 3
        ? upload(file, n + 1)
        : ignore(file)
      : result
  )

// ✅ Orchestration functions
const synchronize = async files => {
  const timedout = delay(30000).then(() => ({ timeout: true }))
  const eligible = files.filter(hasExt('png')).map(gzip)

  const uploaded = Promise.all(eligible.map(upload))
    .then(data => ({ data }))

  const result = await Promise.race([uploaded, timedout])

  return result.timeout
    ? ontimeout(eligible)
    : normalize(result.data)
}

Separate logic from side effects

Prefer pure functions over side effects.
Move side effects to boundaries.
Extract dependencies for easier mocking.

// ✅ Testable - pure function
const discountAmount = (price, pct) => price * (pct / 100)

// ✅ Testable - dependency injection
const notify = (user, emailer) =>
  emailer.send(user.email, 'Welcome!')

// ❌ Hard to test - side effects
const applyDiscount = (price, percentage) => {
  const discount = price * (percentage / 100)

  updateDatabase(discount)
  sendEmail(discount)

  return discount
}

// ❌ Hard to test - hard-coded dependency
const notifyHard = user =>
  EmailService.send(user.email, 'Welcome!')

Extract dependencies when the function:

  • Reaches outside your process (network, filesystem)
  • Depends on uncontrollable factors (timers, sensors)
  • Is slow or awkward to test
  • Needs different implementations in different contexts

However, every indirection adds complexity.
Extract only when the benefit outweighs the cost.

// ✅ Simple operations don't need extraction
const formatName = (first, last) => `${first} ${last}`

// ❌ Needless indirection
const formatNameBad = (first, last, formatter) =>
  formatter.format(first, last)

Skip needless intermediates

Chain or inline unless the intermediate clarifies complex logic.

// ✅ No intermediates needed
const promote = users => users.map(review).filter(passed)

// ❌ Needless intermediates
const promoteBad = users => {
  const reviewed = users.map(review)
  const eligible = reviewed.filter(passed)
  return eligible
}

Some functions naturally orchestrate others.
Do not mangle business logic for brevity.
Do not make it verbose.

Preserve wrapped signatures

Wrappers should accept the same arguments as the function they wrap.
Add behavior by prepending/appending, but forward the rest unchanged.
This keeps call sites stable as upstream APIs evolve.

const prefix = col(['yellow'], 'warn:')

// ✅ signature mirrors console.warn
const warn = (...args) =>
  console.warn(prefix, ...args)

// ❌ brittle wrapper
const warnBad = message =>
  console.warn(prefix, message)

Avoid nesting; use early returns

Prefer expressions for selectors and transforms.
Use early returns only when you need statements.

Early returns flatten procedures and eliminate branches early.
Each bailout removes a level of indentation.
The happy path stays obvious.

Guards are for bailouts in procedures.
For value selection, prefer expressions (?:, ||, &&, ??).

// ❌ selector written as statements (cop-out)
const active = users => {
  if (!users.length)
    return []

  return users.filter(u => u.active())
}

// ✅ expression-bodied selector
const active = users =>
  users.filter(u => u.active())
// ✅ early return in a procedure
function onUpdate(fence) {
  if (this.insulated) return
  if (!this.intersects(fence)) return this.electrocute()
  // ... sync logic
}

// ❌ nested conditions
function onUpdateBad(fence) {
  if (!this.insulated) {
    if (this.intersects(fence)) {
      // ... sync logic
    } else {
      this.electrocute()
    }
  }
}

However, do not use guards defensively.
Fix the caller instead of guarding against bad internal inputs.

Exception: validate external input at boundaries.

Group steps with vertical whitespace

Enhance scannability by splitting sections into:

  • setup
  • execute
  • output
// ✅ Clear visual grouping
const notify = async user => {
  if (user.notified()) 
    return false

  const opts = { retries: 3, timeout: 5000 }

  for (const email of user.emails)
    await notifier.send(email, opts)

  if (user.active)
    user.complete(new Date())
        .unlock()

  return repo.save(user, opts)
}
// ❌ Dense, unstructured
const notify = async user => {
  if (user.notified()) 
    return false
  const opts = { retries: 3, timeout: 5000 }
  for (const email of user.emails)
    await notifier.send(email, opts)
  if (user.active)
    user.markSeen()
  return repo.save(user, opts)
}

Avoid blank lines just to preserve intermediates.
Prefer chaining.

This is generic.
It applies to functions, files, modules, and projects.

Naming

Guidelines for clear, intention-revealing names.

Name concisely and precisely

Name by role, not by type.
Choose single words when precise alternatives exist.

// ✅ Good
users
active
name
overdue

// ❌ Avoid
userList
isUserActive
firstName
lastName
isPaid // when overdue is clearer

Use short, contextual names

Prefer domain-relevant names.
If none exists, pick the clearest.

Avoid single-letter params.
Use domain nouns.
Shorten with scope.

Name by intent.
Drop type/structure noise.

Exception: inline callbacks may use single letters when context is clear.

// ✅ Single-letter OK in concise inline callbacks
tables.filter(t => t.active)
rows.map(r => r.id)
items.forEach(i => send(i))

// ❌ Single-letter in function definitions
const process = (u, r) => u.id === r.ownerId

// ✅ Full names in function definitions
const process = (user, resource) =>
  user.id === resource.ownerId
// ❌ negated passive
invoice.hasNotBeenPaid

// ✅ domain term
invoice.overdue
// ❌ forced domain jargon in a generic util
const chunk = (ledgerEntries, size) => {}

// ✅ clear generic when no domain term fits
const chunk = (items, size) => {}
// ❌ single-letter params in named functions
const editable = (u, r) =>
  u.role === 'admin' || u.id === r.ownerId

// ✅ domain nouns in named functions
const editable = (user, resource) =>
  user.role === 'admin' || user.id === resource.ownerId
// ❌ type/structure noise
const userArray = await repo.list()

// ✅ intent + context
const users = await repo.list()

Use domain verbs; avoid verb+noun names

Prefer a domain verb over a generic verb+noun.
Use singular verbs for actions.
Use plural nouns for collections.

// ❌ generic verb+noun names muddy the domain
student.addGrade(95)
lesson.addStudent(alice)
user.sendNotification(msg)
post.createComment(text)

// ✅ precise domain verbs clarify the domain
student.grade(95)
lesson.enroll(alice)
user.notify(msg)
post.comment(text)

// ✅ escape hatch: generic verbs on generic containers are fine
lesson.students.add(alice)

Drop redundant qualifiers

Assume the surroundings provide the context.

// ✅ Context eliminates redundancy
const user = { name, email, age }
const { data, status } = response

// ❌ Redundant qualifiers
const userBad = { userName, userEmail, userAge }
const { responseData, responseStatus } = response

Name for property shorthand

Name variables to match the object keys they will populate.
This enables concise object literals via property shorthand.
It avoids redundant qualifiers like requestHeaders or payloadBody.

The name should anticipate its use.

// ✅ Name matches the destination property
const headers = { 'X-Custom': 'value' }
const body = JSON.stringify({ id: 1 })

fetch(url, { method: 'POST', headers, body })

// ❌ Poor naming prevents shorthand
const requestHeaders = { 'X-Custom': 'value' }
const payloadBody = JSON.stringify({ id: 1 })

fetch(url, {
  method: 'POST',
  headers: requestHeaders,
  body: payloadBody,
})

Avoid prefix grouping; use objects

If values form a conceptual unit, group them in an object.
Do not encode relationships via naming conventions.

// ✅ object is the concept (a range)
const fps = { start: 1, end: 40 }

// ❌ relationship encoded in prefixes
const startFps = 1
const endFps = 40

// ❌ single value wrapped pointlessly
const fps = { target: 60 }

Use an object when:

  • values are meaningless alone (start/end, x/y, min/max)
  • they will be passed or returned as a unit
  • the grouping has a name (range, point, bounds, config)

Use a flat binding when:

  • the value stands alone
  • no relationship to express

Flat is fine when:

  • values have independent sources or lifetimes
  • hot path where allocation matters
  • React state (update ergonomics)

Use namespacing for structure

Group related properties under logical objects.
Avoid meaningless wrappers or technical groupings.

// ✅ Appropriate namespacing
const user = {
  name: { first, last, full },
  contact: { email, phone },
  status: { active, verified },
}

const config = {
  server: { port, host },
  database: { url, timeout },
}

// ❌ Inappropriate namespacing
const userBad = {
  data: { value: 'John' },
  info: { type: 'admin' },
  props: { id: 123 },
}

Syntax

Conventions for minimal, readable code.

Use minimal semis, parens, and braces

Minimal syntax, maximum signal.
Code should read itself.

// ✅ Minimal syntax
const name = 'Alice'
const double = x => x * 2
const greet = name => `Hello ${name}`

users.map(u => u.name)

if (subscribers.length)
  throw new Error('Session is subscribed')

const user = { name, email, age }

// ❌ Unnecessary syntax
const doubleBad = x => {
  return x * 2
}
const userBad = { name: name, email: email }

No comments, unless functionally necessary

Comments are a maintenance burden.
If your code needs explanation, rewrite the code.

Prefer names and structure.
Do not add remarks.

Exceptions:

  • Comments that affect behavior (JSDoc/TSDoc, linter toggles)
  • Instructional style guides and docs
  • READMEs and templates
// ❌ Explaining bad code with a comment
const x = users.filter(u => u.age > 10) // get active users

// ✅ Explain via naming
const activeUsers = users.filter(u => u.active)

ASI: know the hazards, avoid the tricks.

No semis is fine.
Do not start a statement with these tokens.
If you must, prefix the line with ;:

  • (
  • [
  • / (regex literal)
  • + or -
  • ` (template literal)

Prefer rewriting to avoid these starts.
The guard semicolon is fine.

// ❌ ASI hazard: array literal becomes index access
doThing()
[1, 2, 3].forEach(run)

// ✅ guard with leading semicolon
doThing()
;[1, 2, 3].forEach(run)
// ❌ ASI hazard: IIFE becomes a call of the previous expression
setup()
(() => start())()

// ✅ guard with leading semicolon
setup()
;(() => start())()
// ❌ regex literal can parse as division
const input = read()
/^ok$/i.test(input)

// ✅ guard with leading semicolon
const input = read()
;/^ok$/i.test(input)

Prefer const; use let; never var

Default to const.
Use let only for reassignment.
Never use var.

// ✅ const by default
const users = await repo.list()
const total = items.reduce(sum, 0)

// ✅ let only when it must change
let page = 1
page++

// ❌ var and needless let
var count = 0
let name = 'Alice'

Generally, prefer not declaring variables at all.

Use strict equality

Prefer === and !== to avoid coercion.
Use == null only when you mean nullish.

// ✅ strict equality
if (status === 'ok') return next()

// ✅ intentional nullish check
if (value == null) return defaultValue

// ❌ coercive equality
if (count == '0') return

Keep only tiny controls inline

This rule applies when you are already in statement form.
Selectors and transforms should stay expression-bodied.

Unusually tiny guards (≤25 chars) can stay inline.
Otherwise, break after the condition.

// ✅ Short controls inline
if (!user) return

// ✅ guard broken when it stops being tiny
if (!foobar)
  return barbaz()

// ✅ Ternaries: break when it stops being tiny
const price = member
  ? discount(base)
  : base

// ✅ Over ~40 chars: break
if (authenticated && verified && !suspended)
  redirect('/dashboard')

for (const item of cart.items)
  calculateTotal(item.price, item.quantity, tax)

// ❌ Short controls unnecessarily broken
if (ready)
  start()

// ❌ Long conditions crammed inline
if (authenticated && verified && !suspended) redirect('/dashboard')

Prefer arrows & implicit return

Skip braces and return for single expressions.

// ✅ Implicit return
const double = x => x * 2
const getName = user => user.name
const sum = (a, b) => a + b
const makeUser = (name, age) => ({ name, age })

// ❌ Explicit return for single expressions
const doubleBad = x => {
  return x * 2
}
const getNameBad = user => {
  return user.name
}

Avoid pointless exotic syntax

Choose clarity over cleverness.
Use features when they improve readability.

// ✅ Clear intent
const isEven = n => n % 2 === 0

// ❌ Clever but unclear
const isEvenBad = n => !(n & 1)

Use comma operator only for commit expressions

Comma is allowed as a tight "do the thing, then return the commit" form.
Keep it boring.

Rules:

  • Expression-bodied arrows only
  • Max 2 expressions
  • No nesting
  • No "clever sequencing"
// ✅ allowed: mutate, then return the commit
export const incAge = user =>
  (user.incAge(1), repo.save(user))

// ❌ banned: multi-step cleverness
export const run = x =>
  (step1(x), step2(x), step3(x))

Consider iteration over repetition

Use a loop for easier extension.

// ✅ Dynamic generation
return Object.fromEntries(
  ['get', 'put', 'post', 'patch']
    .map(method => [method, send])
)

// ❌ Manual enumeration
return { get: send, put: send, post: send, patch: send }

Avoid overuse, especially in tests.
Loops need mental parsing.

Keep lines ≤80; wrap by structure

Prefer to break by structure under ~60 chars.
Hard-limit at 80 unless extraordinary (long URLs, external APIs).

Break at natural boundaries (operators, delimiters, chain links).
For chains, use one call per line.
Keep operators at the end.

// ❌ dense, mixed precedence
const ok = () => a() || (b() && c(10) > 5)

// ✅ wrapped by structure; keep group parens
const ok = () =>
  a() ||
  (b() && c(10) > 5)
// ❌ long chain kept inline
api().get().map().filter().reduce() + extra()

// ✅ one call per line; operator at end
api()
  .get()
  .map()
  .filter()
  .reduce() +
  extra()
// ✅ pipeline chaining — one link per line
const normalize = path =>
  path
    .replace(/\/$/, '')
    .toLowerCase()
    .trim()

// ❌ dense chain kept together
const normalizeBad = path =>
  path.replace(/\/$/, '').toLowerCase().trim()

Indentation counts toward the 80-char limit.
Nested code wraps sooner:

// ✅ nested — wrap earlier due to indent
const utils = {
  path: {
    normalize: path =>
      path
        .replace(/\/$/, '')
        .toLowerCase()
  }
}

Functional

Techniques for data transformation pipelines.

Use functional programming for data flows

When converting data, think pipelines, not procedures.

// ✅ Functional for transformations
const users = data
  .filter(active)
  .map(normalize)
  .sort(by.name)

Functional predicates

This section is about call-site clarity.
The goal is predicates that read like prose without creating a DSL.

Use currying to prefill parameters

Currying turns f(a, b) into f(a)(b).
Use it to prefill configuration once.
Reuse the result as a single-argument function in map/filter/reduce.

Use when:

  • you want data-last helpers that plug into array methods directly
  • you reuse the same predicate/transform in multiple places
  • you want pipelines without inline lambdas and wrapper noise

Avoid when:

  • the helper is single-use; inline it
  • it makes the call site harder to read
  • partial application makes argument order ambiguous

If you do a significant amount of currying:

  • consider a generic limited subset first
  • keep domain-specific cases to a minimum
  • avoid intentionally creating mini DSLs
export const missing = key =>
  obj => obj?.[key] == null

export const has = key =>
  obj => obj?.[key] != null

export const is = key =>
  value => obj => obj?.[key] === value

export const not = pred =>
  value => !pred(value)

export const and = (...preds) =>
  value => preds.every(pred => pred(value))

const eligible = and(
  missing('active'),
  is('plan')('pro'),
  has('email')
)

users.filter(eligible)
items.filter(missing('active'))
items.filter(not(has('active')))

Use domain namespaces for predicates

A bare predicate name loses meaning away from the import block.
For domain rules, keep the domain name at the call site.

This avoids namespacing-by-convention like isEligibleUser(...).

Use when:

  • predicates appear far from imports (long modules)
  • multiple domains share similar names (isEligible, isActive)
  • you want a small, cohesive domain vocabulary

Avoid when:

  • the domain module is a dependency hub (I/O, DB, globals)
  • the predicate is purely mechanical (has, missing, is)

Do this:

  • export domain predicates as named exports
  • import the domain as a namespace (import * as User)
  • keep domain predicates pure and dependency-free

In user.js:

export const isEligible = user =>
  !user.active && user.plan === 'pro'

export const canInvite = user =>
  user.role === 'admin' || user.role === 'owner'

At the call site:

import * as User from './user.js'

users
  .filter(User.isEligible)
  .filter(User.canInvite)

If you refuse namespace imports, alias explicitly at the boundary:

import { isEligible as isEligibleUser } from './user.js'

users.filter(isEligibleUser)

Limit util/helper naming to test/throwaway scopes

Utils makes sense in small, non-source scopes.
In tests, prototypes, or tiny helpers it is a frictionless hatch.

When the intent is to express a domain concept,
a generic namespace adds noise.

Avoid:

  • mixing unrelated code under one meaningless name
  • creating a dependency magnet that grows without bounds
  • hiding domain meaning behind generic imports

Exceptions:

  • utils/ folders in test/ to reduce clutter
  • throwaway scripts or tiny apps
  • one-file test suites

Do this:

  • group code by a real concept (domain or mechanic)
  • keep mechanic modules small and orthogonal
  • keep domain vocabulary in domain namespaces
import * as User from './user.js'
import * as pred from './pred.js'
import * as str from './str.js'

users.filter(User.isEligible)
items.filter(pred.missing('active'))
names.map(str.trimLines)

Avoid this:

import { isEligible, trimLines, missing } from './utils.js'

Program flow

Patterns for control flow, sequencing, and conditionals.

Sequencing

Keep control flow top-to-bottom.

Chain or inline unless the intermediate clarifies complex logic.
Skip braces and return for single expressions.
For chains, use one call per line.

Inside-out is hard.
Top-to-bottom is natural.

✅:

export const users = {
  create: raw =>
    Promise.resolve(raw)
      .then(User.validate)
      .then(User.from)
      .then(user => repo.save(user))
}

❌:

export const usersBad = {
  create: raw => repo.save(User.from(User.validate(raw)))
}

Inline by default.
Avoid intermediates.

✅:

export const norm = s => s.trim().toLowerCase()

❌:

export const normBad = s => {
  const x = s.trim().toLowerCase()
  return x
}

Prefer vertical chaining over temporary names.

✅:

export const slug = s =>
  s
    .trim()
    .toLowerCase()
    .replaceAll(' ', '-')

❌:

export const slugBad = s => {
  const a = s.trim()
  const b = a.toLowerCase()
  const c = b.replaceAll(' ', '-')
  return c
}

Allow pure recomputation to avoid braces and bindings.
If it is not hot-path, prefer clarity over performance.

✅:

export const userView = raw => ({
  href: `/u/${raw.name.trim().toLowerCase()}`,
  label: raw.name.trim().toLowerCase()
})

❌:

export const userViewBad = raw => {
  const name = raw.name.trim().toLowerCase()
  return { href: `/u/${name}`, label: name }
}

Keep transforms separate from effects.

If mutation exists, make it explicit and commit immediately.

✅:

export const users = {
  incAge: user => (user.incAge(1), repo.save(user))
}

❌:

export const usersBad = {
  incAge: user => (incrementAge(user, 1), repo.save(user))
}

Prefer functional promise chains over await for straight pipelines.
Use await when you need branching, try/catch, or early bailouts.

Stay immutable where possible

New values are easier to debug than mutations.

// ✅ immutable - data transforms
const add = (items, item) => [...items, item]
const totals = prices.map(p => p * 1.2)
const config = { ...defaults, port: 3000 }

// ❌ mutating
items.push(item)
defaults.port = 3000

Entities with identity get classes.
Data gets transforms.

Chain methods over nesting calls

Inside-out is hard.
Top-to-bottom is natural.

// ✅ Multi-line chaining
const process = data =>
  data
    .filter(x => x > 0)
    .map(x => x * 2)
    .reduce((a, b) => a + b)

// ❌ Nested function calls
const processBad = data =>
  reduce(
    map(
      filter(data, x => x > 0),
      x => x * 2
    ),
    (a, b) => a + b
  )

Conditionals

Prefer expressions over statement blocks.

Selectors and transforms should be expressions.
Use early returns only when you need statements.

Expressions instead of statements

Expressions return values and compose.
Statements do not.

Guideline: collapse statements all the way where possible.

// ❌ statement
const greet = user => {
  if (user.known) {
    return user.welcome()
  } else {
    return user.introduce()
  }
}

// ↓ remove else

const greet = user => {
  if (!user.known) return user.introduce()
  return user.welcome()
}

// ↓ collapse to expression

const greet = user => user.known
  ? user.welcome()
  : user.introduce()

Stop only when further collapsing hurts clarity.

If every branch is a single expression, prefer a ternary.
Return it directly.

// ❌ stopped early out of habit
const roleBad = user => {
  if (user.admin) return 'admin'
  return 'user'
}

// ✅ return the expression directly
const role = user => user.admin ? 'admin' : 'user'

// ✅ simple fallback
const name = user.name || user.nickname

Prefer conditional expressions (?:, ||, &&, ??) over statement blocks.

Booleans → ternary.
Strings → object map.

Avoid cascading negated guards for nested access.
Consolidate into one guard with optional chaining.

// ❌ stacked guards
if (!foo) return
if (!foo.bar) return
if (!foo.bar.baz) return doStuff()

// ✅ consolidated guard
if (!foo?.bar?.baz) return

// ✅ positive guard when action is short
if (foo?.bar?.baz) doStuff()
// ✅ concise arrow returning the ternary
const greet = user =>
  user.admin
    ? user.dashboard()
    : user.known
      ? user.welcome()
      : user.introduce()

// ✅ string key - object map
const handle = action => ({
  create: () => save(data),
  update: () => modify(data),
  delete: () => remove(data),
})[action]?.() ?? cancel()

Sync iteration

Prefer declarative array methods over imperative loops.

Use functional methods over loops

prices.filter(discounted) tells you what.
A for loop makes you figure it out.

// ✅ functional
const valid = items.filter(active)
const names = items.map(pick('name'))
const total = prices.reduce(sum, 0)

// ❌ imperative
const active = []

for (let i = 0; i < items.length; i++)
  if (items[i].active)
    active.push(items[i])

Async iteration

Patterns for async loops and concurrent operations.

Use for...of for sequential side effects

Sequential side effects need ordered execution.

for (const file of files)
  await upload(file)

Use Array.fromAsync for sequential results

Collect results in order without manual accumulation.

const responses = await Array.fromAsync(urls, fetch)

Use Promise.all for concurrent results

Run independent operations in parallel.

const responses = await Promise.all(urls.map(fetch))

Use .allSettled, .race, .any when appropriate

Choose the right combinator for your use case.

// ✅ concurrent, tolerate failures
const results = await Promise.allSettled(tasks)
const passed = results.filter(r => r.status === 'fulfilled')

// ✅ first to settle
const fastest = await Promise.race(requests)

// ✅ first to fulfill (ignores rejections)
const first = await Promise.any(requests)

Object-oriented

When to use classes and how to design them well.

Use OOP for stateful entities

If it has identity and changes over time, make it a class.

Use classes when:

  • there's a who: an entity with identity and lifecycle
  • behavior mutates state intrinsic to something
  • type hierarchy or instanceof checking is needed
  • host environment is already OOP (DOM, streams, web APIs)
  • the domain maps clearly to it (entities, actors, state machines)

Prefer plain objects and functions when:

  • data is just a record with no subject
  • operations are stateless transformations
  • no intrinsic state to protect
// ✅ OOP for entities
class User {
  constructor(name, role) {
    this.name = name
    this.role = role
    this.score = 0
  }

  get eligible() { return this.score > 80 }

  promote() {
    this.role = 'senior'
    return this
  }
}

What to model

  • entities with identity and lifecycle
  • actors that perform actions
  • state machines with guarded transitions
  • wrappers around stateful host APIs

What not to model

  • records, payloads, config objects
  • value types with no behavior
  • bags of utility functions
  • one-off procedures

Use methods to express behavior

If a class exists, its behavior belongs on it.
Do not push behavior into external functions.

// ❌ Anemic
class User {
  constructor(name, email) {
    this.name = name
    this.email = email
  }
}

const validate = user => user.email.includes('@')
const greet = user => `Hello, ${user.name}`

// ✅ Behavior on the class
class User {
  constructor(name, email) {
    this.name = name
    this.email = email
  }

  get valid() { return this.email.includes('@') }
  get greeting() { return `Hello, ${this.name}` }
}

Anemic classes are structs with ceremony.
Move the behavior onto the class or drop the class.

Exception: frameworks that force anemic models (service layers, DI containers).
Work with the constraint, but keep your domain logic isolated.

Use private fields to hide state

Private fields (#field) are the idiomatic way to encapsulate state.

class Counter {
  #count = 0

  increment() { this.#count++ }
  get value() { return this.#count }
}

class Connection {
  #socket
  #retries = 0

  constructor(url) {
    this.#socket = new WebSocket(url)
  }

  get connected() { return this.#socket.readyState === 1 }
}

Use them when:

  • state should not leak to JSON serialization
  • invariants must be enforced (no external mutation)
  • implementation details may change

Avoid when fields must serialize or when the ceremony outweighs the benefit
(simple DTOs, short-lived objects).

Choose method type by data source

Is it this.valid(this.status) or this.valid?
Is it calculate(amount) or this.calculated?

Static methods for pure operations

Static methods declare operations as stateless and context-free.

const DAY_MS = 86_400_000

class Invoice {
  static overdue(date) {
    return Date.now() > date + 30 * DAY_MS
  }

  static taxable(region) {
    return !['DE', 'TX'].includes(region)
  }

  static fee(amount, rate) {
    return amount * rate
  }
}

// Usage
Invoice.overdue(invoice.dueDate)
invoices.filter(i => Invoice.overdue(i.dueDate))

Use for validation, calculation, and transformation logic that applies across
all instances.

Getters for computed properties

Getters provide property semantics for derived state.

const DAY_MS = 86_400_000

class Invoice {
  get overdue() {
    return Date.now() > this.dueDate + 30 * DAY_MS
  }

  get payable() {
    return this.status === 'pending' && !this.disputed
  }

  get total() {
    return this.subtotal + this.tax - this.discount
  }
}

// Usage
return invoice.overdue ? sendReminder() : null
const amount = invoice.payable ? invoice.total : 0

Name them as states or conditions, not actions.
Prefer precise domain terms over compound names:

// ❌ Verbose
get isActive()    { return this.status === 'active' }
get hasPositive() { return this.balance > 0 }
get isOverLimit() { return this.usage > this.limit }

// ✅ Domain terms
get active()   { return this.status === 'active' }
get funded()   { return this.balance > 0 }
get exceeded() { return this.usage > this.limit }

Instance methods for external interactions

Instance methods mediate object collaboration.
They make coupling explicit through parameters.

class User {
  manages(employee) {
    return this.reports.includes(employee.id)
  }

  authored(document) {
    return document.author.id === this.id
  }

  transfer(amount, recipient) {
    this.balance -= amount
    recipient.balance += amount
  }

  granted(permission) {
    return this.permissions.includes(permission)
  }
}
class Document {
  editable(user) {
    return this.draft
      ? this.author.id === user.id
      : user.admin
  }

  get locked() {
    return this.archived || this.published
  }
}

Never pass own properties

Passing this.property to your own methods breaks encapsulation.

// ❌
class Order {
  validate(status) {
    return status === 'confirmed'
  }

  process() {
    if (this.validate(this.status)) this.ship()
  }
}

// ✅
class Order {
  get confirmed() {
    return this.status === 'confirmed'
  }

  process() {
    return this.confirmed ? this.ship() : null
  }
}

That logic belongs in a getter or should be static.

Static factories for semantic construction

Static factories create instances with named construction paths.

class Transaction {
  static deposit(account, amount) {
    return new Transaction({
      type: 'deposit',
      account: account.id,
      amount
    })
  }

  static between(sender, receiver, amount) {
    return new Transaction({
      from: sender.id,
      to: receiver.id,
      amount
    })
  }
}

Clearer than multi-shape constructors.

Design for marshalling

JSON.parse(JSON.stringify(instance)) loses prototype, methods, and private
state.

Object-parameter constructors

Wire payloads are objects.
Positional constructors break revival.

// ❌ Positional
class Engine {
  constructor(hp) {
    this.hp = hp
  }
}

// ✅ Object parameter with defaults
class Engine {
  constructor({ hp, rpm } = {}) {
    this.hp = hp
    this.rpm = rpm
  }
}

Adding fields later does not break call sites.

Guard nested revival

Reviver runs bottom-up.
Nested objects may already be instances.

// ❌ Always re-wraps revived children
class Car {
  constructor({ engine } = {}) {
    this.engine = new Engine(engine)
  }
}

// ✅ Accept instances or plain objects
class Car {
  constructor({ engine } = {}) {
    this.engine =
      engine instanceof Engine ? engine
        : engine ? new Engine(engine)
        : null
  }
}

Allowlisted serialization

Use explicit type keys and field lists.
constructor.name is unstable across bundlers.

const pick = (obj, keys) =>
  Object.fromEntries(
    keys
      .filter(k => Object.hasOwn(obj, k))
      .map(k => [k, obj[k]])
  )

class Serializable {
  static #reg = new Map()

  static register(Ctor) {
    if (!Ctor?.type) throw new TypeError('type required')
    Serializable.#reg.set(Ctor.type, Ctor)
  }

  toJSON() {
    const { type, fields } = this.constructor
    return { __type: type, ...pick(this, fields) }
  }

  static fromJSON({ __type, ...data }) {
    return new this(data)
  }

  static reviver = (k, v) => {
    if (!v || typeof v !== 'object' || !('__type' in v)) return v
    const Ctor = Serializable.#reg.get(v.__type)
    return Ctor ? Ctor.fromJSON(v) : v
  }
}

class Engine extends Serializable {
  static type = 'Engine@1'
  static fields = ['hp']

  constructor({ hp } = {}) {
    super()
    this.hp = hp
  }
}

Serializable.register(Engine)

Unknown types stay plain objects.
Version the type string for schema evolution.

Caution

Reviving from untrusted JSON is input validation.
Always validate the __type against an allowlist before instantiation.
Never use eval, new Function, or dynamic property access on
untrusted type strings.

Avoid cycles and non-serializable fields

JSON cannot represent cycles.
Handles and resources do not survive transport.

Serialize identifiers instead:

// ❌ Cycle-prone
class Node extends Serializable {
  static fields = ['next']  // Reference to another Node
}

// ✅ ID reference
class Node extends Serializable {
  static type = 'Node@1'
  static fields = ['id', 'nextId']

  constructor({ id, nextId } = {}) {
    super()
    this.id = id
    this.nextId = nextId
  }
}

Open/Closed Principle

Open/Closed Principle (OCP) keeps the main workflow stable.
New cases are added by extending small units, not editing the runner.

Use when:

  • new cases get added regularly
  • changing the workflow risks regressions
  • adding a case should not modify existing cases
  • the need for extensibility is explicit, not speculative

Define the extension seam

Find the part that keeps changing.
Turn that variation into one seam where new behavior plugs in.

Do this:

  • keep the workflow focused on ordering and composition
  • move branching logic into replaceable extensions
  • keep the seam small enough to test in isolation elsewhere

Specify the rule contract

A contract is more than "has the right method name".
It also defines valid inputs, output invariants, allowed side effects,
and error behavior for valid inputs.

Contract should include:

  • accepted inputs and required fields
  • returned type and invariants
  • allowed side effects
  • error behavior on valid inputs

Extend via base class

The runner calls a base API.
You extend behavior by adding a new subclass.

Use when:

  • variants share defaults, state, or lifecycle helpers
  • you want nominal checks (instanceof Rule)
export class Rule {
  apply(state, context) {
    throw new Error('Rule.apply must be implemented')
  }
}

export class Engine {
  #rules

  constructor(rules = []) {
    this.#rules = [...rules]
  }

  with(rule) {
    return new Engine([...this.#rules, rule])
  }

  run(context) {
    return this.#rules.reduce(
      (state, rule) => rule.apply(state, context),
      context.initial
    )
  }
}

export class Multiply extends Rule {
  #factor

  constructor(factor) {
    super()
    this.#factor = factor
  }

  apply(state, context) {
    return state * this.#factor
  }
}

export class Add extends Rule {
  #amount

  constructor(amount) {
    super()
    this.#amount = amount
  }

  apply(state, context) {
    return state + this.#amount
  }
}
const result = new Engine()
  .with(new Multiply(2))
  .with(new Add(5))
  .run({ initial: 10 })

result

Extend via duck typing

If something matches the contract, treat it like a rule.
The runner does not care about inheritance.

Use when:

  • inheritance is overkill but you still want OCP
  • extensions are small and easiest as factories
export class Engine {
  #rules

  constructor(rules = []) {
    this.#rules = [...rules]
  }

  with(rule) {
    return new Engine([...this.#rules, rule])
  }

  run(context) {
    return this.#rules.reduce(
      (state, rule) => rule.apply(state, context),
      context.initial
    )
  }
}

export const multiply = factor => ({
  apply: (state, context) => state * factor,
})

export const add = amount => ({
  apply: (state, context) => state + amount,
})
const result = new Engine()
  .with(multiply(2))
  .with(add(5))
  .run({ initial: 10 })

result

Extend via composition

If extensions do not need identity or lifecycle, functions are a clean seam.
Add behavior by appending a function.

Use when:

  • each extension is a pure transform of a value or state
  • you want minimal ceremony and direct unit tests elsewhere
export const run = (context, rules = []) =>
  rules.reduce(
    (state, rule) => rule(state, context),
    context.initial
  )

export const multiply = factor =>
  (state, context) => state * factor

export const add = amount =>
  (state, context) => state + amount
const result = run(
  { initial: 10 },
  [multiply(2), add(5)]
)

result

Extend via type registry

This is the "data drives behavior" seam.
You register handlers once, and external data chooses which runs.

Use when:

  • behavior is selected from external data ({ type, ...params })
  • you need plugins without editing the core runner
export const createEngine = () => {
  const rules = new Map()

  const use = (type, apply) => {
    if (rules.has(type))
      throw new Error(`Rule already registered: ${type}`)

    rules.set(type, apply)
    return api
  }

  const run = (context, specs = []) =>
    specs.reduce((state, spec) => {
      if (!spec?.type)
        throw new Error('Rule spec.type required')

      const apply = rules.get(spec.type)
      if (!apply)
        throw new Error(`Unknown rule: ${spec.type}`)

      return apply(state, context, spec)
    }, context.initial)

  const api = { use, run }

  return api
}
import { createEngine } from './engine.js'

const engine = createEngine()
  .use('multiply', (state, context, spec) => state * spec.factor)
  .use('add', (state, context, spec) => state + spec.amount)

const result = engine.run(
  { initial: 10 },
  [
    { type: 'multiply', factor: 2 },
    { type: 'add', amount: 5 },
  ]
)

result

Liskov Substitution Principle

Liskov Substitution Principle (LSP) keeps polymorphism honest.
If code works with the base contract, it must keep working with every variant
without special cases.

Use when:

  • a workflow consumes rules behind one contract (apply, area, etc.)
  • the workflow relies on invariants about inputs, outputs, and errors

Define behavioral contracts

Signatures are not enough.
Callers also depend on behavior.

Make explicit:

  • invariants the workflow relies on
  • which errors are part of normal operation
  • what side effects are allowed at the seam

Avoid stronger preconditions

A rule breaks substitutability when it demands extra requirements
the contract did not promise.

Avoid:

  • requiring extra context fields not required by the contract
  • rejecting values the contract accepts as valid

Avoid weaker postconditions

A rule breaks substitutability when it returns something the workflow does
not expect.

Avoid:

  • returning a different shape sometimes
  • breaking output invariants downstream code assumes

Keep side effects within the contract

Side effects are not always bad.
They must be predictable.

Avoid:

  • hidden network or filesystem work
  • hidden mutation of shared state

Recognize invariant clashes

Inheritance is where LSP failures show up loudly.
If the base API cannot express a subtype invariant safely, the subtype ends
up fighting the API.

Example:

export class Ellipse {
  constructor(a, b) {
    this.a = a
    this.b = b
  }

  setA(a) {
    this.a = a
    return this
  }

  setB(b) {
    this.b = b
    return this
  }
}

export class Circle extends Ellipse {
  constructor(r) {
    super(r, r)
  }

  setA(r) {
    this.a = r
    this.b = r
    return this
  }

  setB(r) {
    this.a = r
    this.b = r
    return this
  }
}

export const stretchX = (shape, a) =>
  shape.setA(a)

Callers that rely on setA changing only a will be surprised by Circle.

Enforce substitutability

Treat the seam contract as enforceable, not aspirational.
Use one shared expectation set that every rule must satisfy.

Error handling

Strategies for errors, validation, and input normalization.

Avoid defensive programming

Trust internal invariants.
If you cannot, write tests.
Do not litter internal code with defensive guards.

Guards are fine for valid bailouts in procedures.
Do not use guards to hide broken call sites.

Exception: validate external data at boundaries.

// ✅ Trust internal functions
const process = data => data.filter(active).map(normalize)

// ❌ Defensive programming within internal functions
const processBad = data => {
  if (!data || !Array.isArray(data)) return []
  return data
    .filter(i => i && active(i))
    .map(i => (i ? normalize(i) : null))
    .filter(Boolean)
}

// ❌ Defensive early returns
const formatNameBad = (first, last) => {
  if (!first) return ''
  if (!last) return ''
  return `${first} ${last}`
}

// ✅ Trust the caller
const formatName = (first, last) => `${first} ${last}`

Use appropriate error types

Be specific:

  • wrong type: TypeError
  • out of bounds: RangeError
  • everything else: Error with a clear message
// ✅ Specific error types
if (typeof value !== 'number')
  throw new TypeError(
    `Expected number, got ${typeof value}`
  )

if (value < 0 || value > 100)
  throw new RangeError('Value must be between 0 and 100')

if (!process.env.API_KEY) throw new Error('API_KEY missing')

// ❌ String errors
throw 'Invalid value'

// ❌ Generic errors
throw new Error('Invalid input')

Normalize & validate external input

Users add spaces.
Environment variables arrive as strings.

Clean everything at the borders.

// ✅ Environment variables
const port = parseInt(process.env.PORT || '3000', 10)
const env = (process.env.NODE_ENV || 'development')
  .toLowerCase()

// ✅ Booleans
const enabled = /^(true|1|yes|on)$/i.test(
  process.env.ENABLED || ''
)

// ✅ Arrays from CSV
const hosts = (process.env.HOSTS || '')
  .split(',')
  .map(s => s.trim())
  .filter(Boolean)

// ✅ User input
const email = (input || '').trim().toLowerCase()

Validation:

// ✅ Validate at boundaries
const handleRequest = req => {
  if (!req.body?.userId)
    throw new Error('userId required')

  if (typeof req.body.userId !== 'string')
    throw new TypeError('userId must be string')

  return processUser(req.body.userId)
}

// ✅ Early validation
const createUser = data => {
  if (!data.name) throw new Error('Name required')
  if (!data.email) throw new Error('Email required')
  return save(normalize(data))
}

Testing

Guidelines for the Node.js built-in test runner.

Use built-in test runner

Use node --test.
Zero dependencies.

// ✅ Built-in test runner
// Run with: node --test "**/*.test.js"
import { test } from 'node:test'

Use global hooks via --import

Preload shared setup with --import for hooks that run across all test files.
Place global setup in test/utils/setup.js.

Note: --import preloads modules for side effects.
Use --test-global-setup for explicit setup/teardown exports.

// test/utils/setup.js
import { before, after } from 'node:test'

before(async () => {
  // start server, connect db, etc.
})

after(async () => {
  // cleanup resources
})
node --test --import ./test/utils/setup.js "**/*.test.js"

Or use --test-global-setup for explicit setup/teardown exports:

// test/utils/setup.js
export const globalSetup = async () => {
  // runs once before all tests
}

export const globalTeardown = async () => {
  // runs once after all tests
}
node --test --test-global-setup=./test/utils/setup.js "**/*.test.js"

Setup errors abort the run.
Teardown is skipped if setup fails.

Propagate errors and set timeouts

Hanging tests are worse than failing tests.

Important

Propagate unhandled errors so failures surface immediately.
Set sensible timeouts to prevent indefinite hangs.
Use timeout <secs> <cmd> when running tests in CI.

// test/utils/setup.js
import { before, after } from 'node:test'

process.on('unhandledRejection', err => {
  console.error('Unhandled rejection:', err)
  process.exit(1)
})

process.on('uncaughtException', err => {
  console.error('Uncaught exception:', err)
  process.exit(1)
})
node --test --test-timeout=5000 "**/*.test.js"
  • --test-timeout=<ms> sets per-test timeout (default: Infinity)
  • exit on unhandled errors; never swallow silently
  • prefer short timeouts; long waits hide real problems

Rely on stable test discovery

Keep the test command stable as the tree grows.
Use one glob that includes co-located tests but excludes helpers.

Do this:

node --test "**/*.test.js"

Avoid narrow paths that miss co-located tests:

node --test test/unit/*.test.js

package.json examples:

test: node --test "**/*.test.js"
test: node --test --import test/utils/setup.js "**/*.test.js"

Structure tests hierarchically

Tests tell a story: component → scenario → expectation.
Nest them that way.

// ✅ Hierarchical structure
test('#withdraw', async t => {
  await t.test('amount within balance', async t => {
    await t.test('disperses requested amount', async t => {
      // assertion with await
    })
  })

  await t.test('amount exceeds balance', async t => {
    await t.test('throws appropriate error', async t => {
      // assertion with await
    })
  })
})

Write focused tests

One test, one assertion, one failure reason.

// ✅ Granular tests
test('#withdraw', async t => {
  t.beforeEach(t => (t.atm = new ATM(100)))

  await t.test('returns updated balance', async t => {
    t.assert.strictEqual(await t.atm.withdraw(30), 70)
  })
})

Attach fixtures to test context

Attach test data directly to the context using
t.beforeEach(t => t.atm = new ATM(100)).

Avoid external variables or imports for fixtures.

// ✅ Context fixtures
test('#withdraw', async t => {
  t.beforeEach(t => (t.atm = new ATM(100)))

  await t.test('disperses amount', async t => {
    await t.atm.withdraw(30)
  })
})

Use context for test utilities

Everything is on t: t.assert, t.mock, t.beforeEach.
t.mock resets automatically between tests.

// ✅ Context utilities
test('#notify', async t => {
  await t.test('sends email', t => {
    const emailer = t.mock.fn()
    notify(user, { send: emailer })

    t.assert.strictEqual(emailer.mock.callCount(), 1)
  })
})

Assert the minimum required

Do not test what you do not care about.
Avoid asserting entire objects that will grow.

// ✅ Partial assertions
test('#user properties', async t => {
  t.beforeEach(t => {
    t.user = { name: 'Alice', role: 'admin', extra: 'ignore' }
  })

  await t.test('has correct details', t => {
    t.assert.partialDeepStrictEqual(t.user, {
      name: 'Alice',
      role: 'admin',
    })
  })
})

Test for keywords, not sentences

Match keywords that survive rephrasing.

Pick keywords unique to the test:

  • /insufficient/ - specific to this error
  • /balance/ or /invalid/ - too generic
// ✅ Flexible matching
test('#withdraw', async t => {
  t.beforeEach(t => (t.atm = new ATM(100)))

  await t.test('exceeds balance', async t => {
    await t.assert.rejects(() => t.atm.withdraw(150), {
      name: 'Error',
      message: /insufficient/i,
    })
  })

  await t.test('preserves balance', async t => {
    await t.atm.withdraw(150).catch(() => null)
    t.assert.strictEqual(t.atm.balance, 100)
  })
})

Complete test example:

test('#atm', async t => {
  t.beforeEach(t => (t.atm = new ATM(100)))

  await t.test('#withdraw', async t => {
    await t.test('within balance', async t => {
      await t.test('returns updated balance', async t => {
        t.assert.strictEqual(await t.atm.withdraw(30), 70)
      })
    })

    await t.test('exceeds balance', async t => {
      await t.test('throws error', async t => {
        await t.assert.rejects(() => t.atm.withdraw(150), {
          name: 'Error',
          message: /insufficient/i,
        })
      })

      await t.test('preserves balance', async t => {
        await t.atm.withdraw(150).catch(() => null)
        t.assert.strictEqual(t.atm.balance, 100)
      })
    })
  })

  await t.test('#properties', async t => {
    await t.test('has location and currency', t => {
      t.assert.partialDeepStrictEqual(t.atm, {
        location: 'Main Street',
        currency: 'USD',
      })
    })
  })
})

Documentation

technical documentation guidelines

Philosophy

  • scannable at a glance
  • get to the point fast
  • strip noise and repetition

Break new sentences into newlines

Start each sentence on its own line for cleaner diffs.
End each prose line with two spaces to force a hard line break.

Lorem ipsum dolor sit amet.  
Consectetur adipiscing elit.  
Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.  

Checklist

Validation rules for all technical documentation.

  • Title: Single # heading naming the scope
  • Headers:
    • Shape: 1–10 words
    • Style: verbs (Use/Never/Prefer/Avoid) or nouns for reference sections
    • Patterns: [Verb] [Subject], X instead of Y, If X, do Y
    • Spacing:
      • One empty line before and after each header
      • Never place a header directly after another header
      • Always have at least one non-header line between headers
  • Section openings:
    • Prose sections need ≥1 context line after the header
    • Reference sections with obvious content are exempt
  • Sentences: ≤20 words; active voice; neutral tone; no filler
  • Lists:
    • Bullets for enumeration
    • Numbered lists for stepped workflows
  • Tables: Columns must be aligned
  • Pseudo-headers:
    • **Label:** format
    • One empty line before and after
    • Emoji allowed for patterns (❌/✅)
    • Use **❌:** and **✅:** only in style guides
  • Line wrap:
    • Soft: 80 chars
    • Hard: 90 chars
    • End wrapped prose lines with two spaces
    • Never rely on editor soft wrap
    • Break at spaces or punctuation
    • Never split words or text inside parens or code
  • Links: Reference-style only; URLs at end
  • Code:
    • Language tags on fenced blocks
    • Use backticks inline for code, commands, paths, and filenames

Prefer to break into newline

Break at semantic boundaries (punctuation, clauses, logical units).
Do not break at arbitrary character counts.

❌:

The `--import` flag loads global test hooks, but you still need to set
timeouts and propagate errors.  

✅:

The `--import` flag loads global test hooks,  
but you still need to set timeouts and propagate errors.  

Template

README structure for Node.js packages.

# [Project Name]

[![test][testb]][test]

[1-3 word description, e.g., "web server"]

```bash
npm i author/repo
```

## [Verb] [subject]

[Context line without repeating header]

**[Label]:**

```js
// code example
```

## [Verb] [subject]

[Context line without repeating header]

1. [First step]
2. [Second step]
   1. [Nested substep]
   2. [Nested substep]
3. [Third step]

```js
// code example for workflow
```

```js
// concise example
```

## Run tests

```bash
npm test
```

## License

[MIT][li-spdx]

[testb]: https://github.com/user/repo/actions/workflows/test.yml/badge.svg
[test]: https://github.com/user/repo/actions/workflows/test.yml
[li-spdx]: https://opensource.org/licenses/MIT
[link-ref]: https://example.com

Style guide template

Pattern-based guides with examples.

# [Style Guide Name]

[Short description of this guide]

## [Category]

[Context about this category]

### [Topic]

[One-line principle.]

**❌: [Anti-Pattern]**

[Why it's bad.]

```[lang]
// bad example
```

**✅: [Pattern]**

[When to use.]

```[lang]
// good example
```

**✅: [Pattern]**

[When to use.]

```[lang]
// good example
```

Warning

Author constraint: Never invent or list features or requirements
unless explicitly requested in the prompt or spec.

HTML/Markup

Minimal HTML/CSS for simple pages.

Principles

Principles for small pages.
Prefer modern, native, classless, accessible defaults.

  • Semantic: Use main, headings, focus states, and good contrast.
  • Classless: Use element selectors; add classes only when needed.
  • Fluid: Use 80ch, rem, and a ratio scale via pow().
  • Native: Use color-scheme, light-dark(), and system fonts.
  • Modern: Use nesting, pow(), text-wrap, and light-dark().

Headings

Use headings for structure, not styling.

Rules:

  • one h1 per page
  • do not skip levels (h2h3, not h2h4)
  • headings should label sections, not decorate layouts
  • keep heading text short and concrete

Boilerplate

A minimal, classless HTML template with modern CSS.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta name="description" content="dolor sit amet.">
  <title>Lorem Ipsum</title>
  <link rel="icon" href="https://fav.farm/🪜"/>
  

  <style>
  body {
    background:  var(--bg);
    color:       var(--text);
    font-family: var(--font-prose);
    font-size:   0.9rem;
    line-height: 1.6;
  
    a {
      color: var(--accent);
      &:hover { color: var(--tertiary); }
      &:focus-visible {
        outline:        2px solid var(--accent);
        outline-offset: 2px;
      }
    }
  
    main { max-width: 80ch; margin: 2rem auto; padding: 0 1rem; }
  
    h1, h2, h3, h4, h5, h6 {
      font-family:   var(--font-ui);
      font-weight:   500;
      line-height:   1.2;
      margin-bottom: .5em;
      text-wrap:     balance;
    }
  
    p { text-wrap: pretty; }
  
    h1 { font-size: calc(1rem * pow(var(--ratio), 3)); margin-top: 2em;     }
    h2 { font-size: calc(1rem * pow(var(--ratio), 2)); margin-top: 1.75em;  }
    h3 { font-size: calc(1rem * var(--ratio));         margin-top: 1.5em;   }
    h4, h5, h6 { font-size: 1rem;                      margin-top: 1em;     }
  
    h1:first-child { margin-top: 0; }
  }
  </style>
</head>

<body>
  <main>
    <h1>Less, but better</h1>
    <p>
      Good design is <a href="#">as little design as possible</a>. Less, but
      better—because it concentrates on the essential aspects, and the products
      are not burdened with non-essentials.
    </p>

    <p>
      Minimalism isn't the absence of detail—it's the absence of the unnecessary.
      What remains must be considered exhaustively. Every margin, every weight,
      every pixel earns its place or gets cut.
    </p>
    
    <blockquote>
      <p>Good design is thorough down to the last detail.</p>
      <cite><a href="https://www.vitsoe.com/gb/about/dieter-rams">Dieter Rams</a></cite>
    </blockquote>
    
    <h2>Clarity</h2>
    <p>
      Typography exists to honor content. A well-set page invites the reader in
      and makes the act of reading effortless. The best interface is one you
      don't notice.
    </p>
    
    <h3>Hierarchy</h3>
    <p>
      Scale creates rhythm. Consistent ratios between heading levels guide the
      eye naturally through content, establishing importance without shouting.
    </p>
  </main>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment