Skip to content
RED SAGSkills

Node.js interview questions and answers

Node.js questions cover how JavaScript runs on the server: the event loop, async I/O, modules and building reliable APIs.

Below are 41 Node.js questions with the correct answer and a short explanation, grouped from fresher to experienced level. Try each one before you open the answer.

What Node.js interviews usually check

  • ✓The event loop and non-blocking I/O
  • ✓Promises, async/await and error handling
  • ✓Modules, npm and package versions
  • ✓Express middleware and routing
  • ✓Streams, performance and scaling

Node.js interview questions for freshers easy

1. Installing a test-only package

You want to add Vitest to a project. It is only needed during development and testing, not in production code. Which command is correct?

  1. A.npm install --global vitest
  2. B.npm install -D vitest
  3. C.npm run vitest
  4. D.npm install --save-exact --prod vitest
Show answer

Answer: B. npm install -D vitest

npm install -D (short for --save-dev) installs the package and records it under devDependencies in package.json. Build tools, linters and test runners belong there. A global install would not be recorded in the project, so teammates and CI would not get it.

2. What ~2.4.1 allows

In package.json, which versions does "~2.4.1" accept?

  1. A.2.4.1 up to, not including, 3.0.0
  2. B.Only exactly version 2.4.1
  3. C.2.4.1 up to, not including, 2.5.0
  4. D.Any version at all from 2.0.0
Show answer

Answer: C. 2.4.1 up to, not including, 2.5.0

The tilde allows patch updates only when a minor version is given: >=2.4.1 <2.5.0. The caret would allow minor updates too, up to <3.0.0.

3. npm ci in pipelines

Why use npm ci instead of npm install in a CI pipeline?

  1. A.It installs exactly what the lockfile says, from clean
  2. B.It updates every package to the newest version
  3. C.It skips installing all devDependencies
  4. D.It works without a package.json file
Show answer

Answer: A. It installs exactly what the lockfile says, from clean

npm ci deletes node_modules and installs the exact tree in package-lock.json, failing if the lockfile and package.json disagree. Builds become repeatable and usually faster.

4. Committing the lockfile

Should package-lock.json be committed to Git for an application?

  1. A.No, it is a temporary file made by npm
  2. B.Only on the main branch, not on others
  3. C.Only when the app has no dependencies
  4. D.Yes, so every install gets the same versions
Show answer

Answer: D. Yes, so every install gets the same versions

The lockfile records the exact resolved version of every direct and nested package. Committing it means teammates, CI and production install the same tree and avoids "works on my machine" bugs.

5. What npx does

What does running npx create-vite my-app do?

  1. A.Adds create-vite to the project dependencies
  2. B.Publishes my-app to the npm registry
  3. C.Updates npm itself to the latest version
  4. D.Runs the package's command, downloading it if needed
Show answer

Answer: D. Runs the package's command, downloading it if needed

npx runs a binary from a package, using a local copy if present or fetching it temporarily. It is handy for one-off tools such as project creators without installing them globally.

6. What a 504 from Nginx means

Users see "504 Gateway Timeout" from Nginx, which sits in front of your Node.js app. What does it tell you?

  1. A.The client sent a malformed request
  2. B.The TLS certificate on the proxy has expired, so Nginx refuses to forward the request to the app
  3. C.The client is not authenticated
  4. D.Nginx did not receive a response from the upstream app server in time
Show answer

Answer: D. Nginx did not receive a response from the upstream app server in time

A 504 is produced by a gateway or proxy when the upstream server does not answer within the proxy timeout. It points at a slow or stuck app, a slow database query, or a wrong upstream address. A 502 Bad Gateway, by contrast, usually means the upstream returned an invalid response or refused the connection.

7. Promise and setTimeout order

What does this Node.js code print?

console.log('A')
setTimeout(() => console.log('B'), 0)
Promise.resolve().then(() => console.log('C'))
console.log('D')
  1. A.A B C D
  2. B.A D B C
  3. C.A C D B
  4. D.A D C B
Show answer

Answer: D. A D C B

Synchronous code runs first, printing A and D. When the call stack is empty, Node runs all queued microtasks (the promise callback, C) before moving to the timers phase, where the setTimeout callback prints B. A 0 ms timer never jumps ahead of synchronous code or microtasks.

8. setTimeout as a job queue

A Node.js API schedules follow-up emails with setTimeout(sendEmail, 24 * 3600 * 1000). What is the main problem?

  1. A.setTimeout cannot hold delays longer than an hour
  2. B.The email will be sent once per CPU core
  3. C.It blocks the event loop for 24 hours
  4. D.Pending emails are lost when the process restarts
Show answer

Answer: D. Pending emails are lost when the process restarts

Timers live in process memory, so a deploy, crash or scale-in loses every pending email. A persistent job queue or scheduler stores the job and runs it later on any worker. Also, Node timers above about 24.8 days (2^31 - 1 ms) fire immediately, but the restart issue matters first.

9. Cost of Array.shift in a queue

A Node.js worker uses an array as a queue with push() to add and shift() to remove. With 100,000 items waiting, why can this get slow?

  1. A.shift() re-indexes every remaining item, O(n)
  2. B.push() copies the whole array on each call
  3. C.Arrays cannot hold more than 65,535 items
  4. D.shift() sorts the array before it removes
Show answer

Answer: A. shift() re-indexes every remaining item, O(n)

Removing from the front means every other element gets a new index, so shift is O(n) in general. For large queues, use a ring buffer or keep a head index and only compact sometimes, so removals are O(1).

10. Empty req.body in Express

The server uses express.json(), but req.body is empty when this request arrives. What is the simplest fix on the client?

await fetch('/api/todos', {
  method: 'POST',
  body: JSON.stringify({ title: 'Buy milk' }),
})

// server
app.use(express.json())
app.post('/api/todos', (req, res) => {
  console.log(req.body) // empty, no title
})
  1. A.Change the method from POST to PUT
  2. B.Send the body as FormData instead of a string
  3. C.Add headers: { 'Content-Type': 'application/json' } to the fetch call
  4. D.Call JSON.stringify twice so the body is still valid JSON after the server parses it once
Show answer

Answer: C. Add headers: { 'Content-Type': 'application/json' } to the fetch call

When fetch gets a string body it sends Content-Type: text/plain, and express.json() only parses requests whose Content-Type is JSON. Adding the JSON content type lets the parser fill req.body. FormData would need a different parser, and the method has nothing to do with body parsing.

11. try/catch around an async call

Running this in Node.js, what happens?

async function load() {
  throw new Error('boom')
}

try {
  load()
} catch (e) {
  console.log('caught')
}
console.log('done')
  1. A.'caught' then 'done'
  2. B.'caught' only
  3. C.'done', then the process reports an unhandled rejection
  4. D.Nothing is printed, because the rejected promise is silently ignored
Show answer

Answer: C. 'done', then the process reports an unhandled rejection

Calling an async function never throws synchronously; it returns a rejected promise. Because the promise is not awaited, try/catch never sees the error, done prints, and the rejection is unhandled (which crashes modern Node by default). Writing await load() inside the try fixes it.

12. Generating a reset token

Which is the right way to make an unguessable reset token in Node.js?

  1. A.crypto.randomBytes(32).toString('hex')
  2. B.Math.random().toString(36).slice(2)
  3. C.Date.now().toString(16) + user.id
  4. D.md5(user.email + 'reset')
Show answer

Answer: A. crypto.randomBytes(32).toString('hex')

Math.random is not cryptographically secure and its output can be predicted; timestamps and hashes of known data are guessable too. crypto.randomBytes (or crypto.randomUUID / getRandomValues in browsers) uses the OS secure generator.

13. Speeding up mobile CI builds

A CI pipeline for a cross-platform app spends 12 minutes downloading Gradle, CocoaPods and npm or pub packages on each run. What is the first thing to try?

  1. A.Cache dependency folders keyed on the lock files
  2. B.Run the build twice so the second run is faster
  3. C.Delete the lock files so resolution is quicker
  4. D.Build only on Fridays to save CI minutes
Show answer

Answer: A. Cache dependency folders keyed on the lock files

CI caches restore folders such as ~/.gradle, ~/.pub-cache, node_modules or Pods between runs. Keying the cache on lock files means it is reused until dependencies change. This often cuts several minutes from every build.

14. A YAML-based load tool

Which open-source load tool describes phases and scenarios mainly in YAML files and runs on Node.js?

  1. A.Artillery
  2. B.JMeter
  3. C.Locust
  4. D.Gatling
Show answer

Answer: A. Artillery

Artillery uses YAML for test configuration (phases, arrival rates, flows) with JavaScript hooks for custom logic. JMeter uses XML plans, Locust uses Python and Gatling uses a JVM DSL. The best tool is usually the one your team can script and maintain easily.

Node.js intermediate interview questions medium

15. __dirname in an ES module

A Node.js script was converted to ESM ("type": "module"). It now fails with "__dirname is not defined". What is the modern fix?

  1. A.Call require("path").dirname() with no arguments
  2. B.Use import.meta.dirname (or derive it from import.meta.url)
  3. C.Declare var __dirname = "." at the top of the file
  4. D.Rename the file to .ts so Node adds __dirname
Show answer

Answer: B. Use import.meta.dirname (or derive it from import.meta.url)

__dirname and __filename are CommonJS wrapper variables and do not exist in ES modules. Node provides import.meta.dirname and import.meta.filename (Node 20.11+), or you can use fileURLToPath(new URL(".", import.meta.url)).

16. Two copies of one library

The bundle analyzer shows two copies of date-fns, versions 2.x and 3.x. What is the likely cause?

  1. A.The bundler copies every package twice
  2. B.The lockfile was committed to Git
  3. C.date-fns is listed in devDependencies
  4. D.Different packages depend on incompatible ranges
Show answer

Answer: D. Different packages depend on incompatible ranges

If your app needs date-fns 3 and another dependency needs ^2, npm installs both, and both may be bundled. You can upgrade the other package, use overrides if compatible, or check with npm ls date-fns.

17. Finding why a package is installed

Which command shows which dependency pulled a package into node_modules?

  1. A.npm audit lodash
  2. B.npm explain lodash
  3. C.npm outdated lodash
  4. D.npm pack lodash
Show answer

Answer: B. npm explain lodash

npm explain (or npm why) prints the dependency path from your package.json down to that package. npm ls lodash shows a tree as well. This is the first step before removing or deduping a heavy package.

18. Does devDependencies keep code out?

A library is in devDependencies but is imported in a frontend component. Will it be in the production bundle?

  1. A.No; devDependencies are always left out
  2. B.Only if NODE_ENV is set to development
  3. C.Only if the library has no default export
  4. D.Yes; the bundler includes whatever the code imports
Show answer

Answer: D. Yes; the bundler includes whatever the code imports

For frontend apps, bundlers follow import statements, not dependency categories. The split mainly matters for what npm installs (for example with --omit=dev) and for published packages.

19. A 64-bit id changes after parsing

An upstream service sends a 64-bit numeric id. What does this Node.js code print?

const body = '{"id": 9007199254740993}'
console.log(JSON.parse(body).id)
  1. A.9007199254740993
  2. B.9007199254740992
  3. C."9007199254740993"
  4. D.SyntaxError: number too large
Show answer

Answer: B. 9007199254740992

JavaScript numbers are 64-bit floats and can represent integers exactly only up to 2^53 - 1 (9007199254740991). Larger values are rounded to the nearest representable number, so the id silently becomes ...992 and may point to the wrong record. APIs should send large ids, such as Snowflake or bigint keys, as strings.

20. Webhook signature never matches

You verify a payment provider's webhook signature like this, using the correct secret, but verification always fails. Why?

app.post('/webhooks/pay', express.json(), (req, res) => {
  const expected = crypto
    .createHmac('sha256', process.env.WEBHOOK_SECRET)
    .update(JSON.stringify(req.body))
    .digest('hex')
  if (expected !== req.get('X-Signature')) return res.sendStatus(400)
  // ...
})
  1. A.The signature covers the exact raw request bytes, and re-serialising the parsed JSON changes them
  2. B.HMAC-SHA256 cannot be computed with Node.js crypto
  3. C.The secret must be base64-decoded twice before use
  4. D.The HMAC must be computed over the object with sorted keys, so the code should sort req.body keys before stringifying
Show answer

Answer: A. The signature covers the exact raw request bytes, and re-serialising the parsed JSON changes them

JSON.stringify of the parsed body rarely reproduces the original bytes, so the HMAC differs. Capture the raw body (for example express.raw() on that route) and sign that. Also compare with crypto.timingSafeEqual and check the timestamp to block replays.

21. Pool runs dry after some errors

After a few failed queries, every request hangs waiting for a database connection. What is the bug?

const client = await pool.connect()
const res = await client.query(sql, params)
client.release()
return res.rows
  1. A.The pool must be created inside each request
  2. B.client.release must be called before the query
  3. C.If query throws, release never runs; put it in finally
  4. D.pool.connect returns a new database every time
Show answer

Answer: C. If query throws, release never runs; put it in finally

When client.query throws, execution skips client.release(), and that connection is never returned. After a few errors the pool is empty and new requests wait forever. Use try/finally, or pool.query for single statements, which releases automatically.

22. Algorithm behind Python and V8 sort

Which algorithm do CPython's list.sort() and the V8 engine's Array.prototype.sort (Chrome, Node.js) use?

  1. A.Timsort, a stable merge and insertion sort hybrid
  2. B.Introsort, an unstable quick and heap sort hybrid
  3. C.Plain heap sort, for its O(1) memory
  4. D.Randomised quick sort with three-way partitioning
Show answer

Answer: A. Timsort, a stable merge and insertion sort hybrid

CPython has used Timsort for many years, and V8 switched to Timsort in 2018. Timsort finds existing sorted runs and merges them, so it is very fast on partly sorted real data and it is stable.

23. Promise callback versus setTimeout order

In Node.js or a browser, what does this code print?

console.log(1)
setTimeout(() => console.log(2), 0)
Promise.resolve().then(() => console.log(3))
console.log(4)
  1. A.1 4 3 2
  2. B.1 2 3 4
  3. C.1 4 2 3
  4. D.1 3 4 2
Show answer

Answer: A. 1 4 3 2

The synchronous code prints 1 and 4 first. Then the microtask queue, which holds promise callbacks, is fully drained before the next task, so 3 prints. The setTimeout callback waits in the task (macrotask) queue and prints last.

24. Webhook signature always fails

Stripe webhook verification fails for every event, even though the signing secret is correct. The app registers app.use(express.json()) before all routes. What is the cause?

  1. A.Stripe webhooks must be received over plain HTTP, because TLS changes the signed payload
  2. B.The signature covers the raw bytes, which parsing and re-serializing change
  3. C.Webhook secrets expire every hour
  4. D.Express cannot read headers on POST requests
Show answer

Answer: B. The signature covers the raw bytes, which parsing and re-serializing change

Signature checks hash the exact bytes that were sent. Once express.json() parses the body, you no longer have those bytes, and JSON.stringify will not reproduce them exactly. Register the webhook route with express.raw({ type: "application/json" }) before the JSON parser and pass that buffer to the verify function.

25. One report slows every request

An Express API has a route that builds a large report in a synchronous loop taking 3 seconds. While it runs, even the health check times out. Why?

  1. A.Express handles only one route per file
  2. B.The database connection pool is too small
  3. C.Node runs JavaScript on one main thread, so the loop blocks the event loop for everyone
  4. D.The health check route is missing async
Show answer

Answer: C. Node runs JavaScript on one main thread, so the loop blocks the event loop for everyone

While synchronous code runs, Node cannot pick up any other callback, so all requests on that process wait. Move CPU-heavy work to worker_threads or a background job queue and return a job ID or stream the result. This is also why you should use the async versions of functions like bcrypt.hash.

26. Matrix jobs cancelled early

A workflow tests on Node 20, 22 and 24 using a matrix. When the Node 20 job fails, the other two are cancelled, hiding whether they pass. Why?

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node: [20, 22, 24]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: "${{ matrix.node }}" }
      - run: npm ci && npm test
  1. A.All matrix jobs share one runner, so one failure stops the whole runner.
  2. B.actions/setup-node cancels the other Node versions when one of them fails.
  3. C.fail-fast defaults to true, cancelling the other matrix jobs; set fail-fast: false.
  4. D.You must add needs: between matrix entries so they run independently.
Show answer

Answer: C. fail-fast defaults to true, cancelling the other matrix jobs; set fail-fast: false.

GitHub Actions cancels in-progress and queued matrix jobs when any one fails, because fail-fast is true by default. Setting fail-fast: false lets every combination finish, which is useful for seeing the full compatibility picture. Each matrix entry runs as a separate job on its own runner.

27. Merge conflict in package-lock.json

Two branches both added npm packages, and merging them gives a large conflict in package-lock.json. What is the cleanest way to resolve it?

  1. A.Delete the lockfile and stop committing it to Git
  2. B.Fix package.json, then rerun npm install to rebuild it
  3. C.Pick all of one side's lockfile changes by hand
  4. D.Mark the lockfile as a binary file in .gitattributes
Show answer

Answer: B. Fix package.json, then rerun npm install to rebuild it

The lockfile is generated, so hand-merging thousands of lines is error-prone. Fix package.json so it contains both sets of dependencies, then let npm rebuild the lockfile and commit it. Removing the lockfile would make builds non-reproducible.

28. Focusing audit on shipped code

`npm audit` lists 80 issues, most in build and test tools that never ship. You want a gate that focuses on code that runs in production. What helps?

  1. A.Delete the lockfile so npm audit finds fewer packages
  2. B.Run npm audit --omit=dev for the gate, and track dev issues apart
  3. C.Turn off npm audit in CI
  4. D.Move all dev dependencies into dependencies
Show answer

Answer: B. Run npm audit --omit=dev for the gate, and track dev issues apart

--omit=dev limits the report to dependencies installed in production, which is where runtime risk lives. Dev tools still matter, since a compromised build tool can tamper with the build, so track them with a lower priority. Deleting the lockfile breaks reproducibility.

Node.js interview questions for experienced candidates hard

29. Comparator that returns a boolean

What does this print in Chrome or Node.js?

console.log([3, 1, 2].sort((a, b) => a > b))
  1. A.[3, 1, 2]
  2. B.[1, 2, 3]
  3. C.[3, 2, 1]
  4. D.A TypeError, because the comparator must return a number
Show answer

Answer: A. [3, 1, 2]

sort expects a number that is negative, zero or positive. A boolean becomes 1 or 0, so the comparator never says "a comes first", and the stable sort leaves many pairs in their original order; V8 returns [3, 1, 2] here. Results depend on the engine and input, so always return a - b for numbers or use localeCompare for strings.

30. Deep import breaks after upgrade

After upgrading a library, import 'lib/dist/utils.js' fails with ERR_PACKAGE_PATH_NOT_EXPORTED. Why?

  1. A.The file was converted from JavaScript to TypeScript
  2. B.Its new "exports" map hides that internal path
  3. C.npm now blocks every import from a dist folder
  4. D.Node.js dropped support for the .js file extension
Show answer

Answer: B. Its new "exports" map hides that internal path

When a package defines "exports", only listed entry points can be imported, and others are private. Use a public entry point from the docs, or ask the maintainer to export the path.

31. Build fails after moving to pnpm

After switching from npm to pnpm, a build fails because the code imports a package it never declared. Why does pnpm expose this?

  1. A.pnpm deletes all transitive dependencies
  2. B.pnpm does not support importing packages
  3. C.pnpm only links declared dependencies into node_modules
  4. D.pnpm installs only devDependencies by default
Show answer

Answer: C. pnpm only links declared dependencies into node_modules

npm's flat hoisting lets code reach packages it never declared (phantom dependencies). pnpm uses a content-addressed store with symlinks so each package sees only what it lists. The fix is to add the missing dependency.

32. Connection resets when pods stop

Kubernetes sends SIGTERM to old pods during a deploy, and some in-flight requests fail with connection resets. What should the Node.js app do on SIGTERM?

  1. A.Call process.exit(0) at once to exit cleanly
  2. B.Ignore SIGTERM and wait for SIGKILL to arrive
  3. C.Stop new connections, finish current ones, then exit
  4. D.Restart the HTTP server on a different port
Show answer

Answer: C. Stop new connections, finish current ones, then exit

On SIGTERM, call server.close() so no new connections are accepted, let current requests finish, close idle keep-alive connections and database pools, and then exit. Set a timeout below the pod's grace period. A short delay before closing also lets the load balancer stop sending traffic.

33. A CSV export runs out of memory

An endpoint builds a CSV of 5 million rows as one string and then calls res.send. The process runs out of memory. What is the better design?

  1. A.Stream rows to the response with backpressure
  2. B.Raise the Node.js heap size limit to 16 GB
  3. C.Gzip the whole string before sending it
  4. D.Send the same data as JSON instead of CSV
Show answer

Answer: A. Stream rows to the response with backpressure

Building the whole file in memory scales with the data size. Read rows with a database cursor and write them to the response stream, pausing when write() returns false, so memory stays flat. For very large exports, a background job that uploads a file is also common.

34. Consumer runs out of memory

A Node.js consumer pulls messages as fast as the broker sends them and starts a promise for each. During a backlog it uses 6 GB and crashes. What is the fix?

  1. A.Limit in-flight messages with prefetch and a concurrency cap
  2. B.Give the container 12 GB of memory
  3. C.Ack each message before starting its promise
  4. D.Switch to auto-ack so messages leave the broker
Show answer

Answer: A. Limit in-flight messages with prefetch and a concurrency cap

Without backpressure the consumer keeps accepting work it cannot finish, buffering it all in memory. A prefetch limit plus a bounded worker pool means new messages only arrive as old ones complete, leaving the backlog safely in the broker. More memory only delays the crash, and early acks lose work.

35. Backpressure in Node.js streams

A Node.js script reads a 10 GB file and calls writable.write(chunk) for every chunk, ignoring the return value. Memory grows to several GB. What is wrong?

  1. A.write() returned false; wait for "drain"
  2. B.Streams must be closed after each chunk
  3. C.The file is too large for the fs module
  4. D.write() is asynchronous and needs await
Show answer

Answer: A. write() returned false; wait for "drain"

When the writable's internal buffer passes highWaterMark, write() returns false to ask the producer to pause. Ignoring it keeps queueing chunks in memory. stream.pipeline() or pipe() handle this backpressure for you.

36. Chat across many servers

A chat app runs 3 Node.js instances behind a load balancer. Users on different instances do not receive each other's messages. What is the standard fix?

  1. A.Add a pub/sub backplane such as Redis Pub/Sub or NATS so each instance publishes messages and all instances deliver them to their own connected sockets
  2. B.Run only one instance forever
  3. C.Make every client poll every instance directly
  4. D.Store messages in each instance memory and copy them with a cron job
Show answer

Answer: A. Add a pub/sub backplane such as Redis Pub/Sub or NATS so each instance publishes messages and all instances deliver them to their own connected sockets

Each WebSocket connection lives on one instance, so an instance can only push to its own clients. A shared pub/sub channel fans messages out to every instance, which then sends to its local sockets. Sticky sessions help with connection setup but do not solve cross-instance delivery.

37. Comparing secret tokens safely

A webhook endpoint checks if (provided === process.env.API_KEY). You want to remove the timing side channel. Which approach is correct in Node.js?

  1. A.Keep === and add a random sleep after each check
  2. B.Hash both values (e.g. SHA-256) and compare the digests with crypto.timingSafeEqual
  3. C.Call crypto.timingSafeEqual on the raw strings; it returns false if the lengths differ
  4. D.Convert both strings to lowercase before comparing
Show answer

Answer: B. Hash both values (e.g. SHA-256) and compare the digests with crypto.timingSafeEqual

String equality can stop at the first different character, so response times can leak how much of a guess was right. timingSafeEqual compares buffers in constant time, but it throws a RangeError when lengths differ and does not accept plain strings. Hashing both sides first gives equal-length buffers and hides the real key’s length.

38. Async error skips Express handler

This app runs Express 4 on Node 22, and loadReport() rejects. What happens?

app.get('/report', async (req, res) => {
  const data = await loadReport()
  res.json(data)
})

app.use((err, req, res, next) => {
  res.status(500).json({ error: 'Something went wrong' })
})
  1. A.The error middleware catches the rejected promise and returns the 500 JSON response as written
  2. B.Express retries the handler once
  3. C.The error middleware is skipped; the rejection goes unhandled and crashes the process
  4. D.The client receives an empty 200 response
Show answer

Answer: C. The error middleware is skipped; the rejection goes unhandled and crashes the process

Express 4 ignores the promise an async handler returns, so a rejection never reaches next(err). Since Node 15, unhandled rejections terminate the process by default, taking down every in-flight request. Express 5 forwards rejected promises to the error middleware; on Express 4 you need try/catch with next(err) or a wrapper.

39. Deleted secret still in the image

This Dockerfile copies an npm token, uses it, then deletes it. A scan still finds the token in the pushed image. What is the right fix?

FROM node:22-alpine
WORKDIR /app
COPY .npmrc package*.json ./
RUN npm ci
RUN rm .npmrc
COPY . .
  1. A.Run rm .npmrc in the same RUN line as npm ci, which removes it from every layer of the image.
  2. B.Pass the token with ARG NPM_TOKEN instead, because build args are never stored in the image.
  3. C.Squash the image after pushing it, so the registry drops the older layer that holds the file.
  4. D.Use a BuildKit secret mount (RUN --mount=type=secret,id=npmrc ...) so the token never lands in a layer.
Show answer

Answer: D. Use a BuildKit secret mount (RUN --mount=type=secret,id=npmrc ...) so the token never lands in a layer.

Each instruction makes a layer, and the COPY layer still holds .npmrc even though a later layer deletes it; anyone can extract layers from the image. Build args also leak through image history and cache. BuildKit secret mounts expose the secret only while that RUN step executes and never store it in the image.

40. Same commit, different build result

Rebuilding last month's commit now fails, though no code changed. The Dockerfile uses FROM node:20 and the build runs `npm install` without a lockfile. What is the root problem?

  1. A.The CI runner clock is set to a different time zone
  2. B.Docker images expire after 30 days in the registry
  3. C.Git commits change their content after a month
  4. D.Unpinned inputs: the base image and packages changed since then
Show answer

Answer: D. Unpinned inputs: the base image and packages changed since then

The build depends on moving inputs: node:20 points to new images over time, and without a lockfile npm resolves new package versions. Pin the base image by digest, commit the lockfile, use npm ci, and record tool versions. Then the same commit gives the same result.

41. A Node.js API stalls under load

A Node.js API has one endpoint that hashes passwords with a synchronous function taking 150 ms. Under load, every endpoint becomes slow, even simple health checks. Why?

  1. A.Synchronous work blocks the single event loop for all requests
  2. B.Node.js can only accept one TCP connection at a time
  3. C.Health checks always slow down under any kind of load
  4. D.The hashing endpoint is using too little memory to run
Show answer

Answer: A. Synchronous work blocks the single event loop for all requests

Node.js runs JavaScript on one main thread. A 150 ms synchronous call stops it from handling anything else, so all requests queue behind it. Use async versions that run on the thread pool, worker threads, or move heavy work elsewhere; event-loop lag is a key metric to watch.

Ready to test yourself?

Take a 15-question Node.js MCQ test with a score at the end, or practise the full Node.js question bank and earn points on the leaderboard.