Chapter 38: Node.js ES6+ Features
ES6+ (modern JavaScript) features that matter the most when writing Node.js code in 2025–2026.
I will explain each feature as if we are sitting together:
- Why it exists
- What problem it solves
- How it looks in real Node.js code
- Common mistakes people still make
- When to prefer it over older patterns
- Typical production usage patterns in 2025–2026
We’ll go roughly in order of how often you will actually use them in serious Node.js backend work.
1. Top-level await (the single biggest quality-of-life improvement)
Since Node.js 14.8 (2020) → fully stable, widely used since ~16–18
What it lets you do Use await directly at the top level of a module (no async wrapper function needed)
Old painful way (pre-2020)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 |
// Everyone wrote this ugly wrapper (async function main() { const config = await loadConfig(); const db = await prisma.$connect(); const app = createApp(db); app.listen(3000); })().catch(console.error); |
Modern clean way (2025–2026 standard)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// index.ts import 'dotenv/config'; // works with top-level await import { prisma } from './prisma/client.js'; import { createApp } from './app.js'; await prisma.$connect(); // ← top-level await const app = createApp(); const port = process.env.PORT || 4000; app.listen(port, () => { console.log(`🚀 Server running on http://localhost:${port}`); }); |
Very common patterns you see everywhere today
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// Startup sequence – extremely common import { config } from './config.js'; import { createServer } from './server.js'; import { initLogger } from './logger.js'; const cfg = await config.load(); // top-level await const logger = await initLogger(cfg.logging); const server = createServer(cfg, logger); await server.start(); |
Important gotchas
- Only works in ES Modules (“type”: “module” in package.json or .mjs)
- If the module throws → the whole process crashes (good for startup)
- Cannot use top-level await in CommonJS files
Verdict Must-use in every new Node.js project that loads config, connects to DB, or initializes services.
2. ESM – import / export (you should already be all-in)
package.json requirement
|
0 1 2 3 4 5 6 7 8 |
{ "type": "module" } |
Most common patterns in 2025–2026
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// Named imports – 80% of the time import { createHash } from 'node:crypto'; import { prisma } from './prisma/client.js'; import { validate } from './schemas/user.js'; // Default + named import fastify, { FastifyInstance } from 'fastify'; // Namespace import (when you want everything) import * as fs from 'node:fs/promises'; import * as db from './database/operations.js'; // Dynamic import (lazy loading heavy modules) if (process.env.FEATURE_AI === 'true') { const { generateSummary } = await import('./ai/summary.js'); } |
Barrel files (very common pattern)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 |
// src/index.ts (barrel) export * from './user.service.js'; export * from './task.service.js'; export * from './auth.service.js'; // Usage import { getUserById, createTask } from './services/index.js'; |
Very useful trick – alias paths (tsconfig + package.json)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// tsconfig.json { "compilerOptions": { "baseUrl": "src", "paths": { "@services/*": ["services/*"], "@utils/*": ["utils/*"] } } } |
|
0 1 2 3 4 5 6 7 |
import { prisma } from '@services/db'; import { formatDate } from '@utils/date'; |
3. Optional chaining (?.) + Nullish coalescing (??)
These two operators together remove ~40% of defensive code.
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// Old noisy style const city = user && user.profile && user.profile.address && user.profile.address.city ? user.profile.address.city : 'Unknown'; // Modern clean style const city = user?.profile?.address?.city ?? 'Unknown'; // Even better with default object const city = user?.profile?.address?.city ?? config.defaultCity ?? 'Hyderabad'; |
Very frequent patterns in Node.js code
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 |
// Environment variables const port = process.env.PORT ?? 3000; // Request body (Express/Fastify) const userId = req?.body?.userId ?? throw new Error('Missing userId'); // Deep API response const balance = transaction?.data?.account?.balance?.value ?? 0; |
Important difference: ?? vs ||
|
0 1 2 3 4 5 6 7 8 9 10 |
0 ?? 100 // → 0 (good – 0 is valid value) 0 || 100 // → 100 (bad – 0 is falsy) '' ?? 'default' // → '' (empty string is valid) '' || 'default' // → 'default' (bad) |
Rule: Always prefer ?? over || for defaults unless you specifically want to treat falsy values the same.
4. Destructuring + rest / spread (used literally everywhere)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
// Function parameters + defaults function createTask({ title, description = '', priority = 'medium' }: TaskInput) { // ... } // Nested destructuring const { data: { user: { id, name, email } } } = await prisma.user.findFirstOrThrow(); // Rest in objects (remove sensitive fields) const { passwordHash, refreshToken, ...safeUser } = dbUser; // Spread for immutable updates const updatedUser = { ...user, lastLogin: new Date(), status: 'active' }; // Spread + rename const { name: fullName, age: userAge } = user; |
Very powerful pattern – rename + default + rest
|
0 1 2 3 4 5 6 7 8 9 10 11 |
const { id: userId, name: displayName = 'Guest', email, ...metadata } = await getUser(); |
5. Template literals + tagged templates
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// Normal template literal const msg = `User ${user.name} (id: ${user.id}) logged in at ${new Date().toISOString()}`; // Tagged template (very powerful) import sql from 'sql-template-strings'; const query = sql` SELECT id, name, email FROM users WHERE email = ${email} AND active = TRUE LIMIT ${limit} `; |
Popular tagged template libraries in Node.js ecosystem
- sql-template-strings
- common-tags (dedent, stripIndent, oneLine)
- clsx / twMerge for Tailwind classes
- zod error formatting (sometimes)
Very common helper
|
0 1 2 3 4 5 6 7 8 9 10 11 12 |
export const dedent = String.raw; const sql = dedent` SELECT * FROM users WHERE id = ${userId} `; |
6. Logical assignment operators (very clean defaults)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 |
// Old style config.timeout = config.timeout || 5000; // Modern (ES2021+) config.timeout ||= 5000; // assign only if falsy config.apiKey ??= throw new Error('Missing API key'); // assign only if null/undefined config.debug &&= process.env.NODE_ENV !== 'production'; // assign only if true |
Very frequent in config files
|
0 1 2 3 4 5 6 7 8 9 10 |
export const settings = { port: process.env.PORT ?? 4000, logLevel: process.env.LOG_LEVEL ||= 'info', debug: process.env.DEBUG === 'true' &&= true }; |
7. Private class fields & methods (#private)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
class UserService { #prisma: PrismaClient; #logger: Logger; constructor(prisma: PrismaClient, logger: Logger) { this.#prisma = prisma; this.#logger = logger; } async findByEmail(email: string) { this.#validateEmail(email); return this.#prisma.user.findUnique({ where: { email } }); } #validateEmail(email: string) { if (!email.includes('@')) { throw new Error('Invalid email'); } } } |
Benefits in real Node.js code
- True encapsulation (cannot access from outside)
- Better refactoring safety
- Cleaner mental model than private keyword in TypeScript alone
Quick summary table – most impactful features for Node.js backend
| Feature | Daily impact in Node.js | Typical line of code example |
|---|---|---|
| Top-level await | Clean startup, config, DB init | await prisma.$connect() |
| ESM (import/export) | Future-proof, same syntax as frontend | import { prisma } from ‘./prisma.js’ |
| ?. and ?? | Removes tons of defensive code | user?.profile?.city ?? ‘Unknown’ |
| Destructuring + rest/spread | Clean data extraction, immutable updates | const { password, …safe } = user |
| Template literals + tagged | SQL, logging, HTML, CLI output | sqlSELECT * FROM users WHERE id = ${id} |
| Logical assignment | = ??= | |
| Private fields (#) | Real encapsulation in services / domain models | class { #secretKey: string } |
| structuredClone() | Deep copy (native since Node 17) | const copy = structuredClone(original) |
Which of these features would you like to explore much deeper next?
- Full top-level await startup boilerplate (config + DB + logger + server)
- Realistic ESM project structure with barrels, aliases, dynamic imports
- Writing safe & readable SQL with tagged templates
- Using private fields + TypeScript in real services
- Modern configuration loading patterns with validation (zod + top-level await)
Just tell me what feels most useful right now — I’ll continue with very concrete, production-ready code examples. 😊
