Skip to content

Instantly share code, notes, and snippets.

@nicholaswmin
Last active November 6, 2025 01:42
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.
styleguide for concise ES6+

conscise ES6+

incl. guidelines for unit-testing & docs

author: nicholaswmin, MIT license

An example structure

// A queue with concurrency limit

class Queue extends Array {
  enqueue = (...values) => (this.push(...values), this)
  
  dequeue = () => this.shift()
  
  peek = () => this.at(0)
  
  race = async (fn, { max = 3 } = {}) => {
    const exec = async (inactive, inflight = [], finished = []) => {
      if (!inactive.length && !inflight.length)
        return finished

      const dispatch = elem => fn(elem).then(result => ({ result }))
      const required = Math.min(max - inflight.length, inactive.length)
      const starting = inactive.slice(0, required).map(dispatch)

      const tracking = [ ...inflight, ...starting ],
            resolved = await Promise.race(tracking)

      return exec(
        inactive.slice(required),
        tracking.filter(p => p !== resolved),
        [...finished, resolved.result]
      )
    }

    return new Queue(...await exec([...this]))
  }
}

Contents:

Code organization:

Naming:

Syntax:

Paradigms:

Functional:

Object-oriented:

Error handling:

Testing:

Documentation:

foundation

Structural wrapping (≤80, no else)

Hard-wrap at 80. Width is a ceiling, not a target. Wrap early for structure—even if the line is 79.

  • No else blocks. Use guards, locals, or nested ternaries.
  • Operators at EOL. Break after ||, &&, +, -, etc.
  • Chains vertical. One call per line with a leading dot.
  • Ternaries by structure. Split when nested; keep simple inline.
// ✅ guard without else
if (!ready) return
start()

// ❌ fits under 80 but dense
const ok = () => foobarbaz() || foofoofoo() && barbarbar(10) > 5

// ✅ wrap by structure; make precedence explicit
const ok = () =>
  foobarbaz() ||
  (foofoofoo() &&
    barbarbar(10) > 5)

// ❌ nested ternary crammed on one line
const level = u => u.admin ? 'admin' : u.mod ? 'mod' : 'user'

// ✅ split nested ternary
const level = u =>
  u.admin
    ? 'admin'
    : u.mod
      ? 'mod'
      : 'user'

// ❌ long chain kept inline because "it fits"
foo().bar().baz().qux() + extra()

// ✅ verticalize chains; operator at end
foo()
  .bar()
  .baz()
  .qux() + extra()

Prefer ESM, avoid CommonJS

ES modules are the JavaScript standard. CommonJS is Node's legacy.

// ✅ ES modules
import { readFile } from 'fs/promises'
import { users, products } from './data.js'
import config from './config.js'

export const process = data => transform(data)
export const validate = input => check(input)
export default { process, validate }

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

module.exports = { process, validate }

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) =>
  keys.reduce((acc, k) => ({ ...acc, [k]: obj[k] }), {})

// ❌ Don't install a package for one-liners
import uniq from 'lodash.uniq'  // 4.4KB for [...new Set()]
import delay from 'delay'        // 2.8KB for Promise + setTimeout

However: avoid DIY in production for domains that are:

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

code organization

Layer functions by purpose

  • Utility: Generic non-domain helpers
  • Domain: Domain-specific helpers
  • Orchestration: Usually the main of the module, program etc
// ✅ Utility functions
const gzip = file => compress(file, 'gzip', { lvl: 1 })
const type = file => ext => file.filename.includes(ext)

// ✅ Domain functions
const upload = async (file, n = 0) => {
  const result = await s3.upload(file)

  return result.error
    ? n <= 3
      ? upload(file, ++n)
      : ignore(file)
    : result
}

// ✅ Orchestration functions
const synchronize = async files => {
  const timedout = delay(30000)
  const eligible = files.filter(type('png')).map(gzip)

  const uploaded = await Promise.all(eligible.map(upload))
  const response = await Promise.race([uploaded, timedout])

  return response === timedout
    ? ontimeout(eligible)
    : normalize(response)
}

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 discount = (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 notify = (user) => EmailService.send(user.email, 'Welcome!')

Extract dependencies when the function:

  • Reaches outside your process space, e.g: network, filesystem
  • Depends on uncontrollable factors (timers, accelerometers)
  • Is slow or generally 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 formatName = (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 promote = users => {
  const reviewed = users.map(review)
  const eligible = reviewed.filter(passed)

  return eligible
}

However, some functions naturally orchestrate others. Don't mangle business logic for brevity—but don't make it intentionally verbose either.

Avoid nesting; use early returns

Early returns flatten code and eliminate edge cases upfront. Each guard clause removes a level of indentation, making the happy path obvious.

  • Prefer guard returns; avoid else blocks where possible.
// ✅ Early return
onUpdate(fence) {
  if (this.insulated)
    return

  if (!this.intersects(fence))
    return this.electrocute()

  // ... fancy sync logic
}

// ❌ Nested conditions
onUpdate(fence) {
  if (!this.insulated) {
    if (this.intersects(fence)) {
      // ... fancy sync logic
    } else {
      this.electrocute()
    }
  }
}

However, don't use them defensively.
Fix the caller instead of guarding against bad inputs.

Separate function steps with blank lines

Group guards, declarations, and logic into distinct visual blocks.
One blank line between groups makes structure scannable.

// ✅ Good: clear visual grouping
const processUser = user => {
  if (user.id === 10) return null

  const retries = 3
  const timeout = 5000

  for (const email of getUserEmails(user))
    sendNotification(email)

  if (isActive(user))
    updateLastSeen(user)

  return fetchProfile(user)
}
// ❌ Bad: no visual separation
const processUser = user => {
  if (user.id === 10) return null
  const retries = 3
  const timeout = 5000
  for (const email of getUserEmails(user))
    sendNotification(email)
  if (isActive(user))
    updateLastSeen(user)
  return fetchProfile(user)
}

Pattern:

guards → declarations → logic → return, each separated by blank line.

However, avoid blank lines to preserve intermediates; prefer chaining:

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

// ❌ Avoid forcing intermediates just for spacing
const promote = users => {
  const reviewed = users.map(review)

  const eligible = reviewed.filter(passed)

  return eligible
}

naming

Name concisely and precisely

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

// ✅ Good
users
active
name
overdue   // more precise than isPaid

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

Drop redundant qualifiers

Assume the surroundings provide the context.

// ✅ Context eliminates redundancy
const user = { name, email, age }
const { data, status } = response
// ❌ Redundant qualifiers
const user = { userName, userEmail, userAge }
const { responseData, responseStatus } = response

Use short, contextual names

  • Strongly prefer domain-relevant names; if none exist, don't force jargon—pick the clearest generic name.
  • Avoid single-letter params; use domain nouns; shorten with scope.
  • Name by intent; drop type/structure noise.
  • Exit condition: if a domain term would be obscure or disputed, use a clear generic name now; rename when a stable term emerges.
// ❌ 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, vague predicate
const canEdit = (u, r) =>
  u.role === 'admin' || u.id === r.ownerId

// ✅ domain nouns, adjective predicate
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 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 user = {
  data: { value: 'John' },  // Meaningless wrapper
  info: { type: 'admin' },  // Generic grouping
  props: { id: 123 }        // Technical grouping
}

syntax

Avoid needless semis, parens or braces

Minimal syntax, maximum signal.

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

users.map(user => user.name)

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

const user = { name, email, age }

// ❌ Unnecessary syntax
const name = 'Alice';
const double = x => { return x * 2; }
users.map((user) => user.name);
const user = { 'name': name, 'email': email };

Keep short controls inline

Brief control structures stay readable on one line; longer ones break to preserve scannability and enable line-by-line review.

// ✅ Good: under ~40 chars, keep inline
if (authenticated) redirect('/dashboard')
if (!user) return

// Ternaries when simple
const price = member ? applyDiscount(base) : base

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

for (const item of cart.items)
  calculateTotal(item.price, item.quantity, tax)
// ❌ Bad: long conditions crammed on one line
if (authenticated && verified && !suspended) redirect('/dashboard')
for (const item of cart.items) calculateTotal(item.price, item.quantity, tax)

// Simple enough for one line but unnecessarily broken
if (ready)
  start()

However:

  • Never inline else blocks (use early returns instead)
  • Break nested structures regardless of length (flatten with guards)
  • Break try/catch/finally for clarity
  • Break when total line exceeds 80 characters

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 double = x => { return x * 2 }
const getName = user => { return user.name }
const makeUser = (name, age) => { return { name, age } }

Avoid pointless exotic syntax

Choose clarity over cleverness. Use language features when they improve readability, not just because they exist.

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

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

Consider iteration over repetition

Two items? Maybe copy-paste. Three or more? Use a loop for easier extension.

// ✅ Dynamic generation
return ['get', 'put', 'post', 'patch']
  .reduce((acc, method) => ({ ...acc, [method]: send }), {})

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

Avoid overuse, especially in tests; loops need mental parsing.

// ❌ Overabstracted test
['admin', 'user', 'guest'].forEach(role => {
  test(`${role} access`, t => {
    const result = checkAccess(role)
    t.assert.strictEqual(result, role === 'admin')
  })
})

// ✅ Clear, explicit tests
test('admin has access', t => {
  t.assert.strictEqual(checkAccess('admin'), true)
})

test('user lacks access', t => {
  t.assert.strictEqual(checkAccess('user'), false)
})

test('guest lacks access', t => {
  t.assert.strictEqual(checkAccess('guest'), false)
})

Keep lines ≤80; wrap by structure

  • Strongly prefer wrapping <60.
  • Break by structure even under 80; add hard line breaks at natural boundaries (operators, delimiters, chain links); keep grouping obvious; don't collapse.
// ❌ dense, mixed precedence
const ok = () => a() || b() && c(10) > 5

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

// ✅ one call per line; operator at end; hard breaks per link
api()
  .get()
  .map()
  .filter()
  .reduce() + extra()

Use 70 characters for comments

Comments are a maintenance burden. Reserve them for explaining "why", not "what".

// ✅ Explaining unusual approach
// Polling needed due to API limitations
while (!response.complete)
  await request(query, { delay: 200 })

// ❌ Obvious comments
// sum prices
const total = prices.reduce((sum, price) => sum + price, 0)

paradigms

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)

Use object-orientation for stateful entities

If it has identity and changes over time (User, Game, Session), make it a class.

// ✅ OOP for entities
class User {
  constructor(name, role) {
    this.name = name
    this.role = role
  }

  evaluate = () => this.score > 80

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

functional

Prefer expressions over statements

Expressions return values and compose.

// ✅ Good: ternaries over if
const upload = async (file, n = 0) => {
  const result = await s3.upload(file)

  return result.err
    ? n <= 3
      ? upload(file, ++n)
      : ignore(file)
    : result
}

// ✅ Good: object lookup over switch/case
const handle = action => ({
  create: () => save(data),
  update: () => modify(data),
  delete: () => remove(data)
})[action]?.() || cancel()
// ❌ Avoid
switch (action) {
  case 'create':
    return save(data)
  case 'update':
    return modify(data)
  case 'delete':
    return remove(data)
  default:
    return defaultAction()
}

Consider collapsing into an expression

Use a concise arrow only when the body is a single clear expression (≈≤60 chars); otherwise keep the grouped steps.

// from above: paginate
const paginate = (xs, page = 1, size = 25) =>
  !Array.isArray(xs) || page < 1 || size < 1
    ? []
    : xs.slice((page - 1) * size, page * size)

Stay immutable where possible

[...arr, item] not arr.push(item). New values are easier to debug than mutations.

// ✅ Immutable
const add = (users, user) => [...users, user]
const update = (user, data) => ({ ...user, ...data })

// ❌ Mutating
const add = (users, user) => users.push(user)
const update = (user, data) => Object.assign(user, data)

Avoid (most) imperative loops

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

// ✅ Functional approach
const active = users.filter(user => user.active)
const names = users.map(user => user.name)
const total = prices.reduce((sum, price) => sum + price, 0)

// ✅ Sequential async operations
for (const file of files)
  await fetch(file)

// ❌ Imperative loops
const active = []
for (let i = 0; i < users.length; i++) {
  if (users[i].active) {
    active.push(users[i])
  }
}

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 process = data =>
  reduce(map(filter(data, x => x > 0), x => x * 2), (a, b) => a + b)

object-oriented

Use method-chaining for fluent APIs

animation.fadeIn().wait(300).slideOut()

Each method returns the object for the next.

// ✅ Method chaining
class Calculator {
  constructor(val = 0) { this.val = val }

  add = n => new Calculator(this.val + n)
  mul = n => new Calculator(this.val * n)
  get = () => this.val
}

// Usage: new Calculator(5).add(3).mul(2).get() // 16

// ✅ Builder pattern for async workflows
const query = select('name', 'email')
  .from('users')
  .where('active', true)
  .where('role', 'admin')
  .orderBy('name')
  .limit(10)

// ✅ Domain-specific naming
const animation = animate(element)
  .to({ opacity: 0, x: 100 })
  .duration(300)
  .easing('ease-out')
  .then(() => element.remove())

error handling

Avoid defensive programming

Trust your code; if you can't, write tests. Definitely don't litter your code with guard clauses.

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

// ❌ Defensive programming within internal functions
const process = data => {
  if (!data || !Array.isArray(data)) return []

  return data
    .filter(item => item && active(item))
    .map(item => item ? normalize(item) : null)
    .filter(Boolean)
}

// ❌ Defensive early returns
const formatName = (first, last) => {
  if (!first) return ''  // Why is caller passing empty names?
  if (!last) return ''   // Fix the caller instead

  return `${first} ${last}`
}

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

Exception:

External data must be validated.

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()

// ❌ No normalization
const port = process.env.PORT  // Could be undefined/string
const enabled = process.env.ENABLED  // String, not boolean

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')

  // Trust internal processing
  return save(normalize(data))
}

// ❌ Late validation
const processUser = data => {
  const result = expensiveOperation(data)
  if (!data.name) throw new Error('Invalid data')  // Too late
  return result
}

testing

Use built-in test runner

Since Node v19: node --test. Zero dependencies.

Node v24+ automatically awaits subtests, eliminating a major source of flaky tests.

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

// ❌ External dependencies
import { describe, it } from 'jest'
import { expect } from 'chai'

Structure tests hierarchically

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

Write for non-technical readers—including future you. You won't remember the context when it breaks.

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

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

// ❌ Flat structure
test('withdraw disperses amount when within balance', t => {})
test('withdraw throws when amount exceeds balance', t => {})

Write focused tests

One test, one assertion, one failure reason.

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

// ❌ Multiple assertions, redundant titles
test('ATM withdrawal when amount is within available balance should disperse the requested amount and update balance', async t => {
  const initial = t.atm.balance
  const result = await t.atm.withdraw(30)
  t.assert.strictEqual(result, 70)
  t.assert.strictEqual(t.atm.balance, 70)
  t.assert.strictEqual(initial - 30, t.atm.balance)
  t.assert.ok(t.atm.lastTransaction)
})

Use the test context for passing data

Avoid global shared variables for passing data between tests. Attach shared test data directly to the context using:

t.beforeEach(t => tr.atm = new ATM(100)).

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

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

// ❌ External fixtures
let atm  // Shared global

beforeEach(() => {
  atm = new ATM(100)
})

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

Use context for test utilities

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

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

    t.assert.strictEqual(emailer.mock.calls.length, 1)
  })
})

// ❌ Imported utilities
import { assert, mock } from 'node:test'

test('sends email', t => {
  const emailer = mock.fn()
  // ...
  assert.strictEqual(emailer.mock.calls.length, 1)
})

Assert the minimum required

Don't test what you don't care about,
especially if the object is expected to be extended.

// ✅ Partial assertions
test('#user properties', t => {
  t.test('has correct details', t => {
    t.assert.partialDeepStrictEqual(t.user, {
      name: 'Alice', role: 'admin'
    })
  })
})

// ❌ Full object matching
test('has correct details', t => {
  t.assert.deepStrictEqual(t.user, {
    id: '123',
    name: 'Alice',
    role: 'admin',
    createdAt: new Date('2024-01-01'),
    updatedAt: new Date('2024-01-01'),
    // ... all properties
  })
})

Test for keywords, not sentences

Match keywords that survive rephrasing: "Insufficient balance" → "You have insufficient funds"

However: pick keywords unique to the test:

  • /insufficient/ - specific to this error
  • /balance/ or /invalid/ - too generic

Otherwise your test becomes junk.

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

// ❌ Exact string matching
await t.assert.rejects(() => t.atm.withdraw(150), {
  name: 'Error',
  message: 'Insufficient funds: cannot withdraw 150 from balance of 100'
})

Complete test example:

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

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

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

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

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

documentation

Line breaks and spacing

  • Headers: 3-5 words max
  • 70-85 chars per line, break at 50+ to newline
  • AVOID WRAPPING - start new line instead of wrapping long text
  • One empty line before and after all headers and bold: labels
  • 2-space hard line breaks where applicable
// ✅ Proper breaks and spacing
Configure the database.
Set DB_URL in your environment.

**Required:**

- Node 18+
- PostgreSQL

```js
const db = connect(url)
```

Next section starts here.

// ❌ Poor formatting
Configure the database connection by setting the DB_URL
environment variable in your shell or configuration file.
**Required:**
- Node 18+

```js
const db = connect(url)
```
Next section starts here.

Write concisely and directly

Active voice. Matter-of-fact tone. Under 20 words. Lists over paragraphs. No emojis. Reference-style links.

// ✅ Direct, scannable, technical
The server validates requests.

Requirements:

- Node 18+
- 2GB RAM

Processes 1000 requests/second.
See [node-docs] for details.

[node-docs]: https://nodejs.org/api

// ❌ Passive, verbose, marketing
Requests are validated by the server infrastructure 🚀

In order to run this powerful enterprise solution, you'll
need Node.js version 18 or higher and at least 2GB of RAM.

Lightning-fast performance at enterprise scale.

Use reference-style links

Link to authoritative sources. Use a short, consistent, non-numeric format:

// ✅ Descriptive, consistent references
See the [node-docs] for details.
Based on [rfc-7231].

[node-docs]: https://nodejs.org/api/documentation.html
[node-util]: https://nodejs.org/api/util.html
[rfc-7231]: https://tools.ietf.org/html/rfc7231

// ❌ Numeric or inline URLs
See the [docs][1] for details.
See the [docs](https://nodejs.org/api/documentation.html).

[1]: https://nodejs.org/api/documentation.html

Add whitespace between headers and content

// ✅ Linebreak before and after header

Lorem Ipsum Dolor sit amet,
Lorem Ipsum Dolor, lorem Ipsum Dolor sit amet..

### Overview

Lorem Ipsum Dolor sit amet. Lorem Ipsum Dolor,
lorem Ipsum Dolor sit amet...

**lorem:**

Lorem Ipsum Dolor sit amet. Lorem Ipsum Dolor,
lorem Ipsum Dolor sit amet...

// ❌ content immediately follows a header/label.

Lorem Ipsum Dolor sit amet,
Lorem Ipsum Dolor, lorem Ipsum Dolor sit amet..

### Overview.
Lorem Ipsum Dolor sit amet. Lorem Ipsum Dolor,
lorem Ipsum Dolor sit amet...

**lorem:**.
Lorem Ipsum Dolor sit amet. Lorem Ipsum Dolor,
lorem Ipsum Dolor sit amet...

author: nicholaswmin, MIT

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment