Apollo Server 5 resolvers with per-request DataLoader batching: working versions (tested 2026-10-15)
An Apollo Server 5 schema whose nested resolvers use per-request DataLoader instances to batch and dedupe lookups (no N+1), with GraphQLError extension codes and cache invalidation after a mutation, tested in-process with executeOperation.
passed5 passed · 0 failed2026-10-15 14:10 UTC- Status
- passed
- Last passed
- 2026-10-15 14:10 UTC
- Tests
- 5 / 5
- Language
- TypeScript
- Runtime
- node v22.23.3
npm install --no-audit --no-fund --ignore-scripts=false npm test
node v22.23.3 linux 6.17.0-1022-azure x64 runner: github-actions 38748988915
The code
Exactly the files that passed. Your agent can pull them with get_recipe("apollo-server-dataloader-batching") (install), or see them on GitHub.
{
"name": "apollo-server-dataloader-batching",
"private": true,
"type": "module",
"scripts": {
"test": "node --import tsx --test test/*.test.ts"
},
"dependencies": {
"@apollo/server": "5.5.1",
"graphql": "16.14.2",
"dataloader": "2.2.3"
},
"devDependencies": {
"tsx": "4.23.15",
"@types/node": "26.6.5"
}
}
import { ApolloServer } from "@apollo/server";
import { GraphQLError } from "graphql";
import DataLoader from "dataloader";
type Author = { id: string; name: string };
type Book = { id: string; title: string; authorId: string };
export const typeDefs = `#graphql
type Author { id: ID!, name: String!, books: [Book!]! }
type Book { id: ID!, title: String!, author: Author! }
type Query { books: [Book!]!, author(id: ID!): Author }
type Mutation { addBook(title: String!, authorId: ID!): Book! }
`;
/** Fake data source that records every batch call so tests can prove N+1 is gone. */
export function createDb() {
const authors: Author[] = [{ id: "a1", name: "Ursula" }, { id: "a2", name: "Octavia" }, { id: "a3", name: "Iain" }];
const books: Book[] = [
{ id: "b1", title: "The Dispossessed", authorId: "a1" },
{ id: "b2", title: "Kindred", authorId: "a2" },
{ id: "b3", title: "The Left Hand of Darkness", authorId: "a1" },
{ id: "b4", title: "Consider Phlebas", authorId: "a3" },
];
const calls: string[][] = [];
return {
books, calls,
authorsByIds: async (ids: readonly string[]) => { calls.push([...ids]); return ids.map((id) => authors.find((a) => a.id === id) ?? new Error(`author ${id} not found`)); },
booksByAuthorIds: async (ids: readonly string[]) => ids.map((id) => books.filter((b) => b.authorId === id)),
};
}
export type Db = ReturnType<typeof createDb>;
/** Loaders are created per request, so the cache never leaks between users. */
export function createContext(db: Db, user?: string) {
return {
db, user,
loaders: {
author: new DataLoader<string, Author>((ids) => db.authorsByIds(ids)),
booksByAuthor: new DataLoader<string, Book[]>((ids) => db.booksByAuthorIds(ids)),
},
};
}
export type Ctx = ReturnType<typeof createContext>;
export const resolvers = {
Query: {
books: (_: unknown, __: unknown, ctx: Ctx) => ctx.db.books,
author: (_: unknown, { id }: { id: string }, ctx: Ctx) => ctx.loaders.author.load(id).catch(() => null),
},
Book: { author: (b: Book, _: unknown, ctx: Ctx) => ctx.loaders.author.load(b.authorId) },
Author: { books: (a: Author, _: unknown, ctx: Ctx) => ctx.loaders.booksByAuthor.load(a.id) },
Mutation: {
addBook: async (_: unknown, args: { title: string; authorId: string }, ctx: Ctx) => {
if (!ctx.user) throw new GraphQLError("sign in first", { extensions: { code: "UNAUTHENTICATED" } });
await ctx.loaders.author.load(args.authorId).catch(() => {
throw new GraphQLError("unknown author", { extensions: { code: "BAD_USER_INPUT", field: "authorId" } });
});
const book = { id: `b${ctx.db.books.length + 1}`, ...args };
ctx.db.books.push(book);
ctx.loaders.booksByAuthor.clear(args.authorId); // invalidate the per-request cache after a write
return book;
},
},
};
export const createServer = () => new ApolloServer<Ctx>({ typeDefs, resolvers });
import { test } from "node:test";
import assert from "node:assert/strict";
import { createServer, createDb, createContext } from "../src/server.ts";
/** executeOperation runs the full request pipeline in-process, no HTTP server needed. */
async function exec(query: string, variables?: Record<string, unknown>, user?: string, db = createDb()) {
const server = createServer();
const res = await server.executeOperation({ query, variables }, { contextValue: createContext(db, user) });
await server.stop();
assert.equal(res.body.kind, "single");
return { result: JSON.parse(JSON.stringify((res.body as { singleResult: unknown }).singleResult)) as { data?: any; errors?: any[] }, db };
}
test("resolves nested authors with one batched lookup instead of N+1", async () => {
const { result, db } = await exec("{ books { title author { name } } }");
assert.equal(result.errors, undefined);
assert.equal(result.data.books.length, 4);
assert.equal(result.data.books[1].author.name, "Octavia");
assert.deepEqual(db.calls, [["a1", "a2", "a3"]]); // deduped and batched in one tick
});
test("author with books, and null for an unknown id", async () => {
const { result } = await exec(`query($id: ID!) { a: author(id: $id) { name books { title } } b: author(id: "zz") { name } }`, { id: "a1" });
assert.deepEqual(result.data.a.books.map((b: { title: string }) => b.title), ["The Dispossessed", "The Left Hand of Darkness"]);
assert.equal(result.data.b, null);
});
test("mutation requires a user and reports error codes in extensions", async () => {
const anon = await exec(`mutation { addBook(title: "X", authorId: "a1") { id } }`);
assert.equal(anon.result.errors?.[0].extensions.code, "UNAUTHENTICATED");
const bad = await exec(`mutation { addBook(title: "X", authorId: "nope") { id } }`, undefined, "ada");
assert.equal(bad.result.errors?.[0].extensions.code, "BAD_USER_INPUT");
assert.equal(bad.result.errors?.[0].extensions.field, "authorId");
});
test("mutation writes and clears the loader cache so reads see the new book", async () => {
const db = createDb();
const server = createServer();
const contextValue = createContext(db, "ada");
const read = `{ author(id: "a3") { books { title } } }`;
const before = await server.executeOperation({ query: read }, { contextValue });
const added = await server.executeOperation({ query: `mutation { addBook(title: "Excession", authorId: "a3") { id author { name } } }` }, { contextValue });
const after = await server.executeOperation({ query: read }, { contextValue });
await server.stop();
// graphql-js builds results with null prototypes; a JSON round trip makes them plain objects
const data = (r: typeof before) => JSON.parse(JSON.stringify((r.body as any).singleResult.data));
assert.equal(data(before).author.books.length, 1);
assert.deepEqual(data(added).addBook, { id: "b5", author: { name: "Iain" } });
assert.deepEqual(data(after).author.books.map((b: { title: string }) => b.title), ["Consider Phlebas", "Excession"]);
});
test("validation errors come back as GRAPHQL_VALIDATION_FAILED", async () => {
const { result } = await exec("{ books { isbn } }");
assert.equal(result.errors?.[0].extensions.code, "GRAPHQL_VALIDATION_FAILED");
});
Connect to ProvenPin
Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.
Related stacks
GraphQL Yoga 5 schema, context and errors tested via fetch
Build a GraphQL Yoga server with SDL resolvers, per-request context auth and GraphQLError codes, tested in-process with yoga.fetch.
graphql-request client with typed operations, mocked by MSW graphql handlers
Call a GraphQL API with graphql-request (variables, headers, error handling) and test offline with MSW's graphql.link(url).query/mutation handlers (msw 3 API). graphql stays on 16.x: graphql-request 7 peers on graphql 14 - 16.
Pothos GraphQL schema served by GraphQL Yoga
Build a typed GraphQL schema with Pothos (queries and mutations over an in-memory store) and execute it offline through GraphQL Yoga's fetch handler.
All @apollo/server recipesAll graphql recipesAll dataloader recipes