typescript 36 lines · 7 steps

Batching queries with DataLoader in NestJS

A request-scoped provider that collapses many user lookups into a single batched database query.

Explained by highlit
1import { Injectable, Scope } from '@nestjs/common';
2import DataLoader from 'dataloader';
3import { InjectRepository } from '@nestjs/typeorm';
4import { In, Repository } from 'typeorm';
5import { User } from './entities/user.entity';
6 
7@Injectable({ scope: Scope.REQUEST })
8export class UserLoader {
9 private readonly byId: DataLoader<string, User>;
10 
11 constructor(
12 @InjectRepository(User)
13 private readonly users: Repository<User>,
14 ) {
15 this.byId = new DataLoader<string, User>(
16 async (ids) => this.batchLoad(ids),
17 { maxBatchSize: 100 },
18 );
19 }
20 
21 load(id: string): Promise<User> {
22 return this.byId.load(id);
23 }
24 
25 loadMany(ids: readonly string[]): Promise<(User | Error)[]> {
26 return this.byId.loadMany(ids);
27 }
28 
29 private async batchLoad(ids: readonly string[]): Promise<(User | Error)[]> {
30 const rows = await this.users.findBy({ id: In([...ids]) });
31 const map = new Map(rows.map((row) => [row.id, row]));
32 return ids.map(
33 (id) => map.get(id) ?? new Error(`User ${id} not found`),
34 );
35 }
36}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1DataLoader coalesces individual lookups fired in the same tick into one batched query, killing N+1 patterns.
  2. 2The batch function must return results in the exact order of the requested keys, filling gaps explicitly.
  3. 3Request scope gives each request its own loader instance, so DataLoader's cache never leaks between users.

Related explainers

typescript
import { registerLocaleData } from '@angular/common';
import localeFr from '@angular/common/locales/fr';
import localeFrExtra from '@angular/common/locales/extra/fr';
import localeDe from '@angular/common/locales/de';

Locale-aware bootstrapping in Angular

i18n localization dependency-injection
Intermediate 8 steps
typescript
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import * as Joi from 'joi';
 

Validating env config at boot in NestJS

configuration schema-validation environment-variables
Intermediate 8 steps
java
@Component
@Converter
public class EncryptedStringConverter implements AttributeConverter<String, String> {
 

Transparent column encryption in Spring & JPA

encryption aes-gcm jpa-converter
Advanced 10 steps
typescript
import { Inject, Injectable, Logger } from '@nestjs/common';
import { CACHE_MANAGER } from '@nestjs/cache-manager';
import { Cache } from 'cache-manager';
import { InjectRepository } from '@nestjs/typeorm';

A cache-aside country lookup in NestJS

cache-aside dependency-injection batch-lookup
Intermediate 8 steps
java
package com.acme.billing.config;
 
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;

Feature-flagged beans with Spring @ConditionalOnProperty

feature-flags conditional-beans strategy-pattern
Intermediate 5 steps
typescript
import { Injectable, effect, signal, computed } from '@angular/core';
 
interface Preferences {
  theme: 'light' | 'dark';

A signal-based preferences store in Angular

signals state-management persistence
Intermediate 7 steps

Share this explainer

Here's the card — post it anywhere.

Batching queries with DataLoader in NestJS — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code