Skip to content

JavaScript – Code Formatting & Structure

Consistent formatting removes friction when reading, reviewing, and merging code. At Cygnus Dynamics, formatting is automated — Prettier and ESLint handle it, so code reviews focus on logic and design rather than style debates.

Automate everything on this page

Every rule on this page is enforced automatically by the toolchain. Run npm run lint to check and npm run format to auto-fix. Configure your editor to format on save.

Indentation

Use 2 spaces per indentation level. Never use tabs:

// ✅ 2-space indentation
function processOrder(order) {
  if (!order.items.length) {
    throw new Error('Order is empty');
  }

  return order.items.reduce((total, item) => {
    return total + item.price * item.quantity;
  }, 0);
}

// ❌ 4-space indentation (Java convention — not used in JS at Cygnus)
function processOrder(order) {
    if (!order.items.length) {
        throw new Error('Order is empty');
    }
}

Line Length

Keep all lines under 100 characters:

// ✅ Break after comma
const result = await orderService.createOrder(
  userId,
  shippingAddress,
  paymentMethod,
  cartItems,
);

// ✅ Break chained method calls
const activeUserEmails = await userRepository
  .findAll()
  .filter(user => user.isActive && user.hasMarketingConsent)
  .map(user => user.email);

// ❌ Long line — hard to read
const result = await orderService.createOrder(userId, shippingAddress, paymentMethod, cartItems);

Semicolons

Every statement must end with a semicolon. Do not rely on ASI — it has well-known edge cases:

// ✅ Explicit semicolons
const user = await fetchUser(userId);
const orders = await fetchOrders(userId);
sendWelcomeEmail(user);

// ❌ ASI trap — interpreted as: const b = 2[a, b].forEach(...)
const a = 1
const b = 2
[a, b].forEach(n => console.log(n))

Braces

Always Use Braces for Control Structures

// ✅
if (order.isEmpty()) {
  throw new EmptyOrderError();
}

for (const item of order.items) {
  await inventoryService.reserve(item);
}

// ❌ No braces — adding a second line here is a silent bug
if (order.isEmpty())
  throw new EmptyOrderError();

K&R Brace Style

Opening braces on the same line as the statement:

// ✅ K&R style
function fetchUser(userId) {
  if (!userId) {
    throw new Error('userId is required');
  }
  return userRepository.findById(userId);
}

// ❌ Allman style — not used in JavaScript
function fetchUser(userId)
{
  // ...
}

Quotes

Single quotes for plain strings. Template literals for interpolation and multi-line. Double quotes only when the string contains a single quote:

// ✅
const currency = 'USD';
const message = `Welcome back, ${user.firstName}!`;
const label = "It's confirmed";

// ❌
const currency = "USD";
const message = 'Welcome back, ' + user.firstName + '!';

Whitespace

// ✅ One space around binary operators
const total = price + tax;
const isEligible = userAge >= MINIMUM_AGE && hasConsent;

// ✅ One space after keywords, before opening parenthesis
if (condition) { }
for (const item of items) { }

// ✅ No space between function name and parenthesis
function calculateTotal(items) { }
const result = calculateTotal(cartItems);

Trailing Commas

Use trailing commas in multi-line arrays, objects, and parameter lists. This produces cleaner diffs:

// ✅ Trailing commas — adding a new entry only touches one line
const config = {
  apiUrl: 'https://api.cygnusdynamics.com',
  timeout: 5000,
  retryCount: 3,       // ← trailing comma
};

function createOrder(
  userId,
  shippingAddress,
  paymentMethod,       // ← trailing comma
) { }

Blank Lines

// ✅ Blank lines group related logic
async function checkout(userId, cartId) {
  // 1. Load data
  const user = await userService.getUser(userId);
  const cart = await cartService.getCart(cartId);

  // 2. Validate
  validateUserIsActive(user);
  validateCartHasItems(cart);

  // 3. Process
  const order = await orderService.createOrder(user, cart);
  const payment = await paymentService.charge(order);

  return buildCheckoutResponse(order, payment);
}

class OrderService {
  constructor(repository, paymentService) {
    this.#repository = repository;
    this.#paymentService = paymentService;
  }
                                              // ← one blank line between methods
  async getOrder(orderId) {
    return this.#repository.findById(orderId);
  }
                                              // ← one blank line between methods
  async createOrder(request) {
    const order = await this.#buildOrder(request);
    return this.#repository.save(order);
  }
}

Imports

Grouping and Ordering

// 1. Node built-ins
import path from 'path';
import fs from 'fs/promises';

// 2. Third-party packages
import express from 'express';
import { z } from 'zod';

// 3. Internal modules — absolute paths
import { OrderService } from '@/services/order-service.js';
import { UserRepository } from '@/repositories/user-repository.js';

// 4. Relative imports
import { validateRequest } from './middleware/validation.js';
import { buildResponse } from './helpers/response.js';

Named Exports Over Default Exports

// ✅ Named exports — name is fixed and consistent everywhere
export class OrderService { }
export function calculateTotal(items) { }
export const DEFAULT_PAGE_SIZE = 20;

import { OrderService } from './order-service.js';  // always the same name

// ❌ Default export — importers can name it anything
export default class OrderService { }

import Orders from './order-service.js';    // is it the same class? unclear
import OrderMgr from './order-service.js';  // also works — naming chaos

Always Include File Extensions

// ✅
import { OrderService } from './order-service.js';

// ❌ Missing extension — resolution ambiguity in native ES modules
import { OrderService } from './order-service';

No Circular Dependencies

// ❌ Circular dependency — A imports B, B imports A
// ✅ Extract shared logic into a third module C that both import

Switch Statements

Always include a default case:

// ✅
switch (order.status) {
  case OrderStatus.PENDING:
    await notifyCustomer(order);
    break;
  case OrderStatus.CONFIRMED:
    await scheduleShipment(order);
    break;
  case OrderStatus.CANCELLED:
    await processRefund(order);
    break;
  default:
    logger.warn(`Unhandled order status: ${order.status}`);
    break;
}

File Structure

Every JavaScript source file follows this order:

1. File-level JSDoc comment (optional but encouraged for public modules)
2. Imports (grouped as above)
3. Module-level constants
4. Class or function declarations
5. Named exports (if not inlined)

ES Modules — package.json Configuration

Add "type": "module" to package.json to use native ES module syntax throughout your project. Without this, .js files are treated as CommonJS:

{
  "name": "@cygnusdynamics/order-service",
  "version": "1.0.0",
  "type": "module",
  "engines": {
    "node": ">=20.0.0"
  }
}

Config files in CommonJS projects

Some tooling config files (.eslintrc.js, jest.config.js) must remain CommonJS. Rename them to .cjs when your project uses "type": "module":

.eslintrc.js    →  .eslintrc.cjs
jest.config.js  →  jest.config.cjs

Every JavaScript project at Cygnus Dynamics must have these configured and running in CI:

Tool Purpose Config File
ESLint Linting, style rules, code quality .eslintrc.js or eslint.config.js
Prettier Code formatting .prettierrc
Husky Git hooks .husky/
lint-staged Run linters only on staged files package.json

Standard .prettierrc

{
  "semi": true,
  "singleQuote": true,
  "trailingComma": "all",
  "printWidth": 100,
  "tabWidth": 2,
  "useTabs": false,
  "arrowParens": "avoid"
}

Standard ESLint configuration (flat config — eslint.config.js)

ESLint v9 uses flat config by default. Use this for all new projects:

// eslint.config.js
import js from '@eslint/js';

export default [
  js.configs.recommended,
  {
    languageOptions: {
      ecmaVersion: 2022,
      sourceType: 'module',
      globals: {
        console: 'readonly',
        process: 'readonly',
      },
    },
    rules: {
      'no-var':             'error',
      'prefer-const':       'error',
      'no-unused-vars':     ['error', { argsIgnorePattern: '^_' }],
      'eqeqeq':             ['error', 'always', { null: 'ignore' }],
      'no-eval':            'error',
      'no-implicit-globals': 'error',
      'no-param-reassign':  'error',
      'no-console':         ['warn', { allow: ['warn', 'error'] }],
    },
  },
];

Complete Husky and lint-staged setup

# Install and initialise Husky
npm install --save-dev husky lint-staged
npx husky init
# .husky/pre-commit
#!/bin/sh
npx lint-staged
// package.json — lint-staged config
{
  "lint-staged": {
    "*.js": [
      "eslint --fix --max-warnings 0",
      "prettier --write"
    ]
  }
}

Complete package.json scripts

{
  "scripts": {
    "lint":          "eslint src/",
    "lint:fix":      "eslint src/ --fix",
    "format":        "prettier --write 'src/**/*.js'",
    "format:check":  "prettier --check 'src/**/*.js'",
    "test":          "vitest run",
    "test:watch":    "vitest",
    "test:coverage": "vitest run --coverage",
    "check":         "npm run format:check && npm run lint && npm run test"
  }
}

Format on save

Configure your editor to run Prettier on every save. In VS Code: install the Prettier extension and set "editor.formatOnSave": true in .vscode/settings.json. This eliminates formatting as a concern during development entirely.