API 层
RPC 和 GraphQL 集成应该为每个 HTTP 请求创建一个 InferDI 作用域,而不是为每个过程(procedure)或每个解析器(resolver)创建。
这些示例共用 examples/_shared/container.ts。请对比每种集成在何处创建作用域,以及由哪个边界负责释放。
| 示例 | 展示内容 |
|---|---|
trpc.ts | tRPC fetchRequestHandler 围绕整个 HTTP 请求设置作用域 |
apollo-server.ts | 针对非流式执行的 Apollo Server context 作用域 |
graphql-yoga.ts | 针对非流式执行的 GraphQL Yoga context 作用域 |
tRPC
ts
import { initTRPC } from '@trpc/server'
import { fetchRequestHandler } from '@trpc/server/adapters/fetch'
import {
buildRootContainer,
createRequestScope,
type RequestContainer,
} from '../_shared/container.js'
const root = buildRootContainer()
type Ctx = { container: RequestContainer }
const t = initTRPC.context<Ctx>().create()
export const router = t.router({
me: t.procedure.query(({ ctx }) => ctx.container.get('users').profile('me')),
})
// HTTP-level scope. One scope per HTTP request — NOT per procedure.
// tRPC's batched-link sends multiple procedure calls in a single HTTP request;
// `fetchRequestHandler` resolves them all under one `createContext` call. We
// dispose ONCE after the response is built, which is the only correct moment.
//
// (A procedure-level middleware that disposes the container would dispose the
// scope between batched procedures on the same request, breaking later calls.)
export async function handleTrpcRequest(req: Request): Promise<Response> {
await using scope = await createRequestScope(root, {
requestId: req.headers.get('x-request-id') ?? crypto.randomUUID(),
userId: req.headers.get('authorization') ?? undefined,
})
return fetchRequestHandler({
endpoint: '/trpc',
req,
router,
createContext: () => ({ container: scope }),
})
}仓库文件:examples/api-layers/trpc.ts
Apollo Server
ts
import { ApolloServer } from '@apollo/server'
import { startStandaloneServer } from '@apollo/server/standalone'
import {
buildRootContainer,
createRequestScope,
type RequestContainer,
} from '../_shared/container.js'
const root = buildRootContainer()
function normalizeHeader(value: string | string[] | undefined): string | undefined {
return Array.isArray(value) ? value[0] : value
}
type GraphQLContext = { readonly container: RequestContainer }
const typeDefs = `#graphql
type User { id: ID!, name: String! }
type Query { user(id: ID!): User! }
`
const resolvers = {
Query: {
user: (_parent: unknown, args: { id: string }, ctx: GraphQLContext) =>
ctx.container.get('users').profile(args.id),
},
}
export const server = new ApolloServer<GraphQLContext>({
typeDefs,
resolvers,
plugins: [
{
async requestDidStart() {
return {
// NOTE: `willSendResponse` fires once Apollo has built the response
// payload. For `@defer`/`@stream` operations parts of the response
// are still streaming AFTER this point — if your resolvers consume
// scoped DB connections that must survive streaming, dispose from a
// transport-level hook (e.g. res.once('finish') in your HTTP layer)
// instead and pass the scope through the standalone server's
// `context` callback as below.
async willSendResponse({ contextValue }) {
await contextValue.container.dispose()
},
}
},
},
],
})
export async function start() {
return startStandaloneServer(server, {
context: async ({ req }) => ({
container: await createRequestScope(root, {
requestId: crypto.randomUUID(),
userId: normalizeHeader(req.headers.authorization),
}),
}),
})
}仓库文件:examples/api-layers/apollo-server.ts
GraphQL Yoga
ts
import { createYoga, createSchema } from 'graphql-yoga'
import {
buildRootContainer,
createRequestScope,
type RequestContainer,
} from '../_shared/container.js'
const root = buildRootContainer()
type GraphQLContext = { readonly container: RequestContainer }
export const yoga = createYoga<GraphQLContext>({
schema: createSchema<GraphQLContext>({
typeDefs: /* GraphQL */ `
type User { id: ID!, name: String! }
type Query { user(id: ID!): User! }
`,
resolvers: {
Query: {
user: (_parent, args: { id: string }, ctx) =>
ctx.container.get('users').profile(args.id),
},
},
}),
context: async ({ request }) => ({
container: await createRequestScope(root, {
requestId: crypto.randomUUID(),
userId: request.headers.get('authorization') ?? undefined,
}),
}),
plugins: [
{
// NOTE: `onExecuteDone` fires once the GraphQL operation finishes. For
// `@defer`/`@stream` the response continues streaming afterwards, so
// resolvers running in those incremental payloads would race a disposal
// started here. For schemas that use incremental delivery, dispose from
// the HTTP transport-level (`onResponse` in your server framework) and
// remove this plugin.
onExecuteDone({ args }) {
const ctx = args.contextValue as GraphQLContext
return ctx.container.dispose().catch((err) => {
console.error('Failed to dispose Yoga request scope', err)
})
},
},
],
})