How to use GraphQL Dataloaders in NestJS
Solving the N+1 problem for fun and profit
So you've just finished coding your nice, shiny new GraphQL API in NestJS. You launch it into production and... it's a bit sluggish. And there seem to be a lot more queries in the database logs than you'd expect. What's going on?
The N+1 Problem
Let's take a closer look. We're building a blogging API with two entities, Author
and Post
. The GraphQL query to get the feed looks something like this:
query Feed {
posts {
title
content
author {
name
}
}
}
The GraphQL resolver class for Post
will duly fetch a list of posts from the database in a single query. So far, so good. But what about the post authors? The Post
resolver class has a field resolver to fetch the author of a post, and therein lies the problem. If we fetch 10 posts, then we fetch the author of each individual post with a separate database query. That's 11 (10+1 => N+1, see?) separate database queries for a single request! The more posts we fetch, the worse it gets — that's the N+1 problem in a nutshell.
Dataloaders to the Rescue
So how do we solve the N+1 problem? Turns out that being lazy is a big advantage (for once). If we delay fetching authors, we can batch all of them together and fetch everything from the database in a single round trip!
With Facebook's DataLoader library, all of the heavy lifting is done for us. Integrating DataLoader with NestJS is a little more complex. Plenty of packages on npm claim to integrate the two seamlessly, but many aren't maintained, lack essential features and/or aren't compatible with NestJS 8+.
When we ran into this issue at Tracworx, none of the existing packages did a satisfactory job — so we built our own and published it as an open-source project for everyone to use.
Dataloaders in NestJS
Getting started with DataLoaders in your NestJS GraphQL API is a breeze. Don't forget to install the package and add DataloaderModule
to your imports in app.module.ts
first!
$ npm install @tracworx/nestjs-dataloader
import { DataloaderModule } from '@tracworx/nestjs-dataloader';
@Module({
imports: [DataloaderModule],
})
export class AppModule {}
Next, let's make a simple dataloader provider to batch-load Author
entities from the database. The @DataloaderProvider
decorator makes this quick and easy. All we need to do is implement the createDataloader
factory method. If you want to pull in authorization data or other request-scoped info, then you can use the GqlExecutionContext
passed to createDataloader
.
import DataLoader from 'dataloader';
import { DataloaderProvider } from '@tracworx/nestjs-dataloader';
@DataloaderProvider()
class AuthorLoader {
constructor(private readonly authorsService: AuthorsService) {}
createDataloader(ctx: GqlExecutionContext) {
return new DataLoader<string, Item>(async (ids) => {
// Fetch authors from database in a single query
const authors = await this.authorsService.findMany({ id: { in: ids } });
// Map IDs to authors (required by DataLoader library)
return ids.map((id) => authors.find((author) => author.id === id) ?? null);
});
}
}
AuthorLoader
is a NestJS provider — add it to your module's providers
array just like any other injectable. As eagle-eyed readers may notice, this isn't a request-scoped provider, so we deftly avoid the performance penalty.
And that's it! A dataloader instance will be automatically injected into our GraphQL context before we resolve each request. The @Loader
parameter decorator lets us grab this dataloader instance from our resolvers with minimal fuss.
@Resolver(of => Post)
export class PostsResolver {
constructor(private readonly postsService: PostsService) {}
@Query(returns => [Post])
posts(@Args() postsArgs: PostsArgs): Promise<Post[]> {
return this.postsService.findMany(postsArgs);
}
@ResolveField()
async author(@Parent() post: Post, @Loader(AuthorLoader) authorLoader) {
const { authorId } = post;
// Batch query using dataloader
return authorLoader.load(authorId);
}
}
At Tracworx, we used this library to integrate dataloaders across our entire GraphQL backend in December 2021. We got the whole thing done and live in a matter of hours, so we decided to share our work so the community could benefit — happy coding!
Take a look at our source code here: github.com/tracworx/nestjs-dataloader