Skip to content

API 层

RPC 和 GraphQL 集成应该为每个 HTTP 请求创建一个 InferDI 作用域,而不是为每个过程(procedure)或每个解析器(resolver)创建。

这些示例共用 examples/_shared/container.ts。请对比每种集成在何处创建作用域,以及由哪个边界负责释放。

示例展示内容
trpc.tstRPC 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)
        })
      },
    },
  ],
})

仓库文件:examples/api-layers/graphql-yoga.ts