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

headwater

v0.4.1

Published

Dependency Injection for TypeScript

Readme

Headwater

Node.js CI npm version

Dependency Injection and Mediator for TypeScript and JavaScript

Headwater is a simple and fast Inversion of Control and Mediator implementation. These implementations work together or separately.

Example

We can combine our Dependency Injection and Mediator patterns together!

Declare a Types enum.

enum Types {
    Mediator = 'Mediator',
    PostDataAccess = 'PostDataAccess',
}

Create a Mediator.

const mediator = new Mediator();

Create a Request.

interface Post {
    id: string;
    subject: string;
    body: string;
}

class GetPostRequest<Post> {
    constructor(public id: string) {
        super();
    }
}

Add a RequestHandler to the Mediator.

Note the use of inject() anywhere we want to use Dependency Injection.

Assuming we have a PostDataAccess class defined somewhere, we can inject it here!

mediator.add({
    type: GetPostRequest,
    handler: async (
        { id },
        postDataAccess = inject(Types.PostDataAccess)
    ) => {
        const post = await postDataAccess.get(id);
        return post;
    }
});

Bind the values to a Container.

const container = new Container({
    [Types.Mediator]: {
        value: mediator
    },
    [Types.PostDataAccess]: {
        value: PostDataAccess
    }
});

Container.setDefault(container);

type Bindings = typeof container['bindings'];

declare module 'headwater' {
    interface DefaultBindings extends Bindings {}
}

Inject the Mediator, send a Request, and Headwater will do the rest!

async function main(mediator = inject(Types.Mediator)) {
    const post = await mediator.send(new GetPostRequest(1234));
    return post;
}

main();

// returns a Post

Dependency Injection

For Inversion of Control, we need to bind values to a Container, so we can retrieve them later. We can bind three types of values:

  • Value
  • Constructor
  • Factory

Create or use Default Container

We need a Container for binding values. We can either create and manage this container directly, or use the default Container.

We first must import Container.

import { Container } from 'headwater';

Create Container

const container = new Container();

Use Default Container

const container = Container.getDefault();

We can also set the Default Container

const container = new Container();

Container.setDefault(container);

TypeScript Integration

The types for the Default Container can be injected as ambient typings.

Note: It is highly recommended declare ambient typings. This will allow simpler calls to inject() later.

type Bindings = typeof container['bindings'];

declare module 'headwater' {
    interface DefaultBindings extends Bindings {}
}

Binding Values

We can bind any value to a Container. We associate each binding with a unique Type. The Type can be any string, number, or symbol.

Note: It is highly recommended to use TypeScript string enum values:

enum Types {
    UserDataAccess = 'UserDataAccess',
    PostDataAccess = 'PostDataAccess'
}

It is also possible to use const string values:

const USER_DATA_ACCESS = 'UserDataAccess';
const POST_DATA_ACCESS = 'PostDataAccess';

Binding to the Container

Note: It is highly recommended to bind in the constructor. This provides typings automatically.

const container = new Container({
    [Types.UserDataAccess]: {
        value: new UserDataAccess()
    },
    [Types.PostDataAccess]: {
        value: new PostDataAccess()
    }
});

It is also possible to bind later via:

  • Container.prototype.bindValue()
  • Container.prototype.bindConstructor()
  • Container.prototype.bindFactory().

Bind a Value

We can bind a singleton value to a Type.

This can be done in the constructor via:

enum Types {
    Value = 'Value'
}

const container = new Container({
    [Types.Value]: {
        value: 'Some singleton value'
    }
});

It can also be done later via:

container.bindValue(Types.Value, 'Some singleton value');

Bind a Constructor

We can bind a constructor to a Type. This constructor will be called later to create instances.

Note: Constructor parameters should have default values. However, these can be specified upon injection.

class ExampleClass {
    constructor(public value = 0) {
    }
}

enum Types {
    ExampleClass: 'ExampleClass'
}

const container = new Container({
    [Types.ExampleClass]: {
        type: 'constructor',
        value: ExampleClass
    }
});

It can also be done later via:

container.bindConstructor(Types.ExampleClass, ExampleClass);

Bind a Factory

We can also bind a factory to a Type. This factory will be called later.

Note: Factory parameters should have default values. However, these can be specified upon injection.

function ExampleFactory(value = 0) {
    return {
        value
    };
}

enum Types {
    ExampleFactory = 'ExampleFactory';
}

const container = new Container({
    [Types.ExampleFactory]: {
        type: 'factory',
        value: ExampleFactory
    }
});

It can also be done later via:

container.bindFactory(Types.ExampleFactory, factory);

Type Property

The optional type property in the constructor can be specified via string or BindingType. Possible string values are:

  • "value"
  • "constructor"
  • "factory"

If unspecified, it is assumed to be a Value Binding.

const container = new Container({
    [Types.Value]: {
        type: BindingType.Value,
        value: 'Some singleton value'
    },
    [Types.ExampleClass]: {
        type: BindingType.Constructor,
        value: ExampleClass
    },
    [Types.ExampleFactory]: {
        type: BindingType.Factory,
        value: ExampleFactory
    }
});

Retrieving Values

We can get any bound Type with the function inject().

const value = inject(Types.Value);
const example = inject(Types.ExampleClass);
const factory = inject(Types.FactoryExample);

We can also get them directly from a Container.

const value = container.get(Types.Value);
const example = container.get(Types.ExampleClass);
const factory = container.get(Types.FactoryExample);

If a Constructor or Factory use parameters, we may specify them.

function ExampleFactory(value) {
    return value;
}

...

const factory = inject(Types.ExampleFactory, 1);

// result will be 1

Injecting Values

We inject into a function by default parameter values. For any function, we can specify default parameters. If undefined is passed into that parameter, the default value is used instead.

Note: It is highly recommended to inject via default parameter values.

For example:

function ExampleFactory(value = 0) {
    return value;
}

const result = ExampleFactory();

// result will equal 0

In this example, when we call factory with no parameters, value will be 0.

So, we can use a bound Container value for the default value.

function ExampleFactory(value = inject(Types.Value)) {
    return value;
}

const result = ExampleFactory();

// result will be the value bound to Types.Value.

In this example, when factory is called with no parameters, we will use whatever is bound to Types.Value.

Specifying Parameters

If the bound value is a constructor or factory, we can also pass parameters into the Container.get() method.

For exmaple:

function factory(value = container.get('constructor', 1, 2, 3)) {
    return value;
}

Specifying Containers

We can also use inject(), which uses the default Container.

import { inject } from 'headwater';

function factory(value = inject('value')) {
    return value;
}

We can also specify a Container for inject().

function factory(value = inject('value', container)) {
    return value;
}

Mediator

For the Mediator pattern, we bind Handlers to Request types.

Create a Mediator

import { Mediator } from 'headwater';

const mediator = new Mediator();

NOTE: For simplicity, the Mediator can be injected via IOC.

Add Handler

Defining Requests

We must create a new class that extends Request<T>. We specify into the generic <T> the return type of the Request.

import { Request } from 'headwater';

class CreateRequest extends Request<string> {
    data: Data;

    constructor(data: Data) {
        super();
        this.data = Data;
    }
}

NOTE: The super() must be called.

Binding Handlers

The Handler must return a Promise with the type specified in the Request.

mediator.addHandler(async (request: CreateRequest) => {
    // This function is async
    // The return type must match the CreateRequest
    return '';
});

Send

We must now create a new Request object, and pass it into the Mediator. It will match the Request with a Handler and return a Promise with the value.

const request = new CreateRequest({ ... });

const result = await mediator.send(request);