Skip to Content
protoc-gen-dataloader (experimental)Generated Code Reference

Generated Code Reference

EXPERIMENTAL. The exact shape of generated code may change without notice. See the README.

Reference documentation for the TypeScript code generated by protoc-gen-dataloader. Every snippet on this page is excerpted verbatim from the plugin’s golden test fixtures, so it always reflects real output:

  • Entity mode: tests/golden-dataloader/testapis.dataloader.entity/__expected__/testapis/dataloader/entity/entity.pb.dataloader.ts, generated from devPackages/testapis-proto/proto/testapis/dataloader/entity/entity.proto
  • Group mode: tests/golden-dataloader/testapis.dataloader.group/__expected__/testapis/dataloader/group/group.pb.dataloader.ts, generated from devPackages/testapis-proto/proto/testapis/dataloader/group/group.proto

File Layout

One generated file per .proto file that has at least one RPC declaring (graphql.rpc).batch: <proto path>.pb.dataloader.ts (suffix configurable via filename_suffix). Every annotated RPC becomes one exported loader accessor named <rpcNameCamelCase>Loader.

Imports

Every generated file imports the runtime helpers — including the RpcLoader wrapper type every loader const is annotated with (see Loader Params below) — create/MessageShape (and MessageInitShape when any loader has params) from @bufbuild/protobuf, and the protobuf-es service and schema symbols the loaders reference. Generated files never import from dataloader directly; the RpcLoader wrapper type from @proto-graphql/connect-runtime is the only DataLoader-adjacent type they reference:

// @generated by protoc-gen-dataloader vX.Y.Z with parameter "import_prefix=@proto-graphql/e2e-testapis-protobuf-es-v2/lib/" // @generated from file testapis/dataloader/entity/entity.proto (package testapis.dataloader.entity, syntax proto3) /* eslint-disable */ import type { ProtoGraphqlConnectContext, RpcLoader, } from "@proto-graphql/connect-runtime"; import { createRpcLoader } from "@proto-graphql/connect-runtime"; import type { MessageInitShape, MessageShape } from "@bufbuild/protobuf"; import { create } from "@bufbuild/protobuf"; import { BatchGetOrdersRequestSchema, BatchGetUsersInTenantRequestSchema, BatchGetUsersRequestSchema, BatchGetUsersWithLocaleRequestSchema, OrderSchema, UserSchema, UserService, } from "@proto-graphql/e2e-testapis-protobuf-es-v2/lib/testapis/dataloader/entity/entity_pb";

Entity/request/response types are referenced through protobuf-es v2’s MessageShape<typeof XSchema> rather than a class — consistent with how protoc-gen-pothos’s protobuf-es runtime reference works.

Entity Mode

Declared with batch and no group: true. From the source proto:

service UserService { rpc BatchGetUsers(BatchGetUsersRequest) returns (BatchGetUsersResponse) { option (graphql.rpc).batch = { entity_key: "id" }; } }

key_field (ids) and entity_field (users) are inferred. entity_key must be explicit for now — inferring it from the entity’s federation @key is a fallback planned once federation support lands upstream (see (graphql.rpc).batch annotation reference). Generated code:

export const batchGetUsersLoader: ( ctx: ProtoGraphqlConnectContext, ) => RpcLoader<string, MessageShape<typeof UserSchema> | null> = createRpcLoader({ service: UserService, method: "batchGetUsers", requestSchema: BatchGetUsersRequestSchema, call: (client, keys, params, opts) => client.batchGetUsers( create(BatchGetUsersRequestSchema, { ...params, ids: [...keys] }), opts, ), extractEntities: (res) => res.users, extractKey: (user: MessageShape<typeof UserSchema>) => user.id, });

The accessor ((ctx) => RpcLoader<...>) takes only ctx — it’s memoized per ctx, so calling it repeatedly for the same ctx is cheap and returns the same wrapper. A missing key resolves to null: await batchGetUsersLoader(ctx).load("unknown-id")null, never a rejection.

bigint Keys and max_batch_size

max_batch_size caps how many keys go into one RPC call — DataLoader splits a larger request into multiple calls automatically:

rpc BatchGetOrders(BatchGetOrdersRequest) returns (BatchGetOrdersResponse) { option (graphql.rpc).batch = { entity_key: "id", max_batch_size: 2 }; }
export const batchGetOrdersLoader: ( ctx: ProtoGraphqlConnectContext, ) => RpcLoader<bigint, MessageShape<typeof OrderSchema> | null> = createRpcLoader({ service: UserService, method: "batchGetOrders", requestSchema: BatchGetOrdersRequestSchema, call: (client, keys, params, opts) => client.batchGetOrders( create(BatchGetOrdersRequestSchema, { ...params, ids: [...keys] }), opts, ), extractEntities: (res) => res.orders, extractKey: (order: MessageShape<typeof OrderSchema>) => order.id, maxBatchSize: 2, });

Order.id is int64, so the key type is bigint (see Key Type Mapping). Requesting 3 keys results in 2 RPC calls (e.g. 2 + 1) instead of one.

Group Mode

Declared with batch: { group: true }. Since the grouping key (user_id) is not the entity’s own @key, entity_key is always required in group mode — there is nothing to fall back to:

service ReviewService { rpc BatchListReviewsByUsers(BatchListReviewsByUsersRequest) returns (BatchListReviewsByUsersResponse) { option (graphql.rpc).batch = { group: true, entity_key: "user_id" }; } }
export const batchListReviewsByUsersLoader: ( ctx: ProtoGraphqlConnectContext, ) => RpcLoader<string, MessageShape<typeof ReviewSchema>[]> = createRpcLoader({ service: ReviewService, method: "batchListReviewsByUsers", requestSchema: BatchListReviewsByUsersRequestSchema, call: (client, keys, params, opts) => client.batchListReviewsByUsers( create(BatchListReviewsByUsersRequestSchema, { ...params, userIds: [...keys], }), opts, ), extractEntities: (res) => res.reviews, extractKey: (review: MessageShape<typeof ReviewSchema>) => review.userId, group: true, });

The value type is MessageShape<typeof ReviewSchema>[], not ... | null, and a missing key resolves to []: await batchListReviewsByUsersLoader(ctx).load("user-with-no-reviews")[].

Params Variants

When the request has fields besides key_field, the plugin generates a <RpcName>LoaderParams type (the request’s full MessageInitShape, including the key field — see the note below) and folds it into the RpcLoader wrapper’s third type parameter (PArgs), which load/loadMany/loader accept as a trailing argument at call time — not accessor time. The accessor itself ((ctx) => RpcLoader<...>) always takes only ctx, regardless of whether the RPC has params. Whether PArgs is [params?: X] (optional) or [params: X] (required) mirrors whether any of those fields is a required field, using the same isRequiredField semantics as Input type generation (implicit-presence scalars are required; optional / NULLABLE nullability makes a field non-required).

Optional Params

message BatchGetUsersWithLocaleRequest { repeated string ids = 1; // `optional` => proto3Optional => not required: loader `params` stays optional. optional string locale = 2; }
export type BatchGetUsersWithLocaleLoaderParams = MessageInitShape< typeof BatchGetUsersWithLocaleRequestSchema >; export const batchGetUsersWithLocaleLoader: ( ctx: ProtoGraphqlConnectContext, ) => RpcLoader< string, MessageShape<typeof UserSchema> | null, [params?: BatchGetUsersWithLocaleLoaderParams] > = createRpcLoader({ service: UserService, method: "batchGetUsersWithLocale", requestSchema: BatchGetUsersWithLocaleRequestSchema, call: (client, keys, params, opts) => client.batchGetUsersWithLocale( create(BatchGetUsersWithLocaleRequestSchema, { ...params, ids: [...keys], }), opts, ), extractEntities: (res) => res.users, extractKey: (user: MessageShape<typeof UserSchema>) => user.id, });
// usage: params is the (optional) trailing argument to load/loadMany, not to the accessor await batchGetUsersWithLocaleLoader(ctx).load("user-1"); await batchGetUsersWithLocaleLoader(ctx).load("user-1", { locale: "ja" });

Required Params

message BatchGetUsersInTenantRequest { repeated string ids = 1; // Implicit presence scalar => required: loader `params` becomes mandatory. string tenant_id = 2; }
export type BatchGetUsersInTenantLoaderParams = MessageInitShape< typeof BatchGetUsersInTenantRequestSchema >; export const batchGetUsersInTenantLoader: ( ctx: ProtoGraphqlConnectContext, ) => RpcLoader< string, MessageShape<typeof UserSchema> | null, [params: BatchGetUsersInTenantLoaderParams] > = createRpcLoader({ service: UserService, method: "batchGetUsersInTenant", requestSchema: BatchGetUsersInTenantRequestSchema, call: (client, keys, params, opts) => client.batchGetUsersInTenant( create(BatchGetUsersInTenantRequestSchema, { ...params, ids: [...keys] }), opts, ), extractEntities: (res) => res.users, extractKey: (user: MessageShape<typeof UserSchema>) => user.id, });
// usage: params is a required trailing argument to load/loadMany await batchGetUsersInTenantLoader(ctx).load("user-1", { tenantId: "acme" });

Here PArgs is [params: BatchGetUsersInTenantLoaderParams] (no ?) — callers must pass { tenantId: "..." } to load/loadMany/loader. This is a type-level requirement only; nothing is validated at runtime (an invalid or empty request is rejected by the server, same as any other RPC call).

Note on LoaderParams’s shape: the params type includes key_field itself (ids above), not Omit<Request, "ids">. This is intentional — protobuf-es v2’s MessageInitShape resolves to a union, and Omit does not distribute over unions cleanly. Passing ids through params is harmless: call always overwrites it with the batch’s actual keys, so any value you pass there is ignored.

The loader() Escape Hatch

Besides load/loadMany, RpcLoader exposes loader(...args), which resolves (creating on first sight) the underlying per-params DataLoader instance directly — useful for .prime() / .clear() / .clearAll():

batchGetUsersWithLocaleLoader(ctx).loader({ locale: "ja" }).prime("user-1", cachedUser);

loader(...) shares the same per-ctx, per-params-value cache as load/loadMany, so priming through loader() is visible to subsequent load() calls with the same params.

Key Type Mapping

The TypeScript key type K in RpcLoader<K, ...> is derived from key_field’s proto element type:

Proto element typeTS key type
stringstring
int32 / uint32 / sint32 / fixed32 / sfixed32number
int64 / uint64 / sint64 / fixed64 / sfixed64bigint
bool / enum / message / bytes / float / doublenot supported as a key (codegen error)

Key-Matching Semantics

Implemented once in @proto-graphql/connect-runtime’s createRpcLoader (not regenerated per RPC), so every loader — regardless of mode — behaves the same way:

  • Missing key: a requested key absent from the response resolves to null (entity mode) or [] (group mode). It never rejects on its own.
  • Response order: entities are matched back to keys by extractKey, not by response order — a shuffled or reordered response still matches correctly.
  • Duplicate keys in the response: entity mode keeps the last matching entity; group mode collects all of them into that key’s array.
  • Extra entities: entities whose key was not requested are ignored (harmless, not an error).
  • RPC error: if the call closure throws or its promise rejects, every key in that batch rejects with the same error — the plugin never converts a transport/RPC error into a per-key null/[], and never translates it into a GraphQL error itself (that is left to the caller, e.g. a future federation resolveReference wiring).
  • params split batches: load/loadMany/loader calls against the same accessor’s wrapper with a different params value (compared by encoding, not by object identity) never share a batch — each distinct params value gets its own DataLoader instance per context, created lazily the first time that value is seen. undefined and {} are treated as equivalent. Deliberately passing different params per key defeats batching (falls back to one RPC call per distinct params value), which is the correct/expected tradeoff, not a bug — this batching physics is unchanged by params moving from accessor time to load time.
  • max_batch_size: when set, DataLoader never sends more than that many keys in one call invocation — a larger set of concurrently-requested keys is automatically split across multiple RPC calls.