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":
Recommended Tooling
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
// 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.