Code Style & Conventions
Code style isn't about aesthetics — it's about reducing cognitive load. When naming, structure, and patterns are consistent across a codebase, developers read unfamiliar code faster, spot anomalies more readily, and make fewer "this looked right stylistically but was wrong semantically" mistakes. Enforcing style automatically (Prettier, ESLint) removes the conversation from code review entirely. This page covers the naming and structural conventions that most professional Node codebases share, plus the async and error-handling patterns that define readable modern Node code.
Naming conventions
// Variables, functions, methods, parameters: camelCase
const userCount = 0
function findUserById(id: string) {}
// Classes, types, interfaces, enums: PascalCase
class UsersService {}
interface UserDto { name: string }
type CreateUserResult = { id: string }
enum UserRole { Admin = 'admin', Member = 'member' }
// Constants / env-sourced values: UPPER_SNAKE_CASE
const MAX_RETRY_COUNT = 3
const { DATABASE_URL } = process.env // env vars are UPPER_SNAKE already
// Files: kebab-case matching the primary export
// users.service.ts → exports UsersService
// auth.middleware.ts → exports authenticate middleware
// create-user.dto.ts → exports CreateUserDtoAsync code conventions
// ✅ async/await — readable, debuggable, easy to follow the flow:
async function getUser(id: string): Promise<User> {
const user = await usersRepo.findById(id)
if (!user) throw new NotFoundError(id)
return user
}
// ✅ Parallel work — don't await sequentially when independent:
const [user, orders] = await Promise.all([
usersRepo.findById(id),
ordersRepo.findByUser(id),
])
// ❌ Callback hell — avoid in new code; use promisified APIs or util.promisify:
fs.readFile(path, (err, data) => {
if (err) return callback(err)
// nested callback...
})Error handling conventions
// 1. Use typed domain error classes — never strings or generic Error:
class NotFoundError extends Error {
readonly statusCode = 404
constructor(resource: string) { super(`${resource} not found`); this.name = 'NotFoundError' }
}
// 2. Throw at the point of detection, catch at the boundary:
async function deleteUser(id: string) {
const user = await repo.findById(id)
if (!user) throw new NotFoundError(`User:${id}`) // throw here...
await repo.delete(id)
}
// 3. Map to HTTP at the boundary — one central error middleware:
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
if (err instanceof NotFoundError) return res.status(404).json({ error: err.message })
logger.error(err)
res.status(500).json({ error: 'Internal server error' })
})Module and import conventions
Prefer named exports over default exports — refactor tools can rename them reliably, and the name is explicit at the import site.
Use path aliases (
@config,@services) rather than deep relative imports (../../..) — readable and refactor-safe.Import order: Node built-ins first, then npm packages, then internal modules — with a blank line between groups (ESLint import plugin enforces this).
Keep imports at the top of the file — no dynamic
require()in the middle of functions except for lazy-loading.Avoid circular imports — they cause subtle initialization bugs and signal a missing abstraction layer.