Wiremap is a lightweight, type-safe dependency injection framework for TypeScript that favors composition over inheritance. It helps you build scalable, maintainable, and testable applications with intuitive dependency management.
This is an alpha version under active development. The API may change at any time.
- 🔒 Type-Safe: Full TypeScript support without extra boilerplate
- 🧱 Compositional: Build complex apps from small, reusable units
- ♻️ Circular Dependency-Free: Designed to avoid circular dependencies
- 🤯 No decorators. No classes. No gorillas, no jungles. Just the bananas 🍌
- 🌲 Hierarchical: Organize dependencies with namespaces and blocks
- 🧪 Testable: Built-in utilities for mocking and isolation
- 🪶 Lightweight: Minimal runtime overhead with smart caching
- 🔌 Zero Configuration: Install and import, nothing else
- 🔨 Simple API: So simple, it hurts
Wiremap is architecture-agnostic. It doesn’t enforce a particular structure but
adapts naturally to any software design approach. Whether you’re building with
Hexagonal Architecture, Domain-Driven Design (DDD), or the Ports and
Adapters pattern, Wiremap makes it simple to express clear boundaries between
layers. Its compositional blocks and type-safe wires let you organize services,
domains, infrastructure, and application logic in a way that fits your chosen
methodology. You can even combine patterns without fighting the framework.
Wiremap’s role is to provide clean, dependency-free wiring, no matter how you
shape your architecture.
# pick your package manager
npm install wiremap
pnpm add wiremap
deno add jsr:@jacobo/wiremap
bun add wiremap
Wiremap applications are composed of units, organized into hierarchical blocks. Units can depend on each other via wires provided by their blocks.
A block is an object that contains units or other blocks.
There are two ways to define a block: as a file or with the defineBlock
helper.
ℹ️ The root block (the top-level one) doesn’t need to be tagged or defined.
It can simply be a plain object.
To mark a file as a block, export a special tag named $
:
// -- myBlock.ts --
import { tagBlock } from "wiremap";
// tag the file as a block
export const $ = tagBlock();
// export your units and sub-blocks
export const myUnit = ...;
export const otherBlock = ...;
Use file blocks by importing them as *
:
// -- myParentBlock.ts --
import { tagBlock } from "wiremap";
import * as myBlock from "./myBlock.ts";
export const $ = tagBlock();
// expose myBlock as a service
export const myService = myBlock;
Or directly re-export them in one line:
// -- myParentBlock.ts --
import { tagBlock } from "wiremap";
export * as myService from "./myBlock.ts";
export const $ = tagBlock();
defineBlock
is useful when you want to define multiple blocks in the same
file:
import { defineBlock } from "wiremap";
export const myService = defineBlock({
// units and blocks here
});
export const myController = defineBlock({
// ...
});
Units are the smallest building blocks of your app.
They can hold any value and are resolved lazily (on demand) and cached.
To define a unit, add it to a block—either directly or with defineUnit
.
import { defineUnit, tagBlock } from "wiremap";
export const $ = tagBlock();
// direct unit
export const myUnit = function () {};
// using the helper
export const myUnit = defineUnit(function () {});
👉 Use as const
to ensure TypeScript infers precise literal types (e.g. true
instead of boolean
).
Private units can only be used within their own block.
Mark them with isPrivate = true as const
:
// direct definition
export function myUnit() {}
myUnit.isPrivate = true as const;
// with helper
export const myUnit = defineUnit(
() => {},
{ isPrivate: true },
);
Factory units are functions that take the wire as their first argument and
return the unit.
They’re lazy (initialized on first access) and cached.
export const myUnit = (wire: MyWire) => () => {
return theUnitValue;
};
myUnit.is = 'factory' as const;
With helper:
export const myUnit = defineUnit(
(wire: MyWire) => () => theUnitValue,
{ is: 'factory' },
);
Async factories return a Promise.
When async factories exist, wireUp()
returns a Promise that resolves once
everything is initialized and ready.
export async function myUnit(wire: MyWire) {
await whatEver();
return () => {};
}
myUnit.is = 'asyncFactory' as const;
With helper:
export const myUnit = defineUnit(
async (wire: MyWire) => {
await whatEver();
return () => {};
},
{ is: 'asyncFactory' },
);
Bound units are functions that receive the wire as this
.
They must use the function
keyword:
export function myUnit(this: MyWire) {
const otherUnit = this().otherUnit;
}
myUnit.is = 'bound' as const;
With helper:
export const myUnit = defineUnit(
function (this: MyWire) {
const otherUnit = this().otherUnit;
},
{ is: 'bound' },
);
Every block with units has a wire: a function that gives access to units in
other blocks.
It’s automatically injected into:
- bound functions as
this
- factories / async factories as the first argument
You don’t need to create wires yourself, but you’ll usually want to infer their
types using InferBlocks
and InferWire
.
// -- main.ts --
import { type InferBlocks } from "wiremap";
import * as postMod from "./post/postMod.ts";
import * as userMod from "./user/userMod.ts";
const mainBlock = { user: userMod, post: postMod };
export type Blocks = InferBlocks<typeof mainBlock>;
Now use InferWire
to type your wire:
// -- userService.ts --
import { type InferWire, tagBlock } from "wiremap";
import type { Blocks } from "../main.ts";
type Wire = InferWire<Blocks, "user.service">;
export const $ = tagBlock();
export function myUnit(this: Wire) {
return this("post.service").getPosts();
}
myUnit.is = 'bound' as const;
Wires can resolve current, root, child, or absolute blocks:
this(".")
→ current blockthis()
→ root blockthis(".child")
→ child blockthis("path.to.block")
→ absolute path
To bootstrap your app, pass the root block to wireUp()
.
It returns the main wire (or a Promise if async factories exist):
import { type InferBlocks, wireUp } from "wiremap";
import * as mod1 from "./module1.ts";
import * as mod2 from "./module2.ts";
const mainBlock = { mod1, mod2 };
export type Blocks = InferBlocks<typeof mainBlock>;
const main = await wireUp(mainBlock);
const myUnit = main("mod1.service").myUnit;
Wiremap includes built-in utilities to make testing easy and reliable. Because blocks and units are fully compositional, you can swap out real implementations with mocks, stubs, or fakes, ensuring tests stay isolated and predictable.
The mockUnit
helper lets you replace the dependencies of a unit under test
with controlled fake values. This way you can test logic in complete isolation,
without touching databases, APIs, or other external systems.
Here’s an example using a postService
module:
import { mockUnit } from "../../src/mock.ts";
import * as postService from "./postService.ts";
// Fake blocks used during testing
const fakeBlocks = {
// override a repository with an in-memory array
"..": { repo: [] },
// replace user service with a mock
"user.service": {
getUser: (id: string) => ({
id,
name: "jacobo",
email: "[email protected]",
isAdmin: true,
}),
},
};
// Create mocked versions of the real units
const addPost = mockUnit(postService.addPost, fakeBlocks);
const getPost = mockUnit(postService.getPost, fakeBlocks);
// Run the test
const postId = addPost("titulo", "contenido", "11234");
const post = getPost(postId);
if (!post) throw new Error("Post not found");
assertEquals(post.id, postId);
assertEquals(post.title, "titulo");
assertEquals(post.content, "contenido");
assertEquals(post.userId, "11234");
This allows you to test the business logic of your unit without depending on real infrastructure. By controlling the wire through fakeBlocks, you can precisely simulate the environment needed for each test case.
// config.ts
export const config = {
port: 3000,
host: "localhost",
database: { url: "postgresql://localhost:5432/myapp" },
};
// userService.ts
import { type InferWire, tagBlock } from "wiremap";
import type { Blocks } from "./app.ts";
export const $ = tagBlock("user.service");
type Wire = InferWire<Blocks, "user.service">;
export function getUsers(this: Wire) {
return this().database.users.findAll();
}
getUsers.is = 'bound' as const;
export const createUser = defineUnit(
(wire: Wire) => {
const db = wire().database;
return (name: string, email: string) => {
const user = { id: crypto.randomUUID(), name, email };
db.users.create(user);
return user;
};
},
{ is: 'factory' },
);
// app.ts
import { type InferBlocks, wireUp } from "wiremap";
import { config } from "./config.ts";
import * as userService from "./userService.ts";
import * as database from "./database.ts";
const main = {
config,
database,
user: defineBlock({ service: userService }),
};
export type Blocks = InferBlocks<typeof main>;
const app = await wireUp(main);
// Use the application
console.log("Users:", app("user.service").getUsers());
Contributions are welcome!
💬 Questions or Feedback?
Open an issue on GitHub
MIT © Jacobo Tabernero Rey