How to use GraphQL Dataloaders in NestJS

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