LearnThatStack Ace your next interview

Node.js

Node.js 22+ and Express 5 Checked Nov 2026 99 code snippets

The model

  • Your JavaScript runs on one main thread. Underneath, libuv runs the event loop and a small thread pool.
  • I/O doesn't block, so one thread can serve thousands of connections, as long as no callback runs for long.
  • After each callback, Node.js drains the process.nextTick() queue, then the promise microtask queue.
  • An uncaught throw or unhandled rejection crashes the process by default. A process manager then starts a clean one.
Your JavaScriptmain threadEvent loopmain thread, libuvThread poollibuv, 4 by defaultOperating systemepoll, kqueueSTARTfs, crypto, zlibnetwork I/OFINISHtask donesocket readyrun callback
Slow work leaves the main thread and comes back as a callback on that same thread.

How Node.js runs your code

one JavaScript thread, with libuv underneath

Event loop phases

timerspendingpollcheckclose
one passLeft to right, then it repeats. Idle and prepare sit before poll.
Run due timersTimers phase
Collect I/O eventsPoll phase. It waits here if nothing else is due.
Run immediatesCheck phase
Run other callbacksPending and close phases

TrapnextTick and promise callbacks have no phase.

Order of nextTick, promises and timers

// logged order, in a CommonJS filesetTimeout(() => log('timer'), 0);4th or 5thsetImmediate(() => log('immediate'));4th or 5thprocess.nextTick(() => log('tick'));2ndPromise.resolve().then(() => log('p'));3rdlog('sync');1st
Run before the loop moves onprocess.nextTick(fn)
Queue a microtask insteadqueueMicrotask(fn)

TipInside an I/O callback, setImmediate() always runs before a 0 ms timer.

TrapRecursive nextTick calls can starve the loop and stop I/O.

Async code & its errors

from callbacks to await, and where errors land

Promises and async/await

Name the statesPending, then fulfilled or rejected
Chain with one error handlerp.then(f).catch(h)
Wait for a promiseconst x = await p;Pauses this function, not the thread
Get every resultawait Promise.all(ps)Rejects on the first failure
Report each failureawait Promise.allSettled(ps)Never rejects

TipAwaiting one by one runs calls in sequence. Use Promise.all().

TrapNeither all nor allSettled cancels the other promises.

EventEmitter

Add or remove a listenere.on('done', fn)e.off('done', fn)
Fire an evente.emit('done', data)Calls listeners synchronously

TipOver 10 listeners on one event triggers a leak warning.

TrapAn 'error' event with no listener throws and crashes the process.

Modules, npm & patterns

load code, pin versions, set config

CommonJS or ES modules

Load with CommonJSrequire('./x')Synchronous
Load with ES modulesimport x from './x.js'No __dirname, use import.meta.dirname
Pick the system per file.mjs is ESM, .cjs is CommonJS, .js follows "type"
Require an ES module (22.12+)Only with no top-level await

TipESM imports are live bindings. require() gives you a copy.

TrapA relative import must include the file extension.

How require() finds a module

First, try built-in modulesrequire('node:fs')Beats a same-named file
Then a relative or absolute pathExact name, .js, .json, .node, then a directory
Then a packagenode_modules in each folder, up to the root

TipResults are cached by full path, so a file runs only once.

npm scripts and the lock file

Run a script from package.jsonnpm run devprestart runs before start
Compare the two filespackage.json holds ranges. The lock file holds exact versions.
Install what the lock file listsnpm ci

TipCommit package-lock.json. Its hashes catch a tampered download.

TrapScripts run in sh or cmd.exe, so Unix commands fail on Windows.

Environment config

Load a .env filenode --env-file=.env app.js
Load it in older coderequire('dotenv').config()
Read a settingprocess.env.PORT

TipCheck required variables at startup. Keep .env out of Git.

TrapA default JWT_SECRET lets anyone sign valid tokens.

Design patterns in Node.js

Get a singleton from the module cachemodule.exports = new Pool()
Pick code from configUse a factory or a strategy

TipEventEmitter is the observer. Middleware is a chain of responsibility.

Buffer, files & streams

handle data without filling memory

Buffer

Make a zero-filled bufferBuffer.alloc(10)
Encode and decode textBuffer.from('Hi')buf.toString('base64')
Join buffersBuffer.concat([a, b])

TipA Buffer never changes size, so joining copies the bytes.

TrapallocUnsafe() is faster but may expose old data.

Files and streams

Read or write with promisesawait readFile(p, 'utf8')await writeFile(p, s)From fs/promises
Read with a callbackfs.readFile(p, cb)
Read in chunksfs.createReadStream(p)Memory stays flat. readFile loads it all.
Name the four kindsReadable, writable, duplex, transform
Connect streams safelypipeline(a, b, cb)
Explain backpressureThe readable pauses when the writable's buffer fills

TipSync calls such as readFileSync() block the event loop.

Trappipe() doesn't close the writable when the readable fails.

HTTP servers & Express

the built-in server, then routes and middleware

A raw HTTP or HTTPS server

Start a serverhttp.createServer(fn)server.listen(3000)
Read the requestreq.method, req.url, req.headers
Send the status and bodyres.writeHead(404)res.end('done')The first write sends the headers
Serve HTTPShttps.createServer()Needs a key and a certificate

TipTLS often ends at a proxy, and the app listens with plain http.

TrapFinish every response with res.end(), or the client waits.

Make HTTP requests

Call an API with no packageawait fetch(url)Stable since Node.js 21
Stream the bodyhttp.request(opts, cb)

TipLibraries such as axios add timeouts and interceptors.

Trapfetch() doesn't reject on a 404 or 500. Check response.ok.

Routes and request bodies

Say what Express addsRouting, middleware and res.json() on top of http
Read path and query valuesreq.params.idreq.query.q
Group routes under a prefixapp.use('/api', router)
Match any path after a prefix'/files/*filepath'Express 5 needs the name
Parse JSON into req.bodyexpress.json()
Parse HTML form postsexpress.urlencoded({ extended: true })extended defaults to false in Express 5

TipFile uploads are multipart/form-data, which needs a library such as multer.

TrapIn Express 5, req.body is undefined if no parser matched.

Middleware and app.use

requestloggerauthrouteerrors
the chainnext() goes to the next function. next(err) or a throw jumps to the error handler.
Write a middleware(req, res, next) => next()
Run for every method under a prefixapp.use('/api', fn)
Handle only GET on one pathapp.get('/users', fn)

TipBoth run in the order you register them.

TrapA middleware that neither responds nor calls next() hangs the request.

Handle errors in Express

Handle errors after all routesapp.use((err, req, res, next) => {})
Catch async errors in Express 4fn(req, res, next).catch(next)Express 5 does this for you
Answer an unmatched routeAdd an app.use() before the error handler

TipIf res.headersSent is true, call next(err). Send stack traces only in development.

Paginate an API

Jump to any pageskip = (page - 1) * limitCap limit
Page through changing data{ _id: { $lt: cursor } }Can't jump to any page

TipSort a cursor on a unique key. Fetch limit + 1 to see if a next page exists.

TrapOffsets slow down as they grow, and an insert can show an item twice.

Security & auth

the attacks, the login flow, the layers

Common attacks

Stop SQL and shell injectionUse parameterized queries. Never pass input to exec().
Stop XSS and CSRFEscape output, set a Content Security Policy, use CSRF tokens or SameSite cookies
Explain ReDoSCrafted input makes a regex backtrack and block the loop
Explain prototype pollutionMerging untrusted JSON changes Object.prototype

TipRun npm audit to find insecure dependencies.

Passwords and JWT login

Hash, then check a passwordbcrypt.hash(pw, 10)bcrypt.compare(pw, hash)
Issue a token with an expiryjwt.sign(claims, key, opts)
Check the token per requestjwt.verify(token, key)

TipAuthentication proves who you are. Authorization decides what you may do.

TrapA JWT stays valid until it expires. Sessions are easier to revoke.

Secure an API endpoint

Set protective headersapp.use(helmet())
Limit browser originscors({ origin })
Allow only one roleauthorize(['admin'])Your own middleware, answers 403

TipLayer HTTPS, auth, validation and limits. Cap bodies with express.json({ limit }).

Trapcors() is not access control. Non-browser clients ignore it.

Validate input

Check fields, collect failuresbody('email').isEmail()validationResult(req)
Clean a value as wellbody('name').escape()Encodes HTML to prevent XSS

TipPrefer an allow list. Joi or Zod put the same checks in one object.

TrapValidate on the server too. Any client can send any request.

Rate limit requests

Limit requests per clientrateLimit({ windowMs, max })Answers 429
Avoid a burst at the window edgeUse a sliding window or a token bucket

TipBehind a proxy, set trust proxy so req.ip is the real client.

TrapThe default store is per process. Across instances, count in Redis.

Data, caches & real-time

pools, cache-aside, pushing to clients

Database connections and ORMs

Connect once at startupmongoose.connect(url)Mongoose for MongoDB, Prisma or Sequelize for SQL
Change a production schemaUse migrations, not sequelize.sync()

TipReuse one pool. One connection per request can exhaust the database.

TrapNever join strings into SQL. Use parameterized queries.

Caching

Use cache-asideOn a miss, load from the source and store with a TTL
Cache inside the processnew NodeCache()Fastest, lost on restart
Share a cache across instancessetEx(key, ttl, value)Redis adds a network trip

TrapAn unbounded in-process cache becomes a memory leak.

WebSockets or Server-Sent Events

Run plain WebSocketsnew WebSocketServer()The ws package
Message a Socket.IO roomsocket.to(room).emit()Everyone except the sender
Push a Server-Sent Eventdata: hi\n\nOne-way, over text/event-stream
Listen in the browsernew EventSource(url)Reconnects on its own

TipOver HTTP/1.1, browsers allow six SSE connections per domain.

TrapSocket.IO has its own protocol, so plain WebSocket clients can't connect.

CPU, cores & memory

scale past one thread, keep the heap bounded

Cluster or worker threads

Use every core on one portcluster.fork()Own event loop and memory
Replace a crashed workercluster.on('exit', fn)
Run CPU-heavy code in parallelnew Worker(file)Own V8 and event loop, same process
Send a result backparentPort.postMessage(x)Copies, unless you transfer it

TipReuse worker threads in a pool. They add little for I/O-heavy work.

TrapCluster workers share no memory, so keep sessions in Redis.

Event loop lag and performance

Measure event loop lagmonitorEventLoopDelay()Subtract the resolution
List what blocks the loopCPU loops, Sync APIs, huge JSON, ReDoS
Keep the loop freeWorker threads, a job queue, setImmediate()

TipAlso add indexes, avoid N+1 queries, select only the columns you need.

TrapClustering doesn't fix this. A heavy task still blocks its process.

Test, debug & production

test it, ship it, keep it running

Test and debug

Run the built-in test runnernode --test
Mock a module in a unit testjest.mock('./db')
Test with supertestrequest(app).get('/x').expect(200)
Attach a debuggernode --inspect app.jsnode --inspect-brk app.js-brk pauses on the first line
Record a CPU profilenode --cpu-prof app.js
Turn on namespaced logsDEBUG=app:*

TrapKeep listen() out of the file that exports app, so tests can import it.

Deploy

Run one process per core with PM2pm2 start ecosystem.config.js
Replace processes one at a timepm2 reload all
Install only production packagesnpm ci --omit=dev

TipIn containers, run one process each, and expose /health.

Trappm2 reload needs each process to shut down gracefully.

Graceful shutdown and crashes

Catch the stop signalprocess.on('SIGTERM', stop)
Stop new connectionsserver.close(cb)Then close the database
Force exit before SIGKILLsetTimeout(() => process.exit(1), 10000)
Log a throw nothing caughtprocess.on('uncaughtException', fn)
Log a rejection with no handlerprocess.on('unhandledRejection', fn)Fatal since Node.js 15

TipFail the readiness check first, so traffic stops.

TrapDon't resume after an uncaught exception. Log and exit.

Logs, metrics and traces

Write structured logs to stdoutwinston.format.json()Or pino. Add a request id.
Expose metrics for Prometheusregister.metrics()Alert on errors, p99 latency, loop delay

TipLogs explain a failure, metrics show trends, traces show delays.

TrapStart tracing agents first, because they patch modules on load.

AsyncLocalStorage

Start a store for each requestals.run({ requestId }, next)It follows await and timers
Read it in deep codeals.getStore()undefined outside a context

TrapA rare callback-based library loses it. util.promisify() fixes that.

Found this useful? Pass it on.
Pro · $10/mo

This sheet stays free. With Pro, you can:

  • Every answer on this sheet. Read the full answer to all 59 questions it links, not just the 8 free ones.
  • My sheet. The cards you star and the ones you miss in Quiz me, from every sheet, on one page to quiz and print.
  • PDFs. Download this sheet and My sheet, laid out for A4, with every question a link to its answer.
  • The rest of Pro. Every answer in the question library, and 1,000 AI credits a month.