DrillLab
第 13 / 25 节LESSON 13 / 25约 18 分钟~18 min

Node 与 Express 四问4 questions on Node and Express

Node 的事件循环、请求响应周期、查询参数 vs 路径参数、CRUD。The Node.js event loop, the request and response cycle, query parameters vs path parameters, CRUD.

面试 · 第 6 部分Interview · Part 6
这一页有什么On this page5
学完这节你会After this lesson you can
  • 说出 Node 事件循环的几个阶段以及 nextTick 的特殊位置Name the phases of the Node.js event loop, and where nextTick sits apart from them
  • 按顺序描述一个请求从进来到响应出去经过了什么Describe, in order, what happens to a request from arrival to response
  • 在路径参数和查询参数之间做出正确设计选择Choose correctly between a path parameter and a query parameter
  • 把 CRUD 映射到 HTTP 方法和状态码Map CRUD onto HTTP methods and status codes
这在考试里考什么What the exam does with this

这四道直接对应 Federation 那门课里 Task 2 写的六个 Spring 端点 —— 那道题的评分点就是「方法对不对、状态码对不对、参数从哪来」。Node 事件循环那道会和浏览器的对比着问。These four map straight onto the six Spring endpoints written in Task 2 of the Federation exam, where the marks go to the right method, the right status code, and the right place to read each parameter from. The Node.js event loop question is usually asked side by side with the browser one.

§01

Node.js 的事件循环是怎么工作的How does the Node.js event loop work?

#313 How does the event loop work in Node.js

一句话:Node 是单线程执行 JS + 多线程做 I/O。 主线程跑 JS, 耗时的 I/O 交给 libuv 的线程池或操作系统, 完成后把回调放进队列, 事件循环再取出来执行。

六个阶段(顺序要记住):

  1. timers—— 到期的 setTimeout /setInterval
  2. pending callbacks—— 上一轮延后的系统回调
  3. idle / prepare —— 内部用
  4. poll——取新的 I/O 事件, 大部分时间待在这里
  5. check——setImmediate 的回调
  6. close callbacks——socket.on("close") 这类

关键:每个阶段之间都会把微任务清空。而 Node 的微任务分两级:process.nextTick的优先级比 Promise 更高—— 它有自己的队列,在所有 Promise 微任务之前执行

和浏览器的差别(这是考点):浏览器只有「宏任务 / 微任务」两档, 每次只取一个宏任务; Node 分了六个阶段, 同一阶段里的队列会一次取完
另外 Node 多了setImmediateprocess.nextTick这两个浏览器没有的东西。

会追问:setTimeout(fn, 0)setImmediate 谁先?」——不确定! 在主模块里两者顺序取决于进程启动耗时; 但在 I/O 回调里,setImmediate 一定更早(因为 check 阶段紧跟在 poll 后面, 而 timers 要等下一轮)。能答出「主模块里不确定」很加分。
「CPU 密集任务怎么办?」—— 单线程会被卡死。 用 worker_threads、 子进程,或者干脆交给别的服务。

In one line: Node runs your JavaScript on one thread and its I/O on many. The main thread runs JS; anything slow goes to libuv’s thread pool or straight to the OS, and when it finishes the callback is queued for the event loop to pick up.

Six phases, and the order matters:

  1. timerssetTimeout and setInterval callbacks that are due
  2. pending callbacks — system callbacks deferred from the previous turn
  3. idle / prepare — internal use
  4. pollpicks up new I/O events; this is where the process spends most of its time
  5. checksetImmediate callbacks
  6. close callbacks — things like socket.on("close")

The key point: microtasks are drained between every phase. And Node has two levels of them — process.nextTick outranks Promises. It gets its own queue and runs before any Promise microtask.

How this differs from the browser (this is the part they are testing): the browser has just two tiers, macrotask and microtask, and takes one macrotask per turn. Node has six phases, and within a phase it drains the whole queue.
Node also has setImmediate and process.nextTick, neither of which exists in a browser.

Follow-up: “Which fires first, setTimeout(fn, 0) or setImmediate?” — it is not guaranteed. At the top level of the main module the order depends on how long startup took. But inside an I/O callback setImmediate always wins, because check comes right after poll while timers has to wait for the next turn. Saying “undefined in the main module” is the answer that earns you points.
“What about CPU-bound work?” — one thread means it blocks everything. Use worker_threads, a child process, or hand the job to a different service entirely.

JavaScriptNode 的执行顺序示意Illustrative
1console.log("1 同步");
2
3setTimeout(() => console.log("2 timers"), 0);
4setImmediate(() => console.log("3 check"));
5Promise.resolve().then(() => console.log("4 promise 微任务"));
6process.nextTick(() => console.log("5 nextTick"));
7
8console.log("6 同步");
9
10// 输出:1 同步 -> 6 同步 -> 5 nextTick -> 4 promise 微任务
11// -> 2 timers 和 3 check(这两个的相对顺序在主模块里不保证)
12//
13// 记住:nextTick 比 Promise 更优先,这是 Node 独有的
1console.log("1 sync");
2
3setTimeout(() => console.log("2 timers"), 0);
4setImmediate(() => console.log("3 check"));
5Promise.resolve().then(() => console.log("4 promise microtask"));
6process.nextTick(() => console.log("5 nextTick"));
7
8console.log("6 sync");
9
10// Output: 1 sync -> 6 sync -> 5 nextTick -> 4 promise microtask
11// -> 2 timers and 3 check (their relative order is not guaranteed in the main module)
12//
13// Remember: nextTick runs before Promise, and that part is specific to Node
§02

请求 - 响应周期是怎样的What does the request and response cycle look like?

#314 Explain the request & response cycle

一句话:客户端发请求 → 经过一串中间件 → 匹配到路由 → 处理逻辑(查库等)→发出一个响应→ 结束。

Express 里具体经过什么:

  1. 解析——express.json()把请求体解析成 req.body不加这个中间件req.body 就是undefined, 这是最常见的新手问题)
  2. 通用中间件—— CORS、日志、关联 id、限流
  3. 认证 / 鉴权—— 验 token,把用户挂到req.user
  4. 路由匹配—— 按注册顺序找第一个匹配的 method + path
  5. 校验 → 业务逻辑 → 查库
  6. 响应——res.status(200).json(...)
  7. 兜底—— 404 处理 + 错误处理中间件

三个必须知道的规则:

  • 中间件按注册顺序执行, 必须调 next()才会往下走 —— 忘了调请求就永远挂着, 客户端一直转圈。
  • 一个请求只能响应一次。重复 res.send会报Cannot set headers after they are sent所以调用响应之后一定要return
  • 错误处理中间件有四个参数(err, req, res, next)——少一个参数 Express 就认不出它是错误处理器, 这个坑很经典。

会追问:「异步函数里抛的错误 Express 会自动捕获吗?」——Express 4 里不会, 会变成未处理的 rejection,请求挂死。 要么自己 try/catchnext(err), 要么包一层 asyncHandler。Express 5 才支持自动转发。

In one line: the client sends a request, it walks through a chain of middleware, a route matches, the handler does its work (hits the database and so on), one response goes out, done.

What it actually passes through in Express:

  1. Parsingexpress.json() turns the body into req.body (without that middleware req.body is undefined, which is the single most common beginner bug)
  2. Generic middleware — CORS, logging, a correlation id, rate limiting
  3. Authentication and authorisation — verify the token, hang the user off req.user
  4. Route matching — the first registered method + path that matches wins
  5. Validation, then business logic, then the database
  6. Response res.status(200).json(...)
  7. Catch-alls — the 404 handler and the error-handling middleware

Three rules you have to know:

  • Middleware runs in registration order and you must call next() for the chain to continue. Forget it and the request hangs forever while the client spins.
  • One request gets exactly one response. A second res.send throws Cannot set headers after they are sent. So always return after you respond.
  • Error middleware takes four arguments, (err, req, res, next) drop one and Express no longer recognises it as an error handler. A classic trap.

Follow-up: “Does Express catch errors thrown inside an async function?” — not in Express 4. It becomes an unhandled rejection and the request hangs. Either try/catch yourself and call next(err), or wrap the handler in an asyncHandler. Express 5 forwards them for you.

JavaScript一个请求经过的全部环节示意Illustrative
1app.use(express.json()); // 1 解析 body
2app.use(cors()); // 2 通用
3app.use(correlationId); // 关联 id,方便串联日志
4app.use(auth); // 3 认证
5
6app.get("/orders/:id", async (req, res, next) => {
7 try {
8 const order = await db.find(req.params.id);
9 if (!order) return res.status(404).json({ error: "not found" }); // 记得 return
10 res.json(order);
11 } catch (e) {
12 next(e); // Express 4 不会自动接异步错误
13 }
14});
15
16app.use((req, res) => res.status(404).json({ error: "no route" }));
17
18// 错误处理中间件:必须是四个参数,少一个就不生效
19app.use((err, req, res, next) => {
20 console.error(err);
21 res.status(500).json({ error: "internal" });
22});
1app.use(express.json()); // 1 parse the body
2app.use(cors()); // 2 general
3app.use(correlationId); // a correlation id, so logs can be tied together
4app.use(auth); // 3 authentication
5
6app.get("/orders/:id", async (req, res, next) => {
7 try {
8 const order = await db.find(req.params.id);
9 if (!order) return res.status(404).json({ error: "not found" }); // do not forget return
10 res.json(order);
11 } catch (e) {
12 next(e); // Express 4 does not catch async errors for you
13 }
14});
15
16app.use((req, res) => res.status(404).json({ error: "no route" }));
17
18// The error-handling middleware must take four arguments; with three it never runs
19app.use((err, req, res, next) => {
20 console.error(err);
21 res.status(500).json({ error: "internal" });
22});
§03

查询参数 vs 路径参数Query parameters vs path parameters

#315 Query parameters vs Path parameters

一句话:路径参数标识「哪一个资源」/users/42),查询参数描述「怎么取」?page=2&sort=name)。

路径参数查询参数
形式/users/:id/users?role=admin
Express 里读req.params.idreq.query.role
Spring 里读@PathVariable@RequestParam
必填性必填(是路径的一部分)通常可选,有默认值
语义标识资源筛选、排序、分页、字段裁剪

设计判据一句话:「去掉它之后,还是同一个资源吗?」去掉 42 就不知道是谁了 → 路径参数; 去掉 ?page=2还是同一批用户,只是换一页 → 查询参数。

会追问:「密码能放查询参数吗?」——绝对不行。 URL 会进浏览器历史、 服务器访问日志、 Referer 头、CDN 日志 ——即使用了 HTTPS, URL 本身也会被大量记录。 敏感数据放请求体或请求头。
「查询参数有长度限制吗?」—— 规范没有,但实践上服务器和 CDN 通常限制 URL 在 2 KB 到 8 KB。 复杂查询条件多的搜索接口 有时会改用 POST 带 body。

In one line: a path parameter says which resource (/users/42); a query parameter says how you want it (?page=2&sort=name).

Path parameterQuery parameter
Shape/users/:id/users?role=admin
Read in Expressreq.params.idreq.query.role
Read in Spring@PathVariable@RequestParam
Required?Yes — it is part of the pathUsually optional, often with a default
MeaningIdentifies the resourceFiltering, sorting, paging, field selection

One test to decide: “Take it away — is it still the same resource?” Drop the 42 and you no longer know who you mean → path parameter. Drop ?page=2 and it is still the same set of users, just a different page → query parameter.

Follow-up: “Can a password go in a query parameter?” — absolutely not. URLs land in browser history, server access logs, the Referer header and CDN logs. HTTPS encrypts the connection; it does not stop the URL from being written down all over the place. Sensitive values go in the body or a header.
“Is there a length limit on query strings?” — not in the spec, but in practice servers and CDNs cap the URL somewhere between 2 KB and 8 KB. Search endpoints with complex filters sometimes switch to POST with a body for that reason.

§04

什么是 CRUDWhat is CRUD?

#316 What is CRUD

一句话:Create / Read / Update / Delete —— 数据操作的四种基本类型。 REST 把它们映射到 HTTP 方法。

操作方法路径成功状态码
CreatePOST/orders201 Created
Read(列表)GET/orders200
Read(单个)GET/orders/:id200,没有则 404
Update(整体)PUT/orders/:id200
Update(部分)PATCH/orders/:id200
DeleteDELETE/orders/:id204 No Content

两个高频追问:

① PUT vs PATCH。PUT整体替换—— 没传的字段应该被清空;PATCH局部更新—— 只改传了的字段。实践中很多人把 PUT 当 PATCH 用, 这是错的,但要知道现实如此。

② 幂等性。同一个请求发多次,结果一样就叫幂等。

  • GET / PUT /DELETE ——幂等
  • POST ——不幂等(发两次会创建两条)
  • PATCH ——看实现{ n: 5 } 幂等,{ n: { $inc: 1 } } 不幂等)

为什么重要:客户端重试、 网关超时重发时, 幂等的接口是安全的,POST 需要幂等键(idempotency key)来防重复下单。

再一个追问:「删一个不存在的资源返回什么?」——可以是 204 也可以是 404。 返 204 更符合幂等语义 (「删完了」这个结果达成了); 返 404 信息更明确。关键是团队内一致, 并且写进接口文档。

In one line: Create / Read / Update / Delete — the four basic things you do to data. REST maps them onto HTTP methods.

OperationMethodPathStatus on success
CreatePOST/orders201 Created
Read (list)GET/orders200
Read (one)GET/orders/:id200, or 404 if it is not there
Update (whole)PUT/orders/:id200
Update (partial)PATCH/orders/:id200
DeleteDELETE/orders/:id204 No Content

Two follow-ups they almost always ask:

① PUT vs PATCH. PUT is a full replacement — fields you leave out should be cleared. PATCH is a partial update — only the fields you send change. Plenty of real code uses PUT as if it were PATCH, which is wrong, but know that it happens.

② Idempotency. Send the same request more than once, get the same result — that is idempotent.

  • GET / PUT / DELETE idempotent
  • POSTnot idempotent (send it twice and you get two records)
  • PATCHdepends on the payload ({ n: 5 } is idempotent, { n: { $inc: 1 } } is not)

Why it matters: when a client retries or a gateway resends after a timeout, an idempotent endpoint is safe. POST needs an idempotency key to stop the same order being placed twice.

One more follow-up: “What do you return when deleting something that does not exist?” — 204 or 404, both defensible. 204 fits the idempotent reading (the outcome you asked for is true); 404 tells the caller more. What matters is that the team agrees and it is in the API docs.

迁移Transfer

换一道题也能用Works on other problems too

考试不会原题重考。真正能带走的是「看到这种信号 → 伸手去拿这个解法」。The exam will not reuse the same question. What you take away is the reflex: see this signal, reach for that solution.

问 nextTick 和 Promise 谁先Asked whether nextTick or a Promise runs first
nextTick 有独立队列,比所有 Promise 微任务优先nextTick has its own queue, which runs before every Promise microtask
「req.body 是 undefined」req.body is undefined
漏了 express.json()express.json() is missing
请求一直转圈不返回The request never comes back
中间件忘了调 next(),或响应后没 returnA middleware forgot to call next(), or the code did not return after sending the response
错误处理中间件不生效The error-handling middleware never runs
必须四个参数 (err, req, res, next)It has to take four parameters: (err, req, res, next)
设计接口纠结参数放哪Unsure where a parameter belongs when designing an endpoint
「去掉它还是同一个资源吗」Ask: if you remove it, is it still the same resource?
创建资源返回什么码Which status code to return after creating a resource
201;删除用 204201; use 204 for a delete
这节的要点What to take away
  1. Node 六个阶段:timers → pending → idle → poll → check → close;每阶段之间清微任务,nextTick 最优先。Six phases in Node.js: timers, pending, idle, poll, check, close. Microtasks are drained between phases, and nextTick goes first of all.
  2. 主模块里 setTimeout(0) 和 setImmediate 顺序不保证,I/O 回调里 setImmediate 一定更早。In the main module the order of setTimeout(0) and setImmediate is not guaranteed; inside an I/O callback setImmediate always runs first.
  3. Express 请求流程:解析 → 通用中间件 → 认证 → 路由 → 业务 → 响应 → 404/错误兜底。The Express request path: parse, then general middleware, then authentication, then routing, then your handler, then the response, with a 404 and an error handler at the end.
  4. 中间件按注册顺序、必须 next();一个请求只能响应一次;错误中间件必须四个参数。Middleware runs in the order you register it and must call next(); one request can be answered only once; an error handler must take four parameters.
  5. 路径参数标识资源、查询参数描述怎么取;敏感数据永远不放 URL。A path parameter identifies a resource, a query parameter says how to fetch it; never put sensitive data in a URL.
  6. CRUD 映射:POST 201、GET 200/404、PUT 整体替换、PATCH 局部、DELETE 204。CRUD mapping: POST returns 201, GET returns 200 or 404, PUT replaces the whole resource, PATCH changes part of it, DELETE returns 204.
  7. GET/PUT/DELETE 幂等,POST 不幂等 —— 防重复下单要用幂等键。GET, PUT and DELETE are idempotent, POST is not, so use an idempotency key to stop a duplicate order.

接下来What next

  1. 接着看下一节Continue to the next lesson数据库两问2 questions on databases
    下一节Next lesson
  2. 可选:再巩固一下Optional: reinforce it这一节的 4 道八股4 questions from this lesson
读完并且做过上面的练习了吗?Read it and worked through the exercises above?
上一节:Previous: Redux 与 TypeScript · 六问6 questions on Redux and TypeScript