Node.js runs JavaScript on the server on a single-threaded event loop, and TypeScript is now how almost everyone writes it. The draw is one language across the whole stack; the price is that CPU-bound work has to be deliberately pushed somewhere else.
Very high demand20 min readBackend engineer (Node.js), Full-stack engineer (TypeScript), API engineer, Serverless and functions developer, Build, tooling and developer-experience engineer, Real-time systems engineerUpdated 2026-07-27
Assumes you know: JavaScript fundamentals, particularly closures, promises and prototypes, HTTP, status codes and what a REST endpoint is, Enough SQL to read and write a join, Comfort with npm and a terminal
Overview
What this area actually covers
Node.js is a server-side runtime built on V8, Chrome's JavaScript engine, wrapped
in libuv, a C library that provides asynchronous file and network input/output. It
gives you one thread running your JavaScript, a queue of completed operations
waiting to have their callbacks run, and a small pool of background threads for the
things the operating system cannot do asynchronously. TypeScript is a superset of
JavaScript that adds a static type system, checks it at compile time, and then
erases it entirely — the types do not exist at runtime.
The area therefore covers three things that are genuinely distinct. First the
runtime: the event loop, streams, buffers, modules, processes and memory limits.
Second the type system: structural typing, generics, narrowing, and how much of
your domain you can encode in types. Third the ecosystem you build services out
of, which in practice means Express or Fastify or NestJS, an ORM such as Prisma or
Drizzle or TypeORM, a validation library such as Zod, and a test runner.
The relationship between the first two is the thing to understand before anything
else, because it explains most of the confusion in this ecosystem. TypeScript knows
nothing about Node and Node knows nothing about TypeScript. One is a compile-time
checker that produces JavaScript; the other is a runtime that executes JavaScript.
Everything awkward about the tooling — the build step, the source maps, the
declaration files, the mismatch between what the checker believes and what arrives
over the network — follows from those two facts sitting next to each other without
a connection.
What people wrongly bundle in is the browser. Knowing React does not mean knowing
Node, and the overlap is smaller than the shared syntax suggests: no DOM, no
bundle-size anxiety, but instead process lifetime, connection pools, file
descriptors and a heap that has to survive for weeks. The other thing wrongly
bundled in is npm as a skill. Everyone can install a package. Understanding what
happens when two packages want different versions of the same transitive
dependency is a different matter.
The five subsections split into two about the runtime, one about the type system,
one about the frameworks, and one about the surrounding practice. If you are
preparing for an interview, the event loop is not optional and is not
substitutable: nearly every other question in a Node loop turns out to depend on
it.
Subsection
What it is for
Event Loop & Async
The execution model everything else follows from
Node Runtime & Modules
Modules, processes, threads, buffers and memory
Express & NestJS
Structuring a service beyond a single file
TypeScript for Services
Modelling a domain in types, not just annotating it
Testing & Tooling
Test runners, mocking, builds and monorepos
Event Loop & Async
Phases and their order, microtasks versus macrotasks, process.nextTick and where
it sits, promises and async/await, blocking the loop and detecting it, and
streams with backpressure. This is the foundational subsection and the one that
produces the most interview questions, because it is the model that makes Node's
behaviour predictable rather than mysterious. It exists separately from the runtime
subsection because it is conceptual rather than API knowledge: you can know every
stream method and still not be able to say why your health check timed out. What
you will find here is a small number of ideas with a very large number of
consequences.
Node Runtime & Modules
CommonJS and ES modules and the friction between them, worker threads, clustering
and process management, buffers and binary data, and the memory limits a long-lived
Node process runs into. It is a separate area because it is about the process rather
than about your code's control flow: how many of these should run in a container,
what happens when one is asked to shut down, where the heap ceiling is and what
happens when you approach it. This is also where the module question lives, which is
the single most persistent source of practical pain in the ecosystem and comes up in
interviews as a proxy for how much real Node someone has shipped.
Express & NestJS
Middleware ordering, error handling and the specific ways it goes wrong,
dependency injection in Nest, request validation, and structuring an application
that has outgrown a single file. Two frameworks with opposite philosophies sit in
one subsection deliberately: Express is minimal and leaves architecture entirely to
you, while NestJS imposes a module-and-decorator structure familiar to anyone from
Angular or Spring. The comparison is the lesson. Express questions tend to be about
what you built on top of nothing; Nest questions tend to be about whether you
understand the machinery you were handed.
TypeScript for Services
Structural typing and why it differs from the nominal typing most languages use,
generics, narrowing and control-flow analysis, discriminated unions, and encoding
domain constraints so that invalid states become uncompilable rather than merely
discouraged. This is its own subsection because the interview expectation has
shifted: annotating a parameter is table stakes, and what gets graded now is whether
you can design types that make a whole class of bug impossible. Expect questions
about modelling something with several mutually exclusive shapes, about the boundary
where untrusted data enters, and about unknown versus any.
Testing & Tooling
Jest and Vitest and the built-in runner, mocking modules and why that is harder in
ES modules than it was in CommonJS, build and bundling choices, type-checking in
continuous integration, and monorepo workflows. It exists as its own area because in
this ecosystem the tooling genuinely is a discipline — a large TypeScript monorepo
has build performance problems, dependency-graph problems and versioning problems
that are nobody's side task. Anyone who has been the person who made the build fast
again knows this is a real job, and this subsection is where those questions live.
Where it sits in a real system
Node sits where any application server sits — behind a load balancer or an API
gateway, in front of a database and a cache, emitting events to a queue — but two
of its properties change the shape of what gets built around it.
The first is that it is exceptionally good at holding many concurrent connections
that are mostly waiting. One Node process handling ten thousand open WebSockets is
unremarkable, because each idle connection costs a file descriptor and a little
heap rather than a thread. That is why Node concentrates in API gateways,
backend-for-frontend layers, real-time features, streaming proxies and
orchestration services that fan out to other services and stitch the answers back
together.
The second is that everything shares one thread, so a single slow synchronous
operation stalls every other request in the process. A JSON payload that is fifty
megabytes, a regular expression that backtracks catastrophically, a synchronous
crypto call, a tight loop over a large array: any of these freezes the whole
server, not just the request that caused it. This is why Node deployments are
almost always horizontally scaled to several processes, and why the interesting
architectural decisions are about where the CPU-heavy work goes — a worker thread,
a separate service, or a queue consumed by something else entirely.
Loading diagram…
The branch worth arguing about in an interview is the third one: worker threads
solve the stall but not the throughput, because the work still happens on the same
machine competing for the same cores.
The event loop in one picture
Nearly every serious Node question reduces to the event loop, so it is worth being
precise about what it is. The loop runs a fixed sequence of phases, over and over.
Each phase has a queue of callbacks; the loop enters a phase, drains its queue, and
moves on. Between callbacks it drains the microtask queue, which is where promise
continuations live, and process.nextTick callbacks run ahead of even those.
Loading diagram…
The thing to take from this is not the phase names, which nobody needs to recite,
but the shape: it is one loop on one thread, and every callback in it runs to
completion before the next one starts. Your code is never interrupted mid-function,
which removes an entire category of concurrency bug — there are no data races
between two of your own functions — and creates a different one, because nothing
else can happen while a function is running.
Three consequences follow directly and are worth stating as rules. A synchronous
operation of any duration delays everything, including the health check that decides
whether your container is still alive. await inside a loop serialises what looks
like it should be parallel, because each iteration waits before the next begins;
gathering the promises and awaiting them together is a different program with a
different latency. And an unbounded microtask chain can starve the loop entirely,
because microtasks are drained to exhaustion before the loop advances, so a promise
that schedules another promise forever prevents any I/O from ever being processed.
Who does this work
Backend engineers on Node teams write HTTP handlers, database access and
integrations, and spend their difficult days on latency, memory growth and
misbehaving third-party clients. Full-stack engineers are the largest group by
headcount, working in a single TypeScript codebase with Next.js or a similar
framework where the boundary between server and browser is a function call away
and easy to cross by accident. Platform and tooling engineers own the build,
the monorepo, the type-checking pipeline and the release process; in a large
TypeScript codebase this is a full-time job and an undervalued one. Serverless
developers write handlers deployed as functions, where cold starts, bundle size
and the absence of a long-lived process change most of the usual advice.
Role
Where their time goes
The failure they own
Backend engineer
Handlers, queries, integrations
Latency and memory growth
Full-stack engineer
One codebase spanning both sides
Leaking server code to the client
Platform / tooling engineer
Builds, types, releases
A build nobody can wait for
Serverless developer
Individual handlers
Cold starts and bundle size
Real-time engineer
Sockets and event fan-out
Backpressure and dropped connections
The people who operate Node in production — SREs, platform teams — are the ones
who discover its failure modes first. Event-loop lag, heap growth towards the V8
old-space limit, and file-descriptor exhaustion are their vocabulary long before
they become the application team's.
Dependencies and the boundary where types stop
Two practical concerns come up in almost every Node interview and belong together,
because both are about trusting things you did not write.
The first is the dependency graph. Node's ecosystem favours small packages, so a
modest service routinely has hundreds of transitive dependencies, and the lockfile
that pins them is one of the most important files in the repository. It is what makes
a build reproducible, what a security scanner reads, and what tells you why two
versions of the same library are installed side by side. Understanding that the
package manager can and will install several copies of one package at different
points in the tree — and that this is a deliberate design choice rather than a bug —
is the answer to a surprising number of confusing runtime problems, including
instanceof checks failing against a class from a package that is present twice.
The second is validation. TypeScript checks the code you wrote and stops at the
process boundary, so anything arriving from a network call, a database row, a
configuration file or a message queue is typed by assertion rather than by
verification. The mature pattern is to parse untrusted input into a validated shape
once, at the edge, and to derive the static type from that validator rather than
declaring it twice:
The detail that matters is the unknown parameter. Typing the incoming body as
unknown rather than as Order forces the parse to happen, because there is nothing
useful you can do with an unknown until you have narrowed it. Typing it as any
would compile identically and prove nothing, which is the whole argument for why
strict configuration is worth the friction it causes.
Demand, adoption and how that is changing
Demand is very high, and the honest reason is not technical merit. It is that one
language across the stack is an organisational win. A team can hire from one pool,
share validation logic and types between the API and the client, move an engineer
between the two without retraining, and run one build toolchain. For a startup or
a product team, that compounds. Companies rarely choose Node because they
benchmarked it; they choose it because the frontend was already JavaScript and
duplicating the domain model in a second language was worse.
The change worth knowing about is that TypeScript stopped being an option. A new
Node service written in plain JavaScript in 2026 looks like a deliberate and
slightly odd choice, and most published libraries ship type definitions as a
matter of course. That has moved the interview: candidates are now expected to
model a domain in types rather than merely annotate parameters, and "we use
TypeScript but with any everywhere" is a recognisable smell an interviewer will
probe for. It has also raised the floor for hiring. The type system filters out a
class of candidate who could previously ship working JavaScript without a clear
model of their own data, and it means a job advert for a Node role is in practice a
TypeScript role.
Two smaller shifts matter. Node itself can now execute TypeScript files directly by
stripping the annotations, unflagged since Node 23.6 — it removes types rather than
checking them, so tsc still has a job. And the runtime is no longer alone: Deno
and Bun are credible alternatives with better out-of-the-box ergonomics, and while
neither has displaced Node in enterprises, being unable to say anything about them
reads as incuriosity.
A third shift is structural rather than technical and matters more for careers. The
frameworks that dominate hiring have moved up a level: a great deal of what used to
be a separate Node API service is now written inside a full-stack framework, where
server code and client code live in the same file tree and the boundary between them
is a convention rather than a network hop. That is genuinely productive and it
produces a specific new failure — code intended for the server ending up in the
browser bundle, taking a secret with it — which is why "how do you know this runs on
the server" has become a reasonable interview question with a non-obvious answer.
What makes it hard
The conceptual leap is the event loop, described above, and specifically the
consequences rather than the phase order. Once you internalise that your code runs
to completion on one thread between callbacks, the rest follows: why a for loop
over a million records makes health checks time out, why await inside a loop
serialises what you meant to parallelise, why process.nextTick starves timers,
why an unhandled promise rejection terminates the process.
The second hard thing is backpressure, and it is where the maturity gap shows.
Anyone can write an Express route. Far fewer can say what happens when a
downstream consumer reads slower than an upstream producer writes. Ignore the
return value of writable.write(), or pipe a fast source into a slow sink without
pipeline, and Node buffers the difference in memory until the process dies — with
no error, no failing test, and a graph that only bends under real load.
The third is process management, which nobody teaches. How many processes per
container, what happens to in-flight requests on SIGTERM, whether cluster or
your orchestrator does the scaling, how you set the heap limit, what restarts you
on a crash. These are the questions that separate someone who has run Node from
someone who has written it.
Memory deserves a mention alongside it, because a long-lived Node process fails
differently from a request-scoped one. V8 has a heap ceiling, and a process that
approaches it spends progressively more time collecting garbage before it eventually
dies, so the symptom is a slow degradation rather than a clean crash. The usual cause
is not a subtle engine problem but something ordinary held longer than intended: a
module-level cache with no eviction, an array of every request for a metric nobody
reads, a listener registered per connection and never removed, or a closure that
captured a large buffer it only needed one field from. Taking two heap snapshots
minutes apart and comparing what grew between them is the standard diagnosis, and
being able to describe that procedure is a reliable senior signal.
The fourth is modules, and it is the one that makes experienced engineers
audibly tired. Node began with CommonJS, where require is a synchronous function
call that can appear anywhere and returns whatever the module assigned to
module.exports. JavaScript later gained a standard module system, ES modules,
where import is a static declaration resolved before execution, which enables
top-level await and static analysis and cannot be called conditionally in the
same way. Both exist, both are in wide use, and libraries have to decide which to
publish — often both, which creates the possibility of the same package being
loaded twice in one process under two identities. Add TypeScript's own settings for
how it emits and resolves modules, and the resulting configuration space is the
single most common reason a Node project will not start for a new joiner.
And the honest weakness: CPU-bound work. Node is a poor choice for image
processing, large-scale numerical work or heavy cryptography in the request path,
and pretending otherwise in an interview is worse than conceding it. The mature
answers are to move the work off the loop into a worker_threads pool, push it to
a queue and a dedicated consumer, delegate to a native addon that releases the
thread, or accept that this particular service should be written in Go or Rust.
Knowing which of those applies, and why, is a senior signal.
The last difficulty is TypeScript's own, and it is a difficulty of judgement rather
than knowledge. The type system is expressive enough that you can encode almost
anything in it, and a codebase that does becomes unreadable, slow to compile and
impossible for a new joiner to modify. Knowing when a discriminated union is
genuinely making invalid states unrepresentable and when a conditional type is
showing off is a taste that takes time to acquire, and interviewers who have
maintained a large TypeScript codebase probe for it deliberately.
Why study it
Study Node if you want to build and ship whole products with the least context
switching, if the work you want is in product engineering rather than
infrastructure, or if you already know JavaScript and want the shortest honest path
to backend work. It is also the pragmatic choice for anything involving many
concurrent connections and little computation, which is a large share of modern
web plumbing.
The type-system argument deserves its own line, because TypeScript is one of the
better places to learn what a type system is for. It has structural typing, so
compatibility is decided by shape rather than by declared inheritance; it has
control-flow narrowing, so the checker tracks what you have already proved about a
value; and it has unions and intersections that let you describe data whose shape
depends on a field. Learning to think in those terms is portable, and it makes the
type systems you meet later feel like variations rather than novelties.
There is a career argument too, and it is about breadth rather than depth. Because
one language covers the browser, the server, the build tooling and increasingly the
edge runtime, a Node engineer can plausibly own a feature from the database to the
rendered pixel. That is unusual, it is genuinely valuable in a small team, and it is
the reason so many people who describe themselves as full-stack are working in this
ecosystem. The corresponding risk is that breadth without a deep area is easy to
hire and easy to replace, so the useful move is to pick one of the hard parts — the
runtime, the type system, or the build — and be the person who actually understands
it.
Do not study it if you want to learn how computers work — the runtime is designed
to hide the machine. Do not choose it if your target is data engineering or
machine learning, where the ecosystem is Python's and Node has nothing to offer.
And be aware that because the entry barrier is low, the junior end of this market
is crowded; the differentiator is the runtime knowledge described above, not
another framework on the CV.
Your first hour
Prove the single-thread claim to yourself rather than reading about it.
Write a server with two routes: /fast returning immediately, and /slow running
a synchronous loop that sums to five hundred million. Start it, then in one
terminal request /slow and immediately in another request /fast. Time both.
/fast will wait for /slow to finish, and seeing that number yourself is worth
more than any diagram.
Then change /slow to do the same work inside a new Worker() from
node:worker_threads and repeat the experiment. Finally add
console.log(process.memoryUsage().heapUsed) on each request and watch what a
retained array does across a hundred calls.
If you want a second exercise that pays off in interviews, take the same server and
add a route that fetches three things from a slow endpoint. Write it first with
three awaits in sequence and time it; then rewrite it to start all three and await
them together, and time it again. The difference is not a micro-optimisation, it is
the difference between one round trip and three, and being able to spot that pattern
in someone else's code is a genuinely common review responsibility.
The artefact is one small server plus four numbers you can explain: latency of
/fast alone, /fast behind blocking /slow, /fast behind threaded /slow,
and heap used before and after. That is the substance of most Node interviews,
demonstrated rather than asserted.
What this is not
Node is not single-threaded in the sense people usually mean. Your JavaScript runs
on one thread, but the process has several: libuv keeps a pool of background
threads, four by default and configurable through UV_THREADPOOL_SIZE, which is
where file operations, DNS lookups and some cryptographic calls actually happen.
"Non-blocking" describes the interface, not the absence of threads.
Node is not asynchronous by default either. It is asynchronous where you asked for
it. fs.readFileSync, JSON.parse, zlib.gzipSync and a slow regular expression
are all synchronous, and a codebase with await everywhere can still be blocking
throughout.
TypeScript is not a runtime guarantee. It is erased before execution, so a JSON
response typed as User is only a User because you believed the annotation.
Validating data at the boundary with something like Zod is not redundant — it is
the part TypeScript deliberately cannot do. And TypeScript is not sound: any,
type assertions, non-null assertions and unchecked index access all exist, some by
default, and the strictness of a codebase is a configuration choice rather than a
property of the language.
A framework is also not an architecture. Express in particular gives you routing and
middleware and nothing else, which means every decision about where the business
logic lives, how the database is reached and what a module owns is yours to make and
yours to defend. Candidates who describe their architecture as "Express" have
answered a question about a library when they were asked a question about a system,
and it is one of the easiest distinctions for an interviewer to draw.
Node is also not a monolithic product with a release you either have or do not.
Long-term-support lines run for years, the runtime, the package manager and the
compiler all version independently, and "we are on Node" tells a colleague almost
nothing. As with the module question, the useful habit is to ask which versions of
which pieces, because the answer determines what half the advice you will read
online means for you.
One language across the stack is the real reason Node is everywhere, and the
event loop is the real reason it succeeds or fails. Everything else in a Node
interview is downstream of those two facts.
import { z } from"zod";
constOrder = z.object({
id: z.string().uuid(),
quantity: z.number().int().positive(),
});
// The type is derived from the validator, so the two can never drift apart.typeOrder = z.infer<typeofOrder>;
exportfunctionhandle(body: unknown) {
const order = Order.parse(body); // throws here, not three layers downreturn order.quantity;
}
import fs from"node:fs";
import { readFile, writeFile } from"node:fs/promises";
import { pipeline } from"node:stream/promises";
// Reads the entire file into the heap before writing anything.// A 2 GB file is a 2 GB heap spike, and V8's old-space limit is finite.exportasyncfunctioncopyBuffered(src, dst) {
const data = awaitreadFile(src);
awaitwriteFile(dst, data);
}
// Streams it in chunks. pipeline propagates errors AND respects backpressure:// the read pauses whenever the write says its buffer is full.exportasyncfunctioncopyStreamed(src, dst) {
awaitpipeline(fs.createReadStream(src), fs.createWriteStream(dst));
}