Skip to content
QSWEQB

Node.js and TypeScript on the server

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 areas underneath

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.

SubsectionWhat it is for
Event Loop & AsyncThe execution model everything else follows from
Node Runtime & ModulesModules, processes, threads, buffers and memory
Express & NestJSStructuring a service beyond a single file
TypeScript for ServicesModelling a domain in types, not just annotating it
Testing & ToolingTest 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.

flowchart TD
    A[Request arrives] --> B{Is the work CPU bound}
    B -->|No| C[Await IO, loop stays free]
    B -->|Yes, small| D[Do it inline and accept the stall]
    B -->|Yes, large| E[Worker thread pool]
    B -->|Yes and slow| F[Queue and a separate consumer]
    E --> G[Result returned to the loop]
    F --> G

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.

flowchart TD
    A[Timers] --> B[Pending callbacks]
    B --> C[Poll for IO]
    C --> D[Check, setImmediate]
    D --> E[Close callbacks]
    E --> F[Microtasks drained]
    F --> A

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.

RoleWhere their time goesThe failure they own
Backend engineerHandlers, queries, integrationsLatency and memory growth
Full-stack engineerOne codebase spanning both sidesLeaking server code to the client
Platform / tooling engineerBuilds, types, releasesA build nobody can wait for
Serverless developerIndividual handlersCold starts and bundle size
Real-time engineerSockets and event fan-outBackpressure 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:

import { z } from "zod";

const Order = 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.
type Order = z.infer<typeof Order>;

export function handle(body: unknown) {
  const order = Order.parse(body);   // throws here, not three layers down
  return order.quantity;
}

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.

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.
export async function copyBuffered(src, dst) {
  const data = await readFile(src);
  await writeFile(dst, data);
}

// Streams it in chunks. pipeline propagates errors AND respects backpressure:
// the read pauses whenever the write says its buffer is full.
export async function copyStreamed(src, dst) {
  await pipeline(fs.createReadStream(src), fs.createWriteStream(dst));
}

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.

mkdir node-loop && cd node-loop && npm init -y
npm pkg set type=module

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.

Where to go next

Now practise it

10 interview questions in Node.js & TypeScript, each with the rubric the interviewer is scoring against.

All Node.js questions
nodejstypescriptevent-loopbackendjavascript