FORM FOLLOWS DOMAIN. DOMAIN COMES FIRST.NODE.JS / REACT
DEPENDENCIES → WIRES → APPLICATION

CONNECT
THE PARTS.

@domain-first/wire

Dependencies end up either in a container you can't see into or in new calls scattered through the code. Wire composes them with plain functions that TypeScript checks.

01 / The problem

Two bad options.

Every class depends on something: a repository, a client, a configuration value. Someone has to build those dependencies and hand them over, and the usual choices all cost something.

01

new inside the class. A class builds its own dependencies or imports a module-level singleton. Simple — until a test needs a fake and the only way in is module mocking.

02

A wall of manual constructors. Constructor injection by hand works, but the composition root grows into a long chain of new calls where order and lifetime are easy to get wrong.

03

A DI container. Decorators, reflect-metadata, string or symbol tokens. Application classes depend on the container, and a missing binding fails at runtime.

04

Hidden lifetime and environment. Which dependency is shared and which implementation runs in tests is answered by configuration you have to go and find.

02 / The approach

Composition is a function.

Classes stay plain: they declare dependencies as constructor parameters and know nothing about Wire. Composition lives separately, as wires — zero-argument factories, () => T. The compiler checks every connection: a missing, misplaced or mistyped dependency won't compile.

NO CONTAINER

Nothing to register, nothing to look up. A wire is a function: call it to build the graph.

03 / The idea, then the code

Say what needs what.
That’s the code.

First the idea, in the words you would use with a teammate. Then the code, right next to it. Read them side by side: the code you write is the idea, almost word for word.

The ideaThe code you write

01“Projects are stored in Postgres, through Prisma.”

const wirePrismaRepository = wireClass(PrismaProjectRepository, [
  wireValue(prismaClient),
]);

02“In tests they live in memory, one store for the whole run.”

const wireMemoryRepository = memoWire(
  wireClass(InMemoryProjectRepository, []),
);

03“Which one we use depends on the environment.”

const byEnv = branchedWire<'test' | 'production'>(() =>
  process.env.NODE_ENV === 'test' ? 'test' : 'production',
);
const wireProjectRepository = byEnv({
  test: wireMemoryRepository,
  production: wirePrismaRepository,
});

04“Publishing a project needs that repository.”

const wirePublishProject = wireClass(PublishProjectUseCase, [
  wireProjectRepository,
]);

05“Forget it, and it doesn’t compile.”

wireClass(PublishProjectUseCase, []);
// TS error: a dependency is missing

5 sentences. 5 pieces of code. Nothing in the code that the idea didn’t ask for. No container, no tokens, no decorators: the classes know nothing about Wire. Imports are omitted.

04 / Start

Start with a factory.

Terminal
npm i @domain-first/wire

Branches, async composition and caching conditions are covered in the README. The scaffolder generates wires for every module it creates.

README on npm ↗

Guide and the full API.

Scaffolder ↗

Generated factories with layer-checked dependencies.