Node.js Expert
Overview
Advanced expertise in Node.js — from the event loop and core modules to building production REST APIs with Express/Fastify/Hono, streams, worker threads, and modern ESM module patterns.
1. Event Loop & Async Model
┌─────────────────────────────┐
│ timers │ setTimeout, setInterval
├─────────────────────────────┤
│ pending callbacks │ I/O callbacks deferred to next loop
├─────────────────────────────┤
│ idle, prepare │ internal
├─────────────────────────────┤
│ poll │ fetch I/O events, execute callbacks
├─────────────────────────────┤
│ check │ setImmediate
├─────────────────────────────┤
│ close callbacks │ socket.on('close')
└─────────────────────────────┘
Between each phase: process.nextTick() → Promise microtasks
// Execution order
console.log('1') // sync
process.nextTick(() => console.log('2')) // nextTick queue
Promise.resolve().then(() => console.log('3')) // microtask queue
setImmediate(() => console.log('4')) // check phase
setTimeout(() => console.log('5'), 0) // timers phase
// Output: 1, 2, 3, 4, 5 (approximately)
2. Modules — ESM vs CommonJS
// ESM (modern — use in new projects)
// package.json: "type": "module"
import { readFile } from 'node:fs/promises'
import express from 'express'
export const add = (a: number, b: number) => a + b
export default class Server { }
// CommonJS (legacy)
const fs = require('fs')
module.exports = { add: (a, b) => a + b }
exports.add = (a, b) => a + b
// Dynamic import (works in both)
const { default: chalk } = await import('chalk')
// __dirname equivalent in ESM
import { fileURLToPath } from 'node:url'
import { dirname, join } from 'node:path'
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
3. Core Node.js Modules
File System
import { readFile, writeFile, readdir, mkdir, rm, stat, watch } from 'node:fs/promises'
import { createReadStream, createWriteStream } from 'node:fs'
// Read / Write
const content = await readFile('./config.json', 'utf-8')
await writeFile('./output.json', JSON.stringify(data, null, 2))
// Directory operations
await mkdir('./logs', { recursive: true })
const files = await readdir('./src', { recursive: true })
await rm('./tmp', { recursive: true, force: true })
// File info
const info = await stat('./file.txt')
console.log(info.size, info.mtime, info.isDirectory())
// Watch for changes
const watcher = watch('./src', { recursive: true })
for await (const event of watcher) {
console.log(event.eventType, event.filename)
}
Path
import { join, resolve, dirname, basename, extname, parse, relative } from 'node:path'
join('/users', 'alice', 'docs') // '/users/alice/docs'
resolve('./src', '../dist') // absolute path
basename('/foo/bar/baz.ts') // 'baz.ts'
extname('index.html') // '.html'
parse('/home/user/file.txt')
// { root: '/', dir: '/home/user', base: 'file.txt', ext: '.txt', name: 'file' }
HTTP / HTTPS
import { createServer } from 'node:http'
const server = createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ ok: true }))
})
server.listen(3000, () => console.log('Listening on :3000'))
Child Processes
import { exec, spawn, execFile } from 'node:child_process'
import { promisify } from 'node:util'
const execAsync = promisify(exec)
const { stdout } = await execAsync('git log --oneline -10')
// Streaming output
const child = spawn('npm', ['run', 'build'], { stdio: 'inherit' })
child.on('exit', code => console.log('Exit:', code))
Worker Threads
import { Worker, isMainThread, parentPort, workerData } from 'node:worker_threads'
if (isMainThread) {
const worker = new Worker('./worker.js', { workerData: { n: 40 } })
worker.on('message', result => console.log('Result:', result))
worker.on('error', err => console.error(err))
} else {
const result = fibonacci(workerData.n)
parentPort?.postMessage(result)
}
4. Express.js
import express, { Request, Response, NextFunction } from 'express'
import { z } from 'zod'
const app = express()
// Middleware
app.use(express.json())
app.use(express.urlencoded({ extended: true }))
app.use(cors({ origin: process.env.ALLOWED_ORIGIN }))
// Route with typed params/body/query
const createUserSchema = z.object({ name: z.string().min(1), email: z.string().email() })
app.post('/users', async (req: Request, res: Response) => {
const body = createUserSchema.parse(req.body)
const user = await db.user.create({ data: body })
res.status(201).json(user)
})
// Middleware pattern
function authenticate(req: Request, res: Response, next: NextFunction) {
const token = req.headers.authorization?.replace('Bearer ', '')
if (!token) return res.status(401).json({ error: 'Unauthorized' })
try {
req.user = verifyToken(token)
next()
} catch {
res.status(401).json({ error: 'Invalid token' })
}
}
// Router
const userRouter = express.Router()
userRouter.use(authenticate)
userRouter.get('/', getUsers)
userRouter.get('/:id', getUser)
userRouter.put('/:id', updateUser)
userRouter.delete('/:id', deleteUser)
app.use('/api/users', userRouter)
// Global error handler (must have 4 params)
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
console.error(err)
if (err instanceof z.ZodError) return res.status(400).json({ errors: err.errors })
res.status(500).json({ error: 'Internal server error' })
})
app.listen(3000)
5. Fastify (high-performance alternative)
import Fastify from 'fastify'
import { z } from 'zod'
const app = Fastify({ logger: true })
// Schema validation (JSON Schema — built-in)
app.post<{ Body: { name: string; email: string } }>('/users', {
schema: {
body: {
type: 'object',
required: ['name', 'email'],
properties: {
name: { type: 'string' },
email: { type: 'string', format: 'email' },
},
},
response: {
201: { type: 'object', properties: { id: { type: 'string' } } },
},
},
handler: async (req, reply) => {
const user = await db.user.create({ data: req.body })
reply.status(201).send(user)
},
})
// Plugins
await app.register(import('@fastify/cors'), { origin: true })
await app.register(import('@fastify/jwt'), { secret: process.env.JWT_SECRET! })
// Hooks
app.addHook('onRequest', async (req, reply) => {
// runs before route handler
})
await app.listen({ port: 3000, host: '0.0.0.0' })
6. Hono (edge-ready, ultra-light)
import { Hono } from 'hono'
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
const app = new Hono()
// Middleware
app.use('*', cors())
app.use('/api/*', bearerAuth({ token: process.env.API_TOKEN! }))
// Routes
const userRoute = new Hono()
.get('/', async c => {
const users = await db.user.findMany()
return c.json(users)
})
.post('/', zValidator('json', z.object({ name: z.string(), email: z.string().email() })), async c => {
const body = c.req.valid('json')
const user = await db.user.create({ data: body })
return c.json(user, 201)
})
.get('/:id', async c => {
const user = await db.user.findUnique({ where: { id: c.req.param('id') } })
if (!user) return c.json({ error: 'Not found' }, 404)
return c.json(user)
})
app.route('/api/users', userRoute)
export default app // works on Cloudflare Workers, Bun, Node.js, Deno
7. Streams
import { pipeline } from 'node:stream/promises'
import { createReadStream, createWriteStream } from 'node:fs'
import { createGzip } from 'node:zlib'
import { Transform } from 'node:stream'
// Compress a file
await pipeline(
createReadStream('./input.log'),
createGzip(),
createWriteStream('./input.log.gz')
)
// Custom transform stream
const uppercase = new Transform({
transform(chunk, encoding, callback) {
this.push(chunk.toString().toUpperCase())
callback()
}
})
// Async iterator over stream
const readable = createReadStream('./data.csv', 'utf-8')
for await (const chunk of readable) {
process.stdout.write(chunk)
}
8. Environment & Configuration
// .env loading (Node 20.6+ built-in)
// node --env-file=.env server.js
// Or with dotenv
import 'dotenv/config'
// Type-safe env with Zod
import { z } from 'zod'
const envSchema = z.object({
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
PORT: z.coerce.number().default(3000),
DATABASE_URL: z.string().url(),
JWT_SECRET: z.string().min(32),
REDIS_URL: z.string().optional(),
})
export const env = envSchema.parse(process.env)
// env.PORT is number, not string
9. Error Handling Patterns
// Async error wrapper for Express
const asyncHandler = (fn: RequestHandler) =>
(req: Request, res: Response, next: NextFunction) =>
Promise.resolve(fn(req, res, next)).catch(next)
app.get('/users/:id', asyncHandler(async (req, res) => {
const user = await db.user.findUnique({ where: { id: req.params.id } })
if (!user) throw new NotFoundError('User not found')
res.json(user)
}))
// Custom error classes
class AppError extends Error {
constructor(public message: string, public statusCode: number, public code?: string) {
super(message)
this.name = this.constructor.name
Error.captureStackTrace(this, this.constructor)
}
}
class NotFoundError extends AppError {
constructor(msg = 'Not found') { super(msg, 404, 'NOT_FOUND') }
}
class ValidationError extends AppError {
constructor(msg: string) { super(msg, 400, 'VALIDATION_ERROR') }
}
// Unhandled rejections & exceptions
process.on('unhandledRejection', (reason) => {
console.error('Unhandled rejection:', reason)
process.exit(1)
})
process.on('uncaughtException', (err) => {
console.error('Uncaught exception:', err)
process.exit(1)
})
10. Package Management
# npm
npm init -y
npm install express zod
npm install -D typescript @types/node tsx
npm ci # clean install from lock file (CI)
npm audit fix
# pnpm (recommended — faster, disk efficient)
pnpm init
pnpm add express zod
pnpm add -D typescript @types/node tsx
pnpm dlx create-next-app # like npx but no install
# Workspaces (monorepo)
# pnpm-workspace.yaml:
# packages:
# - 'apps/*'
# - 'packages/*'
pnpm --filter @myapp/ui build
pnpm -r run test # run in all packages
package.json Patterns
{
"type": "module",
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc -p tsconfig.build.json",
"start": "node dist/index.js",
"test": "vitest",
"lint": "eslint src --ext .ts"
},
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.cjs",
"types": "./dist/index.d.ts"
}
}
}
11. Performance & Clustering
import cluster from 'node:cluster'
import { availableParallelism } from 'node:os'
if (cluster.isPrimary) {
const numCPUs = availableParallelism()
console.log(`Primary ${process.pid} — forking ${numCPUs} workers`)
for (let i = 0; i < numCPUs; i++) cluster.fork()
cluster.on('exit', (worker) => {
console.log(`Worker ${worker.process.pid} died — restarting`)
cluster.fork()
})
} else {
// Worker process — start your server here
const app = createApp()
app.listen(3000, () => console.log(`Worker ${process.pid} listening`))
}
12. Testing (Vitest / Jest)
import { describe, it, expect, vi, beforeEach } from 'vitest'
import request from 'supertest'
import { app } from '../src/app'
describe('GET /api/users', () => {
beforeEach(() => vi.clearAllMocks())
it('returns list of users', async () => {
vi.spyOn(db.user, 'findMany').mockResolvedValue([{ id: '1', name: 'Alice' }])
const res = await request(app).get('/api/users').expect(200)
expect(res.body).toHaveLength(1)
expect(res.body[0].name).toBe('Alice')
})
it('returns 401 without auth token', async () => {
await request(app).get('/api/users/me').expect(401)
})
})
Core Competency Summary
- Understand the Node.js event loop, microtasks, and async model
- Use ESM modules and core Node.js built-ins (
node:fs,node:path,node:stream) - Build REST APIs with Express, Fastify, or Hono with validation and error handling
- Work with streams, worker threads, and child processes
- Configure type-safe environment variables with Zod
- Manage packages with npm/pnpm, including monorepo workspaces
- Test Node.js APIs with Vitest and Supertest
- Scale with clustering and optimize for production