npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@maiddev/domain-composition-root

v1.0.3

Published

Type-safe DDD composition root for TypeScript

Readme

@maiddev/domain-composition-root

Um Composition Root tipado para organizar e expor o domínio da sua aplicação como uma estrutura navegável.


🧠 Por que esse package existe?

Em aplicações grandes, o domínio rapidamente deixa de ser apenas um conjunto de classes.

Você começa com:

Aluno
 ├── LancarNotaCommand
 ├── BuscarAlunoQuery
 └── ...

Depois aparecem dezenas ou centenas de:

  • Aggregates
  • Commands
  • Queries
  • Repositories
  • Services
  • Policies
  • Use Cases
  • Eventos

E surge uma pergunta simples:

Onde está cada coisa do meu domínio?

Normalmente, a resposta está espalhada por imports, pastas e arquivos.

Este package propõe uma abordagem diferente:

O domínio também pode ser um objeto.

Por exemplo:

domain.context.aggregates.alunos.commands.lancarNota
domain.context.aggregates.alunos.queries.buscarPorId
domain.context.queries.buscarAlunoPorId

Ou seja, o Composition Root passa a representar explicitamente a arquitetura do sistema.


🎯 O objetivo

O objetivo é criar um mapa tipado do domínio da aplicação.

Você registra seus componentes:

const domain = new DomainCompositionRoot({
    aggregates: {
        alunos: {
            commands: {
                lancarNota: new LancarNotaCommand(repository),
            },
        },
    },

    queries: {
        buscarAlunoPorId: new BuscarAlunoPorIdQuery(repository),
    },
});

E passa a ter acesso a eles através de uma estrutura organizada:

domain.context.aggregates.alunos.commands.lancarNota

ou:

domain.context.queries.buscarAlunoPorId

O TypeScript conhece essa estrutura.

Isso significa que você ganha:

  • autocomplete;
  • navegação pelo domínio;
  • segurança de tipos;
  • composição explícita;
  • um único ponto de entrada para o domínio;
  • uma representação estrutural da arquitetura.

📦 Instalação

npm install @maiddev/domain-composition-root

🧠 Conceitos básicos

| Peça | O que é | Contrato | |---|---|---| | CommandInterface | Uma ação que altera algo (ex: lançar uma nota) | execute(input): output | | QueryInterface | Uma ação que só algo (ex: buscar um aluno) | run(input): output | | AggregateInterface | Um agrupamento de comandos e queries de um mesmo assunto (ex: alunos) | { commands?, queries? } | | DomainContext | O "mapa" geral do seu domínio, com todos os agregados e queries soltas | { aggregates?, queries? } | | DomainCompositionRoot | A classe que junta tudo isso e te dá o domain.context pra usar | — |


🚀 Uso básico

1. Crie o contrato do seu repositório

// alunos.repository.contract.ts
export interface AlunosRepositoryContract {
  lancarNota(input: { alunoId: string; periodo: string; disciplinaId: string; valor: number }): void;
  buscarPorId(input: { alunoId: string }): any;
}

2. Implemente o repositório de verdade

// alunos.repository.postgres.ts
import type { AlunosRepositoryContract } from "./alunos.repository.contract.js";

export class AlunosRepositoryPostgres implements AlunosRepositoryContract {
  lancarNota(input: { alunoId: string; periodo: string; disciplinaId: string; valor: number }): void {
    // sua lógica de banco aqui
  }

  buscarPorId(input: { alunoId: string }) {
    // sua lógica de banco aqui
  }
}

3. Crie seus comandos e queries

// lancar-nota.command.ts
import { z } from "zod";
import type { CommandInterface } from "@maiddev/domain-composition-root";
import type { AlunosRepositoryContract } from "../alunos.repository.contract.js";

const inputSchema = z.object({
  alunoId: z.string(),
  periodo: z.string(),
  disciplinaId: z.string(),
  valor: z.number(),
});

export class LancarNotaCommand implements CommandInterface {
  constructor(private readonly alunosRepository: AlunosRepositoryContract) {}

  execute(input: z.output<typeof inputSchema>) {
    const parsedInput = inputSchema.parse(input);
    this.alunosRepository.lancarNota(parsedInput);
    return { sucesso: true };
  }
}
// buscar-aluno.query.ts
import { z } from "zod";
import type { QueryInterface } from "@maiddev/domain-composition-root";
import type { AlunosRepositoryContract } from "../alunos.repository.contract.js";

const inputSchema = z.object({ alunoId: z.string() });

export class BuscarAlunoPorIdQuery implements QueryInterface {
  constructor(private readonly alunosRepository: AlunosRepositoryContract) {}

  run(input: z.output<typeof inputSchema>) {
    const parsedInput = inputSchema.parse(input);
    return this.alunosRepository.buscarPorId(parsedInput);
  }
}

4. Monte o seu domínio 🎉

import { DomainCompositionRoot } from "@maiddev/domain-composition-root";
import { AlunosRepositoryPostgres } from "./entities/aluno/alunos.repository.postgres.js";
import { LancarNotaCommand } from "./entities/aluno/commands/lancar-nota.command.js";
import { BuscarAlunoPorIdQuery } from "./entities/aluno/queries/buscar-aluno.query.js";

const alunosRepository = new AlunosRepositoryPostgres();

const domain = new DomainCompositionRoot({
  aggregates: {
    alunos: {
      commands: {
        lancarNota: new LancarNotaCommand(alunosRepository),
      },
      queries: {
        buscarAlunoPorId: new BuscarAlunoPorIdQuery(alunosRepository),
      },
    },
  },
});

// E é só usar, com autocomplete e tipos corretos:
domain.context.aggregates.alunos.commands.lancarNota.execute({
  alunoId: "akj27h4",
  disciplinaId: "dsp-098",
  periodo: "Semestre 2",
  valor: 9.9,
});

domain.context.aggregates.alunos.queries.buscarAlunoPorId.run({
  alunoId: "akj27h4",
});

🔎 Queries dentro de um agregado

Além de comandos, um agregado também pode ter suas próprias queries. Basta declarar a chave queries junto de commands na hora de registrar o agregado:

const domain = new DomainCompositionRoot({
  aggregates: {
    alunos: {
      commands: {
        lancarNota: new LancarNotaCommand(alunosRepository),
      },
      queries: {
        buscarPorId: new BuscarAlunoPorIdQuery(alunosRepository),
      },
    },
  },
});

// A query fica "dentro" do agregado, junto com os comandos:
domain.context.aggregates.alunos.queries.buscarPorId.run({
  alunoId: "akj27h4",
});

Repare que isso é diferente das queries globais (seção abaixo): aqui a query pertence diretamente ao agregado alunos, então ela fica acessível em aggregates.alunos.queries, e não em context.queries.

Você pode inclusive ter os dois ao mesmo tempo — uma query específica do agregado e uma query global equivalente, caso queira expor a mesma busca de duas formas diferentes:

domain.context.aggregates.alunos.queries.buscarPorId.run({ alunoId: "akj27h4" }); // pelo agregado
domain.context.queries.buscarAlunoPorId.run({ alunoId: "akj27h4" });              // globalmente

💡 Diferente de register_query (que só existe para o contexto raiz), hoje as queries de um agregado só podem ser definidas na hora da criação do DomainCompositionRoot — não existe ainda um register_query equivalente por agregado.


🔗 Registrando queries globais

Nem toda query precisa pertencer a um agregado. Para queries "globais" (fora de qualquer aggregate), use register_query.

⚠️ Atenção: register_query não muda o objeto original — ele retorna uma nova instância de DomainCompositionRoot com o tipo já atualizado. Por isso, a chamada precisa ser encadeada direto na criação do DomainCompositionRoot. Reatribuir o resultado a uma variável (domain = domain.register_query(...)) não resolve — veja o motivo logo abaixo.

// ❌ Não funciona — o domain original não é alterado
let domain = new DomainCompositionRoot({
    aggregates: {
        alunos: {
            commands: {
                lancarNota: new LancarNotaCommand(container.repositories.alunosRepositoryPostgres),
            },
        }
    },
    queries: {
        buscarAlunoPorId: new BuscarAlunoPorIdQuery(container.repositories.alunosRepositoryPostgres)
    }
});

domain.register_query({
    buscarAlunoPorId: new BuscarAlunoPorIdQuery(container.repositories.alunosRepositoryPostgres)
});
// domain.context.queries continua sem "buscarAlunoPorId" 😕
// ✅ Funciona — encadeando a chamada direto na criação
let domain = new DomainCompositionRoot({
    aggregates: {
        alunos: {
            commands: {
                lancarNota: new LancarNotaCommand(container.repositories.alunosRepositoryPostgres),
            },
        }
    },
    queries: {
        buscarAlunoPorId: new BuscarAlunoPorIdQuery(container.repositories.alunosRepositoryPostgres)
    }
})
.register_query({
    buscarAlunoPorId: new BuscarAlunoPorIdQuery(container.repositories.alunosRepositoryPostgres)
});

domain.context.queries.buscarAlunoPorId.run({ alunoId: "akj27h4" });

⚠️ Reatribuir à mesma variável também não funciona (por motivos que eu ainda não faço ideia):

// ❌ Também não funciona
domain = domain.register_query({
    buscarAlunoPorId: new BuscarAlunoPorIdQuery(alunosRepository),
});

domain.context.queries.buscarAlunoPorId.run({ alunoId: "akj27h4" });
// Erro de tipo: o TypeScript continua enxergando "domain" com o tipo
// que foi inferido lá na declaração original (sem o "queries"),
// mesmo que o valor reatribuído já tenha o formato certo em runtime.

Por enquanto, a forma segura é sempre encadear a chamada de register_query direto na criação do DomainCompositionRoot, como no exemplo "✅ Funciona" acima. O TypeScript já sabe automaticamente que queries.buscarAlunoPorId existe na nova instância — sem precisar declarar tipo nenhum manualmente. ✨


🛠️ Boas práticas recomendadas

  • Use zod (ou similar) para validar input/output dentro de cada comando e query;
  • Mantenha os repositórios atrás de contratos (interface), assim você troca Postgres por Mongo, memória, etc, sem tocar no domínio;
  • Um comando faz uma coisa só e retorna algo simples (ex: { sucesso: boolean });
  • Uma query nunca deve alterar dados, só ler;
  • Como register_query (e futuros métodos parecidos) retornam uma nova instância, sempre encadeie as chamadas na hora da criação do domínio — reatribuir a variável não preserva o tipo corretamente.

📄 Licença

MIT — use, modifique e distribua à vontade.