Home > Blog > Desenvolvimento Web
Desenvolvimento Web
JavaScript
Programação

DataLoader no Node.js: Evite N+1

Atualizado em: 1 de setembro de 2026

Estação de trabalho usada no desenvolvimento de aplicações Node.js

Usar DataLoader no Node.js ajuda a reduzir o problema N+1 em APIs GraphQL, REST, BFFs e serviços que carregam dados relacionados. O DataLoader oferece uma API simples de load(key), agrupa chamadas feitas no mesmo ciclo e executa uma única função batch.

Além de batching, cada instância mantém memoization durante a requisição. Isso evita buscar a mesma chave repetidamente. O cuidado principal é criar loaders por request, preservar ordem e comprimento dos resultados, aplicar autorização e limpar o cache depois de mutations.

Neste guia, você aprenderá a criar batch functions, reordenar resultados, tratar chaves ausentes, usar cache por request, integrar com GraphQL, evitar vazamento entre usuários, limitar batches e testar N+1.

O que é DataLoader?

DataLoader é uma biblioteca genérica de batching e caching. A documentação oficial do DataLoader descreve a API e recomenda criar instâncias por requisição. A documentação de execução GraphQL ajuda a entender por que resolvers independentes podem gerar várias consultas.

O problema N+1

Imagine listar dez pedidos e carregar o cliente de cada um:

const orders = await findOrders();

for (const order of orders) {
  order.customer = await findCustomerById(
    order.customerId
  );
}

O código executa uma consulta para pedidos e dez para clientes: 1 + N.

Consulta batch

SELECT id, name, email
FROM customers
WHERE tenant_id = $1
  AND id = ANY($2::uuid[]);

Uma única consulta carrega todos os IDs necessários.

Instalando

npm install dataloader

Criando o loader

import DataLoader from 'dataloader';

function createCustomerLoader({
  tenantId,
  customerRepository
}) {
  return new DataLoader(async customerIds => {
    const customers = await customerRepository.findByIds({
      tenantId,
      customerIds
    });

    const byId = new Map(
      customers.map(customer => [customer.id, customer])
    );

    return customerIds.map(id =>
      byId.get(id) ?? null
    );
  });
}

Contrato da batch function

A função recebe um array de chaves e deve retornar uma Promise com array:

  • do mesmo comprimento;
  • na mesma ordem;
  • com valor, null ou Error para cada chave.

Ordem dos resultados

O banco pode retornar:

[customer9, customer2, customer1]

para as chaves:

[2, 9, 6, 1]

O loader deve produzir:

[customer2, customer9, null, customer1]

Por que reordenar?

DataLoader associa cada posição ao Promise retornado por load(). Uma ordem errada entrega dados de outro ID.

Chave ausente

Escolha uma semântica:

return ids.map(id =>
  byId.get(id) ?? new Error('Customer not found')
);

Um Error individual rejeita apenas aquele load. Null pode ser adequado quando o campo GraphQL é nullable.

Batching no mesmo ciclo

const first = loader.load('1');
const second = loader.load('2');

const [a, b] = await Promise.all([first, second]);

As duas chamadas são agrupadas antes da função batch executar.

Não use await sequencial

const a = await loader.load('1');
const b = await loader.load('2');

A segunda chamada acontece depois que o primeiro batch termina, reduzindo o agrupamento. Em resolvers independentes, o GraphQL costuma criar concorrência naturalmente.

loadMany

const results = await loader.loadMany([
  '1',
  '2',
  '3'
]);

O resultado pode conter valores e objetos Error. Não assume o mesmo comportamento de Promise.all().

Cache por request

Se load('1') é chamado duas vezes, a mesma Promise é reutilizada durante aquela instância.

Não é Redis

O cache do DataLoader é memoization de curta duração. Ele não substitui um cache compartilhado entre requisições.

Loader global é perigoso

export const userLoader = new DataLoader(batchUsers);

Uma instância global pode manter dados entre usuários, crescer em memória e vazar resultados de autorização diferentes.

Criando por request

app.use((req, res, next) => {
  req.loaders = createLoaders({
    tenantId: req.auth.tenantId,
    userId: req.auth.userId,
    permissions: req.auth.permissions
  });

  next();
});

Contexto GraphQL

const context = async ({ req }) => ({
  auth: req.auth,
  loaders: createLoaders({
    tenantId: req.auth.tenantId,
    repositories
  })
});

Cada operação recebe loaders isolados.

Resolver

const Order = {
  customer(order, args, context) {
    return context.loaders.customer.load(
      order.customerId
    );
  }
};

Multi-tenancy

O tenant precisa fazer parte do loader ou da chave. Nunca compartilhe uma instância entre tenants.

Consulte Multi-Tenancy no Node.js.

Autorização

O batch repository deve aplicar tenant e permissões. DataLoader não autoriza recursos.

Veja RBAC no Node.js e ABAC no Node.js.

Chave composta

{ tenantId, customerId }

Quando usar objetos como chave, configure cacheKeyFn ou use uma string canônica.

new DataLoader(batch, {
  cacheKeyFn: key => `${key.tenantId}:${key.id}`
});

Objetos mutáveis

Evite alterar uma chave depois de chamar load(). Prefira valores primitivos ou objetos imutáveis.

maxBatchSize

new DataLoader(batchCustomers, {
  maxBatchSize: 500
});

Isso evita queries com milhares de parâmetros e limites do backend.

Batch grande

Um request GraphQL complexo pode carregar muitos IDs. Combine limites de consulta, paginação e maxBatchSize.

Custom scheduler

DataLoader permite ajustar quando o batch executa. Uma janela maior agrupa mais chamadas, mas adiciona latência.

Use o padrão antes de criar scheduler customizado.

Cache depois de mutation

await updateCustomer(id, input);
context.loaders.customer.clear(id);

Se o valor já foi carregado, o cache fica desatualizado dentro da mesma operação.

clear e prime

loader
  .clear(updated.id)
  .prime(updated.id, updated);

Isso substitui o valor sem nova consulta.

clearAll

Use quando uma mutation invalida muitos valores e você não conhece as chaves. Evite limpar desnecessariamente, pois reduz batching e cache.

Erros em cache

Um Error individual pode ser memoizado. Se o erro é transitório, limpe a chave antes de tentar novamente.

Batch rejeitado

Quando a batch function lança ou rejeita, os valores da chamada não são cacheados como resultados individuais.

Cache desabilitado

new DataLoader(batch, { cache: false });

Chaves duplicadas podem chegar à batch function. Ela deve retornar um resultado para cada ocorrência.

Cache customizado

Instâncias longas podem usar LRU, mas em servidor web a melhor opção geralmente é lifecycle por request.

REST e BFF

DataLoader não é exclusivo de GraphQL. Um BFF pode agrupar chamadas feitas por componentes de uma resposta.

Consulte Backend for Frontend no Node.js.

Batch de API externa

Se um serviço aceita POST /users/batch, o loader pode agrupar IDs. Defina timeout, limite e tratamento parcial.

N+1 em banco

DataLoader reduz round-trips, mas uma query JOIN ou modelo de leitura pode ser mais eficiente para relatórios.

JOIN versus loader

  • JOIN: excelente quando a relação é conhecida.
  • DataLoader: útil quando campos são resolvidos dinamicamente.
  • Projeção: útil para consultas frequentes entre serviços.

CQRS

Quando uma tela exige composição complexa sempre, um read model pode ser melhor. Consulte CQRS no Node.js.

Paginação

Não carregue milhares de relações em um campo sem paginação. Veja Paginação em APIs Node.js.

Cache compartilhado

Um Redis pode guardar dados entre requests. O loader continua útil para deduplicar chamadas dentro da operação.

Invalidação

O cache compartilhado exige TTL e eventos. O cache do DataLoader desaparece no fim do request.

Observabilidade

Registre nome do loader, quantidade de chaves, batch size, duração e falhas.

logger.debug({
  loader: 'customer',
  batchSize: ids.length,
  durationMs
}, 'Batch executado');

Métricas

Monitore:

  • batches por request;
  • tamanho médio;
  • cache hit;
  • latência;
  • erros;
  • IDs ausentes;
  • queries ao banco.

Tracing

Crie um span por batch, não por cada load(), para evitar ruído.

Logs

Não registre arrays enormes de IDs. Use quantidade e amostra limitada. Consulte Logs com Pino no Node.js.

Testando ordem

test('preserva a ordem das chaves', async () => {
  repository.findByIds.mockResolvedValue([
    { id: '2' },
    { id: '1' }
  ]);

  const result = await loader.loadMany(['1', '2']);

  assert.equal(result[0].id, '1');
  assert.equal(result[1].id, '2');
});

Testando chave ausente

Inclua um ID inexistente e confirme null ou Error na posição correta.

Testando batching

Faça várias chamadas no mesmo tick e confirme que o repository executa uma vez.

Testando cache

Carregue a mesma chave duas vezes e confirme uma única consulta.

Testando isolamento

Crie dois contexts para tenants diferentes e confirme que os loaders não compartilham cache.

Testando mutation

Carregue, atualize, limpe e carregue novamente. O segundo valor deve refletir a mutation.

Teste de maxBatchSize

Envie mais chaves que o limite e confirme divisão em batches controlados.

Benchmark

Compare consultas e latência antes e depois. DataLoader não corrige automaticamente queries lentas.

Erros comuns

  • Loader global: cache vaza entre usuários.
  • Não reordenar: dados são associados ao ID errado.
  • Array com tamanho diferente: Promises recebem resultado incorreto.
  • Ignorar tenant: dados cruzam organizações.
  • Cache após mutation: resposta fica antiga.
  • Batch infinito: query excede limites.
  • Usar para tudo: JOIN simples vira complexidade extra.

Boas práticas

  • Crie loaders por request.
  • Inclua tenant e autorização.
  • Preserve ordem e comprimento.
  • Use Map para reordenar.
  • Limite batch size.
  • Limpe após mutations.
  • Use paginação.
  • Meça queries e latência.
  • Não trate cache como Redis.
  • Teste isolamento.

Conclusão

Usar DataLoader no Node.js reduz N+1 ao agrupar loads e evitar leituras duplicadas dentro de uma requisição. A API mantém resolvers simples sem multiplicar round-trips.

A implementação correta depende de batch functions que preservam ordem, loaders por request e autorização no backend. Com limites, limpeza após mutations e observabilidade, DataLoader melhora desempenho sem transformar um cache temporário em fonte de dados desatualizados ou vazamento entre usuários.

10 melhores cursos de programação em 2026

Descubra os melhores cursos de programação. Aprenda a escolher o curso ideal para iniciar ou avançar na carreira de desenvolvedor

POSTS RELACIONADOS

Ver todos

Seta para a direita