inja

Simple dependency injection for node.js and the browser

Usage no npm install needed!

<script type="module">
  import inja from 'https://cdn.skypack.dev/inja';
</script>

README

Inja: Simple Dependency Injection for JavaScript

Inja makes dependency injection easy and helps you write well-structured, testable code. Inja can be added gradually to an existing application or serve as a solid foundation for your next greenfield project.

Features

  • Seperates module loading and initialization
  • Helps avoid global state
  • Explicit injection (no magic)
  • Supports singleton and transient service lifetimes
  • Makes testing easy without stubbing shared bindings
  • Easy to introduce gradually in an existing application
  • Works with Node.js and the Browser

Installation

npm install inja

# or

yarn add inja

Terminology

Inja is designed to be as simple as possible; however, it does introduce a few terms: provider, service, container, singleton, and transient. Don't be intimidated though! Each term represents an easy to understand concept. Having a name for these concepts will help faciliate better communication and naming conventions.

Provider

With inja, a provider is a object with an init() method and optionally an inject() method to describe dependencies that should be passed to init(). Additionally, a provider may have a property identifiying it as a transient provider, meaning it should generate a new service instance each time it is requested. A provider typically maps one to one with a module.

Example:

const exampleProvider = {
  transient: true,

  inject (provide) {
    return [
      otherProvider,
      provide(process.env),
      provide.factory(transientProvider)
    ]
  },

  init (otherService, envService, makeTransientService) {
    return {/* some service object /*}
  }
}

module.exports = exampleProvider

Class Provider

Alternately a provider may be defined as a class. Instead of calling init, the service is created by calling the provider with the new keyword to construct an instance of the class. Dependencies are optionally defined in a static inject method on the class.

Example

class ExampleService {
  static inject (provide) {
    return [provide(process.env)]
  }

  constructor (env) {
    this.env = env
  }
}

Interface Provider

It is possible to supply a alternate implementation of a provider within your container. One case for this would be a service that can depend on any service that satisfies a given interface.

Example:

const inja = require('inja')
const cacheInterface = require('./cache-interface')
const redisProvider = require('./redis')

const thingProvider = {
  inject () {
    return [cacheInterface]
  },

  init (cacheService) {
    return { /* service instance */ }
  }
}

const thingService = inja()
  .implement(cacheInterface, redisProvider)
  .make(thingProvider)

An interface provider may have no default implementation. In this case the init method should throw an Error to force consumers to implement the interface within the container.

Example:

const cacheInterface = {
  init () {
    throw new Error('Cache interface not implemented.')
  }
}

Service

Services handle effects and access to internal or external state. A service is an instance returned when making a provider. Values injected when initializing a service are also services.

Example:

In the code below apiService and thingService are examples of injected and made services respectively.

const inja = require('inja')
const apiProvider = require('./api')

const thingProvider = {
  inject () {
    return [apiProvider]
  },

  init (apiService) {
    return {
      get (id) {
        return apiService.get(`/things/${id}`)
      }
    }
  }
}

const thingService = inja().make(thingProvider)

Container

Calling inja() returns a new container with its own injection context. Singletons will be unique to this context. A container has a make(provider) method used to initialize a service.

Example:

const inja = require('inja')
const exampleProvider = require('./example')

const container = inja()

const exampleService = container.make(exampleProvider)

Best Practices

Bootstrapping

Ideally only one module in an application should have side effects. This module is responsible for bootstrapping your application. With inja, you would typically have a single application service which injects all of its dependencies. In this case there is no need to export the inja container.

const applicationProvider = require('./application')
const inja = require('inja')

// create a new injection container and initialize the application
inja().make(applicationProvider)

License

Copyright 2018 Brent Burgoyne

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.