DataLoader

A batching and caching layer that coalesces per-request data loads into a fraction of the backend calls.

Library
npm
v2.2.3
13,385 stars
MIT License

Repository Health

Pre-computed score based on development activity, maintenance, community, maturity, and trend momentum. How we score it →
66 /100 Good
Development Activity 60
Maintenance 32
Community 72
Maturity 60
Momentum 40

Technical Analysis

AI-assessed by reading the actual repository — architecture, code quality, innovation, and documentation. How we score it →
86 /100 Excellent
Architecture 85
Code Quality 88
Innovation 92
Learning Curve 80

DataLoader is a generic utility for an application’s data-fetching layer, providing a consistent API over any backend (SQL, key-value stores, REST or gRPC services) via automatic batching and per-request caching. Rather than replacing a database or cache, it sits in front of one: every .load(key) call within a single tick of the event loop is coalesced into a single call to a user-supplied batch function, and the resulting values are memoized for the lifetime of the DataLoader instance so repeated loads of the same key never hit the backend twice.

Originally a port of Facebook’s internal “Loader” abstraction that underpinned its early GraphQL server, DataLoader is the reference implementation the JavaScript GraphQL ecosystem builds on to avoid the N+1 query problem, and it has been reimplemented in a dozen other languages. It ships with zero runtime dependencies, a small five-method API surface (load, loadMany, clear, clearAll, prime), and enough configuration (custom cache maps, cache key functions, batch schedulers, max batch size) to fit almost any backend shape without changing how calling code is written.

What You Get

  • Automatic request-scoped batching that coalesces all .load() calls issued within a single tick of the event loop into one call to your batch function
  • A built-in memoization cache (backed by Map by default, or any custom cache-like object) that eliminates redundant loads for the same key within a request
  • loadMany() for loading multiple keys at once, resolving per-key errors as values instead of rejecting the whole batch
  • prime() to seed the cache ahead of time (e.g. cross-populate a by-ID loader from a by-username loader), and clear()/clearAll() for invalidating stale entries after a mutation
  • Pluggable batchScheduleFn, cacheKeyFn, and cacheMap options for adapting the scheduling and caching strategy to non-default environments (browsers, long-lived processes, object keys)
  • Zero runtime dependencies and both Flow and hand-written TypeScript type definitions shipped in the package

Common Use Cases

  • Resolving GraphQL fields without N+1 database queries, by giving each field resolver a shared per-request loader instead of querying the database directly
  • Deduplicating and batching calls to a REST or gRPC backend service across unrelated parts of a request-handling pipeline
  • Providing a per-request cache in front of SQL, Redis, CouchDB, RethinkDB, or Google Datastore lookups (patterns documented in the project’s examples directory)
  • Loading the same entity by multiple keys (e.g. by ID and by username) while keeping both caches in sync via prime()
  • Invalidating cached reads after a mutation within the same request, using clear()/clearAll() to force a fresh load on subsequent calls

Under The Hood

Architecture

The entire implementation lives in one ~500-line file (src/index.js) that exposes a single DataLoader class. Each instance tracks a current “batch” record (pending keys, resolve/reject callbacks, and cache-hit callbacks) that getCurrentBatch either reuses or creates; the first .load() call on a fresh batch schedules dispatch via a configurable batchScheduleFn, defaulting to enqueuePostPromiseJob, which chains a resolved Promise into process.nextTick (or setImmediate/setTimeout in non-Node environments) so the batch closes only after the current synchronous execution and any already-queued microtasks complete. dispatchBatch enforces the batch-function contract (must return a Promise resolving to an array-like of the same length as the input keys) and resolves or rejects each pending Promise by index, while resolveCacheHits replays cached results in the same tick so cached and freshly-loaded values settle together; failedDispatch centralizes cleanup for both synchronous throws and rejected batch promises, clearing the cache for every affected key and rejecting each callback. The Batch record is the one abstraction everything else depends on — changing its shape would ripple through getCurrentBatch, dispatchBatch, resolveCacheHits, and failedDispatch together.

Tech Stack

Pure JavaScript with Flow (@flow strict) type annotations in the source, compiled through Babel (@babel/core, @babel/preset-env, @babel/preset-flow) into the published dist/ output, alongside a hand-maintained index.d.ts for TypeScript consumers. There are no runtime dependencies beyond the global ES6 Promise and Map. Releases are versioned and changelogged through Changesets and published to npm; CI (GitHub Actions) runs the full lint + flow-check + Jest suite across multiple Node versions via Yarn and reports coverage to Codecov.

Code Quality

The test suite is extensive relative to the implementation size — well over a thousand lines across dedicated files for the core API, abusive/malformed-input scenarios, browser scheduling fallbacks, and unhandled-rejection behavior. Error handling is explicit throughout: constructor and option validators throw descriptive TypeErrors synchronously for misuse, and asynchronous batch failures are funneled through a single failedDispatch path so no request is left hanging. Naming is consistent (underscore-prefixed private fields, method names mirroring Promise.all semantics), and both a static type checker (Flow) and a separately maintained TypeScript declaration file back the public API, enforced together with ESLint and Prettier in CI.

API Design The public surface is deliberately small — five methods cover the whole batching and caching contract, and a DataLoader can be constructed with nothing more than a single batch function; every other behavior (batch size limits, custom scheduling, custom cache keys or cache backends) is opt-in with sensible defaults. Documentation is unusually thorough for a library this size, with a long-form README covering batching, caching, custom schedulers, custom cache implementations, and GraphQL integration, plus a dedicated examples directory walking through SQL, Redis, CouchDB, RethinkDB, Google Datastore, and Knex backends.

Join founders buildingwith open source

Opinionated takes, migration guides, cost-saving tips, and insights from the open source ecosystem.

Subscribe on Substack
Join 750+ subscribers