Data Loaders
DataLoaders batch and cache backend fetches during a single request. Axolotl's Yoga adapter supports per‑request context building so you can attach DataLoader instances safely for every request.
Install DataLoader in your project:
npm i dataloaderQuick Start (Yoga)
- Create a context builder function with DataLoader instances.
src/axolotl.ts
import { Models } from '@/src/models.js';
import { Axolotl } from '@aexol/axolotl-core';
import { graphqlYogaWithContextAdapter } from '@aexol/axolotl-graphql-yoga';
import { YogaInitialContext } from 'graphql-yoga';
import DataLoader from 'dataloader';
import { PrismaClient } from '@prisma/client';
const db = new PrismaClient();
// Example batch function that fetches users by IDs
async function batchUsers(ids: readonly string[]) {
// Replace with your DB call that returns users in the same order as ids
const rows = await db.user.findMany({
where: { id: { in: ids as string[] } },
});
const byId = new Map(rows.map((u) => [u.id, u]));
return ids.map((id) => byId.get(id) || null);
}
// Define your context type with loaders
type AppContext = YogaInitialContext & {
db: PrismaClient;
loaders: {
userById: DataLoader<string, User | null>;
};
};
// Build context per request (DataLoaders are created fresh for each request)
async function buildContext(initial: YogaInitialContext): Promise<AppContext> {
return {
...initial, // ✅ Must spread initial context
db,
loaders: {
userById: new DataLoader(batchUsers),
},
};
}
// Pass buildContext function to graphqlYogaWithContextAdapter
export const { createResolvers, createDirectives, createScalars, adapter } = Axolotl(
graphqlYogaWithContextAdapter<AppContext>(buildContext),
)<Models>();- Use loaders in resolvers via context (
input[2]).
src/resolvers.ts
import { createResolvers } from '@/src/axolotl.js';
export default createResolvers({
Query: {
user: async ([, , ctx], { id }) => {
// Use DataLoader to batch and cache user lookups
return ctx.loaders.userById.load(id);
},
},
Post: {
// Nested resolver - prevent N+1 queries when fetching post authors
author: ([source, , ctx]) => {
return ctx.loaders.userById.load(source.authorId);
},
},
});Complete Example
Here's a full working example with multiple loaders:
src/axolotl.ts
import { Models } from '@/src/models.js';
import { Axolotl } from '@aexol/axolotl-core';
import { graphqlYogaWithContextAdapter } from '@aexol/axolotl-graphql-yoga';
import { YogaInitialContext } from 'graphql-yoga';
import DataLoader from 'dataloader';
import { PrismaClient } from '@prisma/client';
const db = new PrismaClient();
// Batch function for users
async function batchUsers(ids: readonly string[]) {
const users = await db.user.findMany({ where: { id: { in: ids as string[] } } });
const userMap = new Map(users.map((u) => [u.id, u]));
return ids.map((id) => userMap.get(id) || null);
}
// Batch function for posts
async function batchPosts(ids: readonly string[]) {
const posts = await db.post.findMany({ where: { id: { in: ids as string[] } } });
const postMap = new Map(posts.map((p) => [p.id, p]));
return ids.map((id) => postMap.get(id) || null);
}
// Batch function for posts by author ID
async function batchPostsByAuthor(authorIds: readonly string[]) {
const posts = await db.post.findMany({
where: { authorId: { in: authorIds as string[] } },
});
const postsByAuthor = new Map<string, Post[]>();
posts.forEach((post) => {
if (!postsByAuthor.has(post.authorId)) {
postsByAuthor.set(post.authorId, []);
}
postsByAuthor.get(post.authorId)!.push(post);
});
return authorIds.map((id) => postsByAuthor.get(id) || []);
}
type AppContext = YogaInitialContext & {
db: PrismaClient;
userId: string | null;
loaders: {
userById: DataLoader<string, User | null>;
postById: DataLoader<string, Post | null>;
postsByAuthorId: DataLoader<string, Post[]>;
};
};
async function buildContext(initial: YogaInitialContext): Promise<AppContext> {
// Extract user from auth token
const token = initial.request.headers.get('authorization');
const userId = token ? await verifyToken(token) : null;
return {
...initial,
db,
userId,
loaders: {
userById: new DataLoader(batchUsers),
postById: new DataLoader(batchPosts),
postsByAuthorId: new DataLoader(batchPostsByAuthor),
},
};
}
export const { createResolvers, createDirectives, createScalars, adapter } = Axolotl(
graphqlYogaWithContextAdapter<AppContext>(buildContext),
)<Models>();src/resolvers.ts
import { createResolvers } from '@/src/axolotl.js';
export default createResolvers({
Query: {
user: async ([, , ctx], { id }) => {
return ctx.loaders.userById.load(id);
},
post: async ([, , ctx], { id }) => {
return ctx.loaders.postById.load(id);
},
},
User: {
posts: ([source, , ctx]) => {
// Batch load all posts for this user
return ctx.loaders.postsByAuthorId.load(source.id);
},
},
Post: {
author: ([source, , ctx]) => {
// Batch load the author
return ctx.loaders.userById.load(source.authorId);
},
},
});Key Points
- Per-Request Loaders: DataLoader instances are created fresh for each request inside
buildContext. This ensures caching is scoped to a single request. - Batch Functions: Must return results in the same order as input keys. Use a Map to reorder results.
- Context Access: Loaders are accessed via
ctx.loaders(the third elementinput[2]of the resolver tuple). - Type Safety: Define your
AppContexttype to get full TypeScript support for loaders.
Example Batch Function Patterns
Single Record Lookup
async function batchUsers(ids: readonly string[]): Promise<(User | null)[]> {
const users = await db.user.findMany({
where: { id: { in: ids as string[] } },
});
const userMap = new Map(users.map((u) => [u.id, u]));
// ✅ Return in same order as input, null for missing records
return ids.map((id) => userMap.get(id) || null);
}Multiple Records Lookup (One-to-Many)
async function batchPostsByAuthor(authorIds: readonly string[]): Promise<Post[][]> {
const posts = await db.post.findMany({
where: { authorId: { in: authorIds as string[] } },
});
const postsByAuthor = new Map<string, Post[]>();
posts.forEach((post) => {
if (!postsByAuthor.has(post.authorId)) {
postsByAuthor.set(post.authorId, []);
}
postsByAuthor.get(post.authorId)!.push(post);
});
// ✅ Return array of arrays, empty array for authors with no posts
return authorIds.map((id) => postsByAuthor.get(id) || []);
}Troubleshooting
- "Context is reused across requests": Verify you're passing
buildContextfunction tographqlYogaWithContextAdapter, not calling it with empty parentheses(). - "N+1 still happening": Confirm all nested resolvers use loaders from context (
ctx.loaders), not direct DB queries. - "Types don't match": Define your
AppContexttype that extendsYogaInitialContextand pass it tographqlYogaWithContextAdapter<AppContext>(buildContext). - "Wrong order returned": Ensure your batch function returns results in the exact same order as the input keys using a Map to reorder.