Chapter 37: JS & TS Features
JavaScript & TypeScript features that matter most when working with Node.js (especially in 2025–2026).
I will speak as if we are sitting together in a coding session — I’m showing you code, explaining why this feature is useful, when you should reach for it, what problems it solves, what common mistakes people make, and how real Node.js developers use it today.
We will cover the features in rough order of importance for modern Node.js backend work.
1. Top-level await (the single biggest game-changer since async/await)
What it is You can now use await directly at the top level of a module (no need to wrap everything in an async function).
When Node.js got it Fully stable since Node.js 14.8 (2020), widely used since ~Node 16–18.
Why it is so important for Node.js
Before top-level await:
|
0 1 2 3 4 5 6 7 8 9 10 11 |
// old style – ugly wrapper (async function main() { const config = await loadConfig(); const db = await connectDb(config); startServer(db); })().catch(console.error); |
After top-level await (clean & beautiful):
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// index.ts import { config } from './config.js'; import { connectDb } from './db.js'; import { startServer } from './server.js'; const dbConfig = await config.load(); const db = await connectDb(dbConfig); startServer(db); console.log(`Server listening on ${process.env.PORT}`); |
Real-world patterns you see everywhere in 2025–2026
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// Most common startup pattern import 'dotenv/config'; // top-level await compatible import { createApp } from './app.js'; import { prisma } from './prisma.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 important gotcha
Top-level await only works in ES Modules (“type”: “module” in package.json or .mjs files).
2. ESM – import / export (you should already be using this)
Quick checklist what you should be doing in 2025–2026
|
0 1 2 3 4 5 6 7 8 9 |
// package.json { "type": "module" } |
|
0 1 2 3 4 5 6 7 8 9 10 11 12 |
// Good import { createServer } from 'node:http'; import { config } from './config.js'; // Bad (legacy) const http = require('http'); const config = require('./config'); |
Modern patterns
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// Named imports (most common) import { randomUUID } from 'node:crypto'; import { prisma } from './prisma/client.js'; // Default + named import Fastify, { FastifyInstance } from 'fastify'; // Namespace import (when you want everything) import * as fs from 'node:fs/promises'; |
Very useful trick – dynamic import
|
0 1 2 3 4 5 6 7 8 9 10 |
// Lazy load heavy module only when needed if (process.env.NODE_ENV === 'production') { const { optimizeImage } = await import('./image-optimizer.js'); // use it } |
3. Optional chaining (?.) & Nullish coalescing (??)
These two operators alone remove ~30–40% of defensive code in real Node.js projects.
|
0 1 2 3 4 5 6 7 8 9 10 11 12 |
// Old style – lots of noise const user = dbResult && dbResult.user; const name = user && user.profile && user.profile.name; const city = name ? user.profile.address?.city : 'Unknown'; // Modern & clean const city = dbResult?.user?.profile?.address?.city ?? 'Unknown'; |
Very common Node.js patterns
|
0 1 2 3 4 5 6 7 8 9 10 11 12 |
const port = process.env.PORT ?? 3000; // Safe access to deeply nested config const apiKey = config?.services?.stripe?.apiKey ?? throw new Error('Missing Stripe key'); // Optional chaining on request objects const userId = req?.user?.id ?? null; |
Pro tip ?? is much better than || for defaults because:
|
0 1 2 3 4 5 6 7 |
0 ?? 100 // → 0 (good – 0 is valid) 0 || 100 // → 100 (bad – 0 is falsy) |
4. Destructuring + rest / spread – everywhere
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
// Function parameters function createUser({ name, email, age = 18 }: { name: string; email: string; age?: number }) { // ... } // Nested destructuring const { data: { user: { name, email } } } = await prisma.user.findFirstOrThrow(); // Rest in objects const { passwordHash, ...safeUser } = user; // Spread for merging const defaults = { timeout: 5000, retry: 3 }; const config = { ...defaults, ...userConfig }; |
Very powerful pattern – rename + default + rest
|
0 1 2 3 4 5 6 7 8 9 10 |
const { id: userId, name: fullName = 'Anonymous', ...rest } = await getUser(); |
5. Template literals + tagged templates
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// Normal template literal const greeting = `Hello ${name}, welcome back!`; // Tagged template (very powerful) import sql from '@your-sql-tag'; const query = sql` SELECT * FROM users WHERE email = ${email} AND active = true `; |
Popular libraries that use tagged templates:
- @prisma/client raw queries
- sql-template-strings
- common-tags
- dedent
- clsx / tw-merge for classnames
Very common pattern
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
const dedent = (strings: TemplateStringsArray, ...values: any[]) => { let result = strings[0]; for (let i = 1; i < strings.length; i++) { result += values[i - 1] + strings[i]; } return result.replace(/^\s+/gm, '').trim(); }; const query = dedent` SELECT id, name, email FROM users WHERE id = ${userId} `; |
6. Logical assignment operators (very clean)
|
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; // only if falsy config.port ??= 3000; // only if null/undefined config.debug &&= process.env.NODE_ENV !== 'production'; // only if true |
Very useful in config files
|
0 1 2 3 4 5 6 7 8 9 10 |
export const config = { 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 |
class Database { #connection: any; #isConnected = false; async connect() { if (this.#isConnected) return; this.#connection = await createConnection(); this.#isConnected = true; } #logQuery(query: string) { if (this.#isConnected) { console.debug('Executing:', query); } } } |
Very common in services & ORMs
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
class UserService { #prisma: PrismaClient; constructor(prisma: PrismaClient) { this.#prisma = prisma; } 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'); } } |
8. Quick summary table – most important features for Node.js in 2025–2026
| Feature | Why you should use it in Node.js | Typical line of code example |
|---|---|---|
| Top-level await | Clean startup scripts, config loading | const db = await connectDb(); |
| ESM (import/export) | Future-proof, tree-shakable, same syntax as frontend | import { prisma } from ‘./prisma.js’ |
| Optional chaining + ?? | Removes 30–50% of defensive code | user?.profile?.address?.city ?? ‘Unknown’ |
| Destructuring + rest/spread | Clean extraction, immutable updates | const { password, …safe } = user |
| Template literals + tagged | SQL, HTML, CLI output, classnames | sqlSELECT * FROM users WHERE id = ${id} |
Logical assignment ( |
=, ??=) | |
| Private fields (#) | Real encapsulation, better refactoring safety | class { #secretKey: string } |
| structuredClone | Deep copy objects (native since Node 17) | const copy = structuredClone(original) |
Which feature would you like to explore much deeper next?
- Full top-level await startup patterns (config, DB, server)
- Realistic ESM project structure with barrel files, aliases, dynamic imports
- Writing tagged template literals (SQL, HTML, CLI output)
- Using private fields + TypeScript in services / domain models
- Modern configuration loading patterns with top-level await + zod
Just tell me what interests you most — I’ll continue with very concrete, production-ready examples. 😊
