CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-dataloader

A data loading utility to reduce requests to a backend via batching and caching.

Pending

Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Pending

The risk profile of this skill

Overview
Eval results
Files

index.mddocs/

DataLoader

DataLoader is a generic utility to be used as part of your application's data fetching layer to provide a simplified and consistent API over various remote data sources such as databases or web services via batching and caching.

Originally developed at Facebook, DataLoader solves the N+1 query problem by automatically batching individual data requests into efficient batch operations and caching results to prevent duplicate requests within the same application cycle.

Package Information

  • Package Name: dataloader
  • Package Type: npm
  • Language: JavaScript with Flow type annotations and TypeScript definitions
  • Installation: npm install dataloader

Core Imports

const DataLoader = require('dataloader');

For TypeScript/ES6:

import DataLoader from 'dataloader';
// Or for CommonJS export compatibility:
import DataLoader = require('dataloader');

Basic Usage

const DataLoader = require('dataloader');

// Create a batch loading function
async function batchGetUsers(userIds) {
  const users = await db.query('SELECT * FROM users WHERE id IN (?)', [userIds]);
  // Return results in same order as input keys
  return userIds.map(id => users.find(user => user.id === id) || new Error(`User ${id} not found`));
}

// Create a DataLoader instance
const userLoader = new DataLoader(batchGetUsers);

// Load individual users - these will be batched automatically
const user1 = await userLoader.load(1);
const user2 = await userLoader.load(2);

// Or load multiple users at once
const [user3, user4] = await userLoader.loadMany([3, 4]);

Architecture

DataLoader is built around several key concepts:

  • Batching: Automatically combines multiple individual load requests into batch requests within a single execution frame
  • Caching: Memoizes results to avoid duplicate requests within the same DataLoader instance
  • Request Coalescing: Multiple calls to load the same key return the same Promise instance
  • Error Handling: Individual errors in batch results are isolated and don't affect other keys in the batch
  • Lazy Execution: Batch requests are scheduled using the event loop to capture all requests in the current execution frame

Capabilities

DataLoader Constructor

Creates a new DataLoader instance with the specified batch loading function and optional configuration.

/**
 * Creates a new DataLoader instance
 * @param batchLoadFn - Function that loads multiple keys and returns Promise of results
 * @param options - Optional configuration object
 */
constructor<K, V, C = K>(
  batchLoadFn: BatchLoadFn<K, V>,
  options?: Options<K, V, C>
): DataLoader<K, V, C>;

type BatchLoadFn<K, V> = (keys: ReadonlyArray<K>) => Promise<ArrayLike<V | Error>>;

interface Options<K, V, C = K> {
  /** Enable/disable batching (default: true) */
  batch?: boolean;
  /** Maximum keys per batch (default: Infinity) */
  maxBatchSize?: number;
  /** Custom batch scheduling function */
  batchScheduleFn?: (callback: () => void) => void;
  /** Enable/disable caching (default: true) */
  cache?: boolean;
  /** Function to generate cache keys from load keys */
  cacheKeyFn?: (key: K) => C;
  /** Custom cache implementation (default: new Map()) */
  cacheMap?: CacheMap<C, Promise<V>> | null;
  /** Name for this DataLoader instance (useful for APM tools) */
  name?: string | null;
}

interface CacheMap<K, V> {
  get(key: K): V | void;
  set(key: K, value: V): any;
  delete(key: K): any;
  clear(): any;
}

Usage Examples:

// Basic usage
const userLoader = new DataLoader(async (userIds) => {
  const users = await getUsersByIds(userIds);
  return userIds.map(id => users[id] || new Error(`User ${id} not found`));
});

// With options
const userLoader = new DataLoader(batchLoadFn, {
  batch: true,
  maxBatchSize: 100,
  cache: true,
  cacheKeyFn: key => key.toString(),
  name: 'UserLoader'
});

// Disable batching (equivalent to maxBatchSize: 1)
const immediateLoader = new DataLoader(batchLoadFn, { batch: false });

// Custom cache
const userLoader = new DataLoader(batchLoadFn, {
  cacheMap: new Map() // or null to disable caching
});

Load Single Key

Loads a single key, returning a Promise for the associated value. Requests are automatically batched with other concurrent load requests.

/**
 * Loads a key, returning a Promise for the value represented by that key
 * @param key - The key to load
 * @returns Promise that resolves to the value for the given key
 * @throws TypeError if key is null or undefined
 */
load(key: K): Promise<V>;

Usage Examples:

// Single load
const user = await userLoader.load(123);

// Multiple concurrent loads (automatically batched)
const [user1, user2, user3] = await Promise.all([
  userLoader.load(1),
  userLoader.load(2),
  userLoader.load(3)
]);

// Error handling
try {
  const user = await userLoader.load(999);
} catch (error) {
  console.log('User not found:', error.message);
}

Load Multiple Keys

Loads multiple keys, promising an array of values or Error instances. Unlike Promise.all(), this method always resolves - individual errors become Error instances in the result array.

/**
 * Loads multiple keys, promising an array of values or Error instances
 * @param keys - Array-like object containing keys to load
 * @returns Promise that resolves to array of values or Error instances
 * @throws TypeError if keys is not array-like
 */
loadMany(keys: ArrayLike<K>): Promise<Array<V | Error>>;

Usage Examples:

// Load multiple users
const results = await userLoader.loadMany([1, 2, 3, 999]);
// results might be: [User{id:1}, User{id:2}, User{id:3}, Error('User 999 not found')]

// Handle mixed results
const [user1, user2, user3, error] = await userLoader.loadMany([1, 2, 3, 999]);
if (error instanceof Error) {
  console.log('Failed to load user 999:', error.message);
}

// Filter out errors
const users = results.filter(result => !(result instanceof Error));

Clear Cache Entry

Clears the value at a specific key from the cache, if it exists. Returns the DataLoader instance for method chaining.

/**
 * Clears the value at key from the cache, if it exists
 * @param key - The key to clear from cache
 * @returns This DataLoader instance for method chaining
 */
clear(key: K): this;

Usage Examples:

// Clear a specific user from cache
userLoader.clear(123);

// Method chaining
userLoader
  .clear(1)
  .clear(2)
  .clear(3);

// Clear after data mutation
async function updateUser(id, updates) {
  const user = await db.updateUser(id, updates);
  userLoader.clear(id); // Remove stale cached data
  return user;
}

Clear All Cache

Clears the entire cache. Useful when some event results in unknown invalidations across the DataLoader. Returns the DataLoader instance for method chaining.

/**
 * Clears the entire cache
 * @returns This DataLoader instance for method chaining
 */
clearAll(): this;

Usage Examples:

// Clear all cached data
userLoader.clearAll();

// Clear cache after bulk operations
async function bulkUpdateUsers(updates) {
  await db.bulkUpdate(updates);
  userLoader.clearAll(); // Invalidate all cached users
}

// Periodic cache clearing
setInterval(() => {
  userLoader.clearAll();
}, 300000); // Clear cache every 5 minutes

Prime Cache

Adds the provided key and value to the cache. If the key already exists, no change is made. Returns the DataLoader instance for method chaining.

/**
 * Adds the provided key and value to the cache
 * @param key - The key to prime
 * @param value - The value, Promise, or Error to associate with the key
 * @returns This DataLoader instance for method chaining
 */
prime(key: K, value: V | PromiseLike<V> | Error): this;

Usage Examples:

// Prime cache with known data
const newUser = await createUser({ name: 'Alice' });
userLoader.prime(newUser.id, newUser);

// Prime with error for known invalid keys
userLoader.prime(0, new Error('Invalid user ID'));

// Prime with promise
const userPromise = db.findUser(123);
userLoader.prime(123, userPromise);

// Method chaining
userLoader
  .prime(1, user1)
  .prime(2, user2)
  .prime(3, user3);

Instance Properties

Name Property

The name given to this DataLoader instance, useful for APM tools and debugging.

/**
 * The name given to this DataLoader instance
 * Useful for APM tools and debugging
 */
name: string | null;

Usage Examples:

// Set name in constructor
const userLoader = new DataLoader(batchLoadFn, { name: 'UserLoader' });
console.log(userLoader.name); // 'UserLoader'

// Access for debugging
function debugLoader(loader) {
  console.log(`DataLoader ${loader.name || 'unnamed'} cache status`);
}

Error Handling

DataLoader provides several error handling mechanisms:

Batch Function Errors

// Individual errors in batch results
async function batchLoadUsers(ids) {
  const users = await db.getUsers(ids);
  return ids.map(id => {
    const user = users.find(u => u.id === id);
    return user || new Error(`User ${id} not found`);
  });
}

// Synchronous errors in batch function
const loader = new DataLoader((keys) => {
  if (keys.length > 100) {
    throw new Error('Too many keys requested');
  }
  return batchLoad(keys);
});

Load Errors

try {
  const user = await userLoader.load(invalidId);
} catch (error) {
  // Handle individual load errors
  console.error('Failed to load user:', error);
}

// With loadMany, errors are returned as Error instances
const results = await userLoader.loadMany([1, 2, invalidId]);
results.forEach((result, index) => {
  if (result instanceof Error) {
    console.error(`Failed to load key ${keys[index]}:`, result.message);
  }
});

Common Patterns

Database Integration

// SQL with proper ordering
const userLoader = new DataLoader(async (userIds) => {
  const query = 'SELECT * FROM users WHERE id IN (?)';
  const users = await db.query(query, [userIds]);
  
  // Return results in same order as input keys
  return userIds.map(id => 
    users.find(user => user.id === id) || new Error(`User ${id} not found`)
  );
});

// Using with GraphQL resolvers
const resolvers = {
  Post: {
    author: (post) => userLoader.load(post.authorId),
  },
  Comment: {
    author: (comment) => userLoader.load(comment.authorId),
  }
};

Request-Scoped Loaders

// Express middleware
app.use((req, res, next) => {
  req.loaders = {
    user: new DataLoader(batchLoadUsers),
    post: new DataLoader(batchLoadPosts)
  };
  next();
});

// Use in routes
app.get('/posts/:id', async (req, res) => {
  const post = await req.loaders.post.load(req.params.id);
  const author = await req.loaders.user.load(post.authorId);
  res.json({ ...post, author });
});

Custom Cache Keys

// Case-insensitive user lookup
const userByEmailLoader = new DataLoader(
  async (emails) => {
    const users = await db.getUsersByEmail(emails);
    return emails.map(email => users[email.toLowerCase()] || null);
  },
  {
    cacheKeyFn: email => email.toLowerCase()
  }
);

docs

index.md

tile.json