subgraph 是怎么跑起来的How a subgraph starts up
buildSubgraphSchema 做了什么,为什么它会凭空多出两个字段。What buildSubgraphSchema does, and why two fields appear that you never wrote.
这一页有什么On this page6
- 读懂 index.js 的启动流程Read the startup flow in index.js
- 说清 buildSubgraphSchema 和普通 makeExecutableSchema 的区别Explain the difference between buildSubgraphSchema and plain makeExecutableSchema
- 知道 _service 和 _entities 这两个字段从哪来Know where the two fields _service and _entities come from
- 会用进程内方式验证 subgraph(不需要起服务器)Check a subgraph from inside the same process, with no server running
启动流程决定了 context 长什么样(你的 resolver 全靠它)。而 _service / _entities 是本地唯一能验证 federation 部分的手段。The startup flow decides what context looks like, and every resolver you write depends on it. _service and _entities are the only way to check the Federation part locally.
graphql-federation-practice/node-subgraph/src/index.js启动流程与 context 构造The startup sequence and how the context is built
graphql-federation-practice/node-subgraph/src/index.jsgraphql-federation-practice/node-subgraph/package.jsonstart / test script 与 federation 依赖The start and test scripts, and the federation dependency
graphql-federation-practice/node-subgraph/package.json启动的五步The five startup steps
- 读 schema 文件。
readFileSync(join(__dirname, 'schema.graphql')), 然后gql(...)把字符串解析成 AST。
(__dirname在 ESM 里不是内置的, 所以上面用fileURLToPath(import.meta.url)手动算了一个 —— 这是 ESM 项目的标准写法。) - 组装 schema。
buildSubgraphSchema([{ typeDefs, resolvers }])—— 这是 federation 的关键一步,下一段细说。 - 建 ApolloServer,带一个
formatError钩子,把错误的 message / code / path / correlationId 打到服务端日志。 - 起服务器,监听 4000。
- 每个请求构造 context(上一模块讲过)。
formatError 那段值得注意:它原样返回formattedError,只是顺手打了日志。 也就是说你在 resolver 里放进extensions 的东西会被客户端看到 —— 这是「结构化错误」能起作用的前提。
- Read the schema file.
readFileSync(join(__dirname, 'schema.graphql')), thengql(...)parses that string into an AST.
(__dirnameis not built in under ESM, so the code above computes one by hand withfileURLToPath(import.meta.url)— the standard move in an ESM project.) - Assemble the schema.
buildSubgraphSchema([{ typeDefs, resolvers }])— the key federation step, covered in the next section. - Create the ApolloServer with a
formatErrorhook that logs each error’s message, code, path and correlationId on the server side. - Start the server, listening on 4000.
- Build a context for every request (covered in the previous module).
That formatError block deserves a close look: it returns formattedError unchanged and only logs on the way past. Which means whatever you put into extensions inside a resolver reaches the client — the precondition for “structured errors” doing any good at all.
buildSubgraphSchema 凭空加了两个字段buildSubgraphSchema adds two fields you never wrote
这是 subgraph 和普通 GraphQL 服务唯一的技术差别。This is the only technical difference between a subgraph and a plain GraphQL service.
普通 GraphQL 服务用 makeExecutableSchema。 subgraph 用 buildSubgraphSchema(来自 @apollo/subgraph)。 后者多做三件事:
- 认识 federation 的 directive:
@key、@external、@shareable等。 普通 schema 遇到它们会报「未知指令」。 - 自动加一个
_service字段, 返回本 subgraph 的 federation SDL。 Router 启动时就是靠查这个字段来收集 schema 的。 - 自动加一个
_entities字段, 接收一批 entity representation,返回对应的对象。 Router 在运行时靠它做跨服务的实体解析。
这两个字段你不用写,也不该写。但你要知道它们存在 —— 因为它们是本地验证 federation 的唯一入口。
A plain GraphQL service uses makeExecutableSchema. A subgraph uses buildSubgraphSchema (from @apollo/subgraph). The second one does three extra things:
- It understands the federation directives:
@key,@external,@shareableand friends. A plain schema reports them as unknown directives. - It adds a
_servicefield for you, returning this subgraph’s federation SDL. That field is how the Router collects schemas at startup. - It adds an
_entitiesfield for you, which takes a batch of entity representations and returns the matching objects. The Router uses it at runtime for cross-service entity resolution.
You do not write those two fields, and you should not. But you need to know they are there — they are the only door into verifying federation locally.
graphql-federation-practice/node-subgraph/src/index.js本地验证:两种办法Checking it locally: two ways
审计时端口 4000 被占,所以我用了第二种 —— 它其实更好用。During the audit port 4000 was taken, so I used the second way. It turns out to be the more useful one.
办法一:起服务器 + curl。npm start 之后服务器在 4000,index.js 最后还贴心地打印了 SDL 的查询地址。
办法二:进程内执行,不起服务器。直接用 buildSubgraphSchema 造出 schema, 再用 graphql() 执行查询。好处是不占端口、不需要等服务器起来、 可以在一个脚本里跑一串查询。
下面这个脚本是审计时我实际写的验证工具。 它把 federation 的关键路径全跑了一遍 —— 包括 _entities,也就是 Router 会发的那个请求。做完 Task 1 之后强烈建议你也写一个类似的。
Way one: start the server and curl it. After npm start the server is on 4000, and index.js even prints the URL that queries the SDL.
Way two: execute in-process, no server at all. Build the schema with buildSubgraphSchema and run queries through graphql(). No port to occupy, no waiting for a server to come up, and you can run a whole series of queries from one script.
The script below is the verification tool I actually wrote during the audit. It walks every important federation path — including _entities, the request the Router would send. After you finish Task 1, write yourself something like it.
两个 ESM 细节Two ESM details
- import 要带
.js。from './resolvers/orderResolvers.js'—— 这个项目是原生 ESM("type": "module"), 不走打包器,所以扩展名必须写。Foundations 那门课有个 Debug Lab 专门练这个。 - 顶层 await 可以用。
const { url } = await startStandaloneServer(...)写在模块顶层 —— 这是 ESM 才有的能力,CommonJS 里做不到。
另外注意 npm test 那条 script 里的NODE_OPTIONS=--experimental-vm-modules —— jest 要跑 ESM 就得带上它。这些配置不用你改,但要认得。
- Imports need the
.js.from './resolvers/orderResolvers.js'— this project is native ESM ("type": "module") with no bundler, so the extension is mandatory. The Foundations course has a Debug Lab just for this. - Top-level await works.
const { url } = await startStandaloneServer(...)sits at module top level — an ESM-only ability, impossible in CommonJS.
Also notice the NODE_OPTIONS=--experimental-vm-modules in that npm test script — jest needs it to run ESM. None of this config is yours to change, but you should recognise it.
动手做Get your hands on it
填空只是过渡。真正掌握的标准,是在没有答案的时候从头写出来 —— 所以做完 L2 之后一定要往 L3、L4 走。Filling blanks is a stepping stone. The real bar is writing it from nothing, so once L2 is comfortable, push on to L3 and L4.
schema.graphql 里从头到尾没有出现_entities,但 Router 能查它。它从哪来?
_entities appears nowhere in schema.graphql, yet the Router can query it. Where does it come from?
仓库里没有 Router。你想确认自己的 User.orders在 federation 链路里能被正确调用。最直接的办法?
There is no Router in the repository. You want to confirm your User.orders is called correctly along the federation path. What is the most direct way?
换一道题也能用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.
- 启动五步:读 schema → buildSubgraphSchema → 建 server → 监听 → 每请求造 context。Five startup steps: read the schema, call buildSubgraphSchema, create the server, listen, then build a context for each request.
- buildSubgraphSchema 认识 federation directive,并自动加 _service 和 _entities 两个字段。buildSubgraphSchema understands the Federation directives and adds the two fields _service and _entities for you.
- formatError 原样返回错误,所以你放进 extensions 的东西客户端能看到。formatError returns errors unchanged, so whatever you put in extensions reaches the client.
- 本地验证优选「进程内执行」:不占端口,能一次跑一串查询,包括 _entities。Prefer running queries inside the process: it needs no port and lets you run several queries in a row, including _entities.
- 原生 ESM:import 带 .js,顶层 await 可用,jest 需要 --experimental-vm-modules。Native ESM: imports need the .js extension, top-level await works, and jest needs --experimental-vm-modules.