Couches API
Les intégrations RPC et GraphQL doivent créer un scope InferDI par requête HTTP, et non par procédure ou par résolveur.
Ces exemples utilisent le graphe partagé. Compare l’endroit où le scope est créé et la limite responsable de sa libération. examples/_shared/container.ts
| Exemple | Contenu |
|---|---|
trpc.ts | Scope autour de toute la requête HTTP avec fetchRequestHandler de tRPC |
apollo-server.ts | Scope de contexte Apollo Server pour une exécution sans streaming |
graphql-yoga.ts | Scope de contexte GraphQL Yoga pour une exécution sans streaming |
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(async ({ ctx }) =>
(await ctx.container.getAsync('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 = 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 })
})
}Fichier du dépôt : 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: async (_parent: unknown, args: { id: string }, ctx: GraphQLContext) =>
(await ctx.container.getAsync('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: createRequestScope(root, {
requestId: crypto.randomUUID(),
userId: normalizeHeader(req.headers.authorization)
})
})
})
}Fichier du dépôt : 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: async (_parent, args: { id: string }, ctx) =>
(await ctx.container.getAsync('users')).profile(args.id)
}
}
}),
context: async ({ request }) => ({
container: 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)
})
}
}
]
})Fichier du dépôt : examples/api-layers/graphql-yoga.ts
