Repository Pattern no Node.js organiza o acesso a dados atrás de uma interface orientada ao domínio. Em vez de espalhar SQL, chamadas de ORM ou detalhes de cache pelos serviços, a aplicação depende de operações como buscarPorId, salvar e remover.
O padrão ajuda quando existe lógica de persistência relevante, mais de uma implementação ou necessidade de testes isolados. Ele não deve virar uma camada genérica que apenas repete todos os métodos do ORM.
Neste guia, você aprenderá contratos, implementação com banco, transações, paginação, fakes, erros e boas práticas.
Problema sem repository
export async function ativarUsuario(id, pool) {
const result = await pool.query(
'SELECT * FROM users WHERE id = $1',
[id]
);
if (!result.rows[0]) throw new Error('Não encontrado');
await pool.query(
'UPDATE users SET active = true WHERE id = $1',
[id]
);
}A regra de negócio conhece SQL, estrutura de tabela e formato do driver.
Contrato do repository
export function criarUsuarioService({ userRepository }) {
return {
async ativar(id) {
const user = await userRepository.buscarPorId(id);
if (!user) throw new UserNotFoundError(id);
user.active = true;
await userRepository.salvar(user);
return user;
}
};
}O serviço trabalha com entidades e operações do domínio.
Implementação PostgreSQL
export function criarUsuarioRepository(pool) {
return {
async buscarPorId(id) {
const result = await pool.query(
'SELECT id, email, active FROM users WHERE id = $1',
[id]
);
return result.rows[0] ?? null;
},
async salvar(user) {
await pool.query(
`UPDATE users
SET email = $2, active = $3
WHERE id = $1`,
[user.id, user.email, user.active]
);
}
};
}Mapeamento de dados
Não devolva linhas do banco diretamente quando nomes, tipos ou nulabilidade diferem do domínio:
function mapRow(row) {
return {
id: row.id,
email: row.email,
active: Boolean(row.active),
createdAt: new Date(row.created_at)
};
}Consultas orientadas ao caso de uso
Evite um repository genérico com dezenas de filtros opcionais. Métodos específicos comunicam intenção:
buscarAtivosPorOrganizacao;listarExpiradosAntesDe;reservarParaProcessamento;existeEmail.
Paginação
O contrato deve definir cursor, ordenação e limite. Não exponha detalhes do ORM como contrato público do domínio.
await repository.listar({
after: cursor,
limit: 50,
status: 'active'
});Transações
Uma operação de negócio pode envolver vários repositories. A transação deve ser coordenada pela unidade de trabalho ou pelo serviço de aplicação, sem cada repository abrir sua própria transação.
await database.transaction(async (tx) => {
const users = criarUsuarioRepository(tx);
const audit = criarAuditRepository(tx);
await users.salvar(user);
await audit.registrar(event);
});Repository e cache
Cache pode decorar um repository:
function comCache(repository, cache) {
return {
async buscarPorId(id) {
const cached = await cache.get(id);
if (cached) return cached;
const value = await repository.buscarPorId(id);
if (value) await cache.set(id, value);
return value;
}
};
}Defina invalidação e consistência. Veja Cache HTTP no Node.js e Cache Stampede no Node.js.
Fake em memória
export function criarUsuarioRepositoryFake(initial = []) {
const data = new Map(initial.map((item) => [item.id, structuredClone(item)]));
return {
async buscarPorId(id) {
return structuredClone(data.get(id) ?? null);
},
async salvar(user) {
data.set(user.id, structuredClone(user));
}
};
}O fake deve respeitar o mesmo contrato da implementação real.
Testes de contrato do repository
Execute uma suíte compartilhada contra o fake e o banco real:
- retorna null quando ausente;
- preserva tipos;
- aplica unicidade;
- respeita ordenação;
- trata concorrência;
- faz rollback.
Erros de infraestrutura
Não transforme qualquer erro do banco em “não encontrado”. Preserve a causa e converta somente violações conhecidas para erros do domínio.
Repository com ORM
Mesmo usando ORM, o padrão pode proteger o domínio de detalhes de query. Porém, se a aplicação é CRUD simples e o ORM já é a interface adequada, uma camada extra pode apenas adicionar código.
Dependency Injection
Forneça o repository ao serviço pelo composition root. Consulte Dependency Injection no Node.js.
Erros comuns
- criar repository genérico universal;
- expor objetos do ORM;
- duplicar toda a API do driver;
- abrir transação em cada método;
- esconder queries ineficientes;
- fake com comportamento diferente;
- capturar todos os erros como domínio;
- usar o padrão sem necessidade.
Checklist
- contrato orientado ao domínio;
- mapeamento explícito;
- queries observáveis;
- transações coordenadas;
- paginação definida;
- fake testado;
- erros preservados;
- sem vazamento de ORM.
Conclusão
Repository Pattern no Node.js cria uma fronteira clara entre regras de negócio e persistência. Ele é valioso quando o acesso a dados possui comportamento próprio ou quando a aplicação precisa trocar e testar implementações.
Mantenha contratos pequenos e específicos. O repository deve representar necessidades do domínio, não esconder o banco atrás de uma camada genérica.
Consulte a descrição do padrão Repository por Martin Fowler e a documentação de testes do Node.js.



