Awilix
Extremely powerful Dependency Injection (DI) container for JavaScript/Node,
written in TypeScript. Make IoC great again!
Check out this
intro to Dependency Injection with Awilix
Table of Contents
Installation
Install with npm
npm install awilix --save
Or yarn
yarn add awilix
You can also use the UMD build from unpkg
<script src="https://unpkg.com/awilix/lib/awilix.umd.js"/>
<script>
const container = Awilix.createContainer()
</script>
Usage
Awilix has a pretty simple API (but with many possible ways to invoke it). At
minimum, you need to do 3 things:
- Create a container
- Register some modules in it
- Resolve and use!
index.js
const awilix = require('awilix')
const container = awilix.createContainer({
injectionMode: awilix.InjectionMode.PROXY
})
class UserController {
constructor(opts) {
this.userService = opts.userService
}
getUser(ctx) {
return this.userService.getUser(ctx.params.id)
}
}
container.register({
userController: awilix.asClass(UserController)
})
const makeUserService = ({ db }) => {
return {
getUser: id => {
return db.query(`select * from users where id=${id}`)
}
}
}
container.register({
userService: awilix.asFunction(makeUserService)
})
function Database(connectionString, timeout) {
this.conn = connectToYourDatabaseSomehow(connectionString, timeout)
}
Database.prototype.query = function(sql) {
return this.conn.rawSql(sql)
}
container.register({
db: awilix.asClass(Database).classic()
})
container.register({
connectionString: awilix.asValue(process.env.CONN_STR),
timeout: awilix.asValue(1000)
})
router.get('/api/users/:id', container.resolve('userController').getUser)
router.get('/api/users/:id', container.cradle.userController.getUser)
That example is rather lengthy, but if you extract things to their proper files
it becomes more manageable.
Check out a working Koa example!
Lifetime management
Awilix supports managing the lifetime of instances. This means that you can
control whether objects are resolved and used once, cached within a certain
scope, or cached for the lifetime of the process.
There are 3 lifetime types available.
Lifetime.TRANSIENT
: This is the default. The registration is resolved every
time it is needed. This means if you resolve a class more than once, you will
get back a new instance every time.Lifetime.SCOPED
: The registration is scoped to the container - that means
that the resolved value will be reused when resolved from the same scope (or a
child scope).Lifetime.SINGLETON
: The registration is always reused no matter what - that
means that the resolved value is cached in the root container.
They are exposed on the awilix.Lifetime
object.
const Lifetime = awilix.Lifetime
To register a module with a specific lifetime:
const { asClass, asFunction, asValue } = awilix
class MailService {}
container.register({
mailService: asClass(MailService, { lifetime: Lifetime.SINGLETON })
})
container.register({
mailService: asClass(MailService).lifetime(Lifetime.SINGLETON)
})
container.register({
mailService: asClass(MailService).singleton()
})
container.register('mailService', asClass(MailService, { lifetime: SINGLETON }))
Scoped lifetime
In web applications, managing state without depending too much on the web
framework can get difficult. Having to pass tons of information into every
function just to make the right choices based on the authenticated user.
Scoped lifetime in Awilix makes this simple - and fun!
const { createContainer, asClass, asValue } = awilix
const container = createContainer()
class MessageService {
constructor({ currentUser }) {
this.user = currentUser
}
getMessages() {
const id = this.user.id
}
}
container.register({
messageService: asClass(MessageService).scoped()
})
app.use((req, res, next) => {
req.scope = container.createScope()
req.scope.register({
currentUser: asValue(req.user)
})
next()
})
app.get('/messages', (req, res) => {
const messageService = req.scope.resolve('messageService')
messageService.getMessages().then(messages => {
res.send(200, messages)
})
})
IMPORTANT! If a singleton is resolved, and it depends on a scoped or
transient registration, those will remain in the singleton for it's lifetime!
const makePrintTime = ({ time }) => () => {
console.log('Time:', time)
}
const getTime = () => new Date().toString()
container.register({
printTime: asFunction(makePrintTime).singleton(),
time: asFunction(getTime).transient()
})
container.resolve('time')
container.resolve('time')
container.resolve('printTime')()
container.resolve('printTime')()
Read the documentation for container.createScope()
for more examples.
Injection modes
The injection mode determines how a function/constructor receives its
dependencies. Pre-2.3.0, only one mode was supported - PROXY
- which remains
the default mode.
Awilix v2.3.0 introduced an alternative injection mode: CLASSIC
. The injection
modes are available on awilix.InjectionMode
-
InjectionMode.PROXY
(default): Injects a proxy to functions/constructors
which looks like a regular object.
class UserService {
constructor(opts) {
this.emailService = opts.emailService
this.logger = opts.logger
}
}
or with destructuring:
class UserService {
constructor({ emailService, logger }) {
this.emailService = emailService
this.logger = logger
}
}
-
InjectionMode.CLASSIC
: Parses the function/constructor parameters, and
matches them with registrations in the container. Don't use this if you
minify your code!
class UserService {
constructor(emailService, logger) {
this.emailService = emailService
this.logger = logger
}
}
Injection modes can be set per-container and per-resolver. The most specific one
wins.
Note: I personally don't see why you would want to have different injection
modes in a project, but if the need arises, Awilix supports it.
Container-wide:
const { createContainer, InjectionMode } = require('awilix')
const container = createContainer({ injectionMode: InjectionMode.CLASSIC })
Per resolver:
const container = createContainer()
container.register({
logger: asClass(Logger).classic(),
emailService: asFunction(makeEmailService).proxy()
notificationService: asClass(NotificationService).setInjectionMode(InjectionMode.CLASSIC)
})
container.register({
logger: asClass(Logger, { injectionMode: InjectionMode.CLASSIC })
})
For auto-loading modules:
const container = createContainer()
container.loadModules(['services/**/*.js', 'repositories/**/*.js'], {
resolverOptions: {
injectionMode: InjectionMode.CLASSIC
}
})
Choose whichever fits your style.
PROXY
technically allows you to defer pulling dependencies (for circular
dependency support), but this isn't recommended.CLASSIC
feels more like the DI you're used to in other languages.PROXY
is more descriptive, and makes for more readable tests; when unit
testing your classes/functions without using Awilix, you don't have to worry
about parameter ordering like you would with CLASSIC
.- Performance-wise,
CLASSIC
is slightly faster because it only reads the
dependencies from the constructor/function once (when asClass
/asFunction
is called), whereas accessing dependencies on the Proxy may incur slight
overhead for each resolve. CLASSIC
will not work when your code is minified! It reads the function
signature to determine what dependencies to inject. Minifiers will usually
mangle these names.
Here's an example outlining the testability points raised.
function database(connectionString, timeout, logger) {
}
const db = database('localhost:1337;user=123...', 4000, new LoggerMock())
function database({ connectionString, timeout, logger }) {
}
const db = database({
logger: new LoggerMock(),
timeout: 4000,
connectionString: 'localhost:1337;user=123...'
})
Auto-loading modules
When you have created your container, registering 100's of classes can get
boring. You can automate this by using loadModules
.
Imagine this app structure:
app
services
UserService.js
- exports an ES6 class UserService {}
emailService.js
- exports a factory function
function makeEmailService() {}
repositories
UserRepository.js
- exports an ES6 class UserRepository {}
index.js
- our main script
In our main script we would do the following
const awilix = require('awilix')
const container = awilix.createContainer()
container.loadModules([
'services/**/*.js',
'repositories/**/*.js'
], {
formatName: 'camelCase',
resolverOptions: {
lifetime: Lifetime.SINGLETON,
register: awilix.asClass
}
)
container.resolve('userService').getUser(1)
Important: Auto-loading relies on glob
and therefore does not with
bundlers like Webpack, Rollup and Browserify.
Per-module local injections
Some modules might need some additional configuration values than just
dependencies.
For example, our userRepository
wants a db
module which is registered with
the container, but it also wants a timeout
value. timeout
is a very generic
name and we don't want to register that as a value that can be accessed by all
modules in the container (maybe other modules have a different timeout?)
export default function userRepository({ db, timeout }) {
return {
find() {
return Promise.race([
db.query('select * from users'),
Promise.delay(timeout).then(() =>
Promise.reject(new Error('Timed out'))
)
])
}
}
}
Awilix 2.5 added per-module local injections. The following snippet contains
all the possible ways to set this up.
import { createContainer, Lifetime, asFunction } from 'awilix'
import createUserRepository from './repositories/userRepository'
const container = createContainer()
.register({
userRepository: asFunction(createUserRepository)
.inject(() => ({ timeout: 2000 }))
})
.register({
userRepository: asFunction(createUserRepository, {
injector: () => ({ timeout: 2000 })
})
})
.register(
'userRepository',
asFunction(createUserRepository, {
injector: () => ({ timeout: 2000 })
})
)
.loadModules([['repositories/*.js', { injector: () => ({ timeout: 2000 }) }]])
Now timeout
is only available to the modules it was configured for.
IMPORTANT: the way this works is by wrapping the cradle
in another proxy
that provides the returned values from the inject
function. This means if you
pass along the injected cradle object, anything with access to it can access the
local injections.
Inlining resolver options
Awilix 2.8 added support for inline resolver options. This is best explained
with an example.
services/awesome-service.js:
import { RESOLVER, Lifetime, InjectionMode } from 'awilix'
export default class AwesomeService {
constructor(awesomeRepository) {
this.awesomeRepository = awesomeRepository
}
}
AwesomeService[RESOLVER] = {
lifetime: Lifetime.SCOPED,
injectionMode: InjectionMode.CLASSIC
}
index.js:
import { createContainer, asClass } from 'awilix'
import AwesomeService from './services/awesome-service.js'
const container = createContainer().register({
awesomeService: asClass(AwesomeService)
})
console.log(container.registrations.awesomeService.lifetime)
console.log(container.registrations.awesomeService.injectionMode)
Additionally, if we add a name
field and use loadModules
, the name
is used
for registration.
// `RESOLVER` is a Symbol.
AwesomeService[RESOLVER] = {
+ name: 'superService',
lifetime: Lifetime.SCOPED,
injectionMode: InjectionMode.CLASSIC
}
const container = createContainer().loadModules(['services/*.js'])
console.log(container.registrations.superService.lifetime)
console.log(container.registrations.superService.injectionMode)
Important: the name
field is only used by loadModules
.
Disposing
As of Awilix v3.0, you can call container.dispose()
to clear the resolver
cache and call any registered disposers. This is very useful to properly dispose
resources like connection pools, and especially when using watch-mode in your
integration tests.
For example, database connection libraries usually have some sort of destroy
or end
function to close the connection. You can tell Awilix to call these for
you when calling container.dispose()
.
Important: the container being disposed will not dispose its' scopes. It
only disposes values in it's own cache.
import { createContainer, asClass } from 'awilix'
import pg from 'pg'
class TodoStore {
constructor({ pool }) {
this.pool = pool
}
async getTodos() {
const result = await this.pool.query('SELECT * FROM todos')
return result.rows
}
}
function configureContainer() {
return container.register({
todoStore: asClass(TodoStore),
pool: asFunction(() => new pg.Pool())
.singleton()
.disposer(pool => pool.end())
})
}
const container = configureContainer()
const todoStore = container.resolve('todoStore')
container.dispose().then(() => {
console.log('Container has been disposed!')
})
A perfect use case for this would be when using Awilix with an HTTP server.
import express from 'express'
import http from 'http'
function createServer() {
const app = express()
const container = configureContainer()
app.get('/todos', async (req, res) => {
const store = container.resolve('todoStore')
const todos = await store.getTodos()
res.status(200).json(todos)
})
const server = http.createServer(app)
server.on('close', () => container.dispose())
return server
}
test('server does server things', async () => {
const server = createServer()
server.listen(3000)
server.close()
})
API
The awilix
object
When importing awilix
, you get the following top-level API:
createContainer
listModules
AwilixResolutionError
asValue
asFunction
asClass
aliasTo
Lifetime
- documented above.InjectionMode
- documented above.
These are documented below.
Resolver options
Whenever you see a place where you can pass in resolver options, you can
pass in an object with the following props:
lifetime
: An awilix.Lifetime.*
string, such as awilix.Lifetime.SCOPED
injectionMode
: An awilix.InjectionMode.*
string, such as
awilix.InjectionMode.CLASSIC
injector
: An injector function - see
Per-module local injectionsregister
: Only used in loadModules
, determines how to register a loaded
module explicitly
Examples of usage:
container.register({
stuff: asClass(MyClass, { injectionMode: InjectionMode.CLASSIC })
})
container.loadModules([['some/path/to/*.js', { register: asClass }]], {
resolverOptions: {
lifetime: Lifetime.SCOPED
}
})
createContainer()
Creates a new Awilix container. The container stuff is documented further down.
Args:
options
: Options object. Optional.
options.require
: The function to use when requiring modules. Defaults to
require
. Useful when using something like
require-stack
. Optional.options.injectionMode
: Determines the method for resolving dependencies.
Valid modes are:
PROXY
: Uses the awilix
default dependency resolution mechanism (I.E.
injects the cradle into the function or class). This is the default
injection mode.CLASSIC
: Uses the named dependency resolution mechanism. Dependencies
must be named exactly like they are in the registration. For
example, a dependency registered as repository
cannot be referenced in a
class constructor as repo
.
asFunction()
Used with container.register({ userService: asFunction(makeUserService) })
.
Tells Awilix to invoke the function without any context.
The returned resolver has the following chainable (fluid) API:
asFunction(fn).setLifetime(lifetime: string)
: sets the lifetime of the
registration to the given value.asFunction(fn).transient()
: same as
asFunction(fn).setLifetime(Lifetime.TRANSIENT)
.asFunction(fn).scoped()
: same as
asFunction(fn).setLifetime(Lifetime.SCOPED)
.asFunction(fn).singleton()
: same as
asFunction(fn).setLifetime(Lifetime.SINGLETON)
.asFunction(fn).inject(injector: Function)
: Let's you provide local
dependencies only available to this module. The injector
gets the container
passed as the first and only argument and should return an object.
asClass()
Used with container.register({ userService: asClass(UserService) })
. Tells
Awilix to instantiate the given function as a class using new
.
The returned resolver has the same chainable API as asFunction
.
asValue()
Used with container.register({ dbHost: asValue('localhost') })
. Tells Awilix
to provide the given value as-is.
aliasTo()
Resolves the dependency specified.
container.register({
val: asValue(123),
aliasVal: aliasTo('val')
})
container.resolve('aliasVal') === container.resolve('val')
listModules()
Returns an array of {name, path}
pairs, where the name is the module name, and
path is the actual full path to the module.
This is used internally, but is useful for other things as well, e.g.
dynamically loading an api
folder.
Args:
globPatterns
: a glob pattern string, or an array of them.opts.cwd
: The current working directory passed to glob
. Defaults to
process.cwd()
.- returns: an array of objects with:
name
: The module name - e.g. db
path
: The path to the module relative to options.cwd
- e.g. lib/db.js
Example:
const listModules = require('awilix').listModules
const result = listModules(['services/*.js'])
console.log(result)
Important: listModules
relies on glob
and therefore is not supported
with bundlers like Webpack, Rollup and Browserify.
AwilixResolutionError
This is a special error thrown when Awilix is unable to resolve all dependencies
(due to missing or cyclic dependencies). You can catch this error and use
err instanceof AwilixResolutionError
if you wish. It will tell you what
dependencies it could not find or which ones caused a cycle.
The AwilixContainer
object
The container returned from createContainer
has some methods and properties.
container.cradle
Behold! This is where the magic happens! The cradle
is a proxy, and all
getters will trigger a container.resolve
. The cradle
is actually being
passed to the constructor/factory function, which is how everything gets wired
up.
container.registrations
A read-only getter that returns the internal registrations. When invoked on a
scope, will show registrations for it's parent, and it's parent's parent, and
so on.
Not really useful for public use.
container.cache
A Map<string, CacheEntry>
used internally for caching resolutions. Not meant
for public use but if you find it useful, go ahead but tread carefully.
Each scope has it's own cache, and checks the cache of it's ancestors.
let counter = 1
container.register({
count: asFunction(() => counter++).singleton()
})
container.cradle.count === 1
container.cradle.count === 1
container.cache.delete('count')
container.cradle.count === 2
container.options
Options passed to createContainer
are stored here.
const container = createContainer({
injectionMode: InjectionMode.CLASSIC
})
console.log(container.options.injectionMode)
container.resolve()
Resolves the registration with the given name. Used by the cradle.
Signature
resolve<T>(name: string, [resolveOpts: ResolveOptions]): T
container.register({
leet: asFunction(() => 1337)
})
container.resolve('leet') === 1337
container.cradle.leet === 1337
The optional resolveOpts
has the following fields:
allowUnregistered
: if true
, returns undefined
when the dependency does
not exist, instead of throwing an error.
container.register()
Signatures
register(name: string, resolver: Resolver): AwilixContainer
register(nameAndResolverPair: NameAndResolverPair): AwilixContainer
Awilix needs to know how to resolve the modules, so let's pull out the resolver
functions:
const awilix = require('awilix')
const { asValue, asFunction, asClass } = awilix
asValue
: Resolves the given value as-is.asFunction
: Resolve by invoking the function with the container cradle as
the first and only argument.asClass
: Like asFunction
but uses new
.
Now we need to use them. There are multiple syntaxes for the register
function, pick the one you like the most - or use all of them, I don't really
care! :sunglasses:
Both styles supports chaining! register
returns the container!
container.register('connectionString', asValue('localhost:1433;user=...'))
container.register('mailService', asFunction(makeMailService))
container.register('context', asClass(SessionContext))
container.register({
connectionString: asValue('localhost:1433;user=...'),
mailService: asFunction(makeMailService, { lifetime: Lifetime.SINGLETON }),
context: asClass(SessionContext, { lifetime: Lifetime.SCOPED })
})
container.register(
'mailService',
asFunction(makeMailService).setLifetime(Lifetime.SINGLETON)
)
container.register('context', asClass(SessionContext).singleton())
container.register('context', asClass(SessionContext).transient())
container.register('context', asClass(SessionContext).scoped())
The object syntax, key-value syntax and chaining are valid for all register
calls!
container.loadModules()
Given an array of globs, registers the modules and returns the container.
Awilix will use require
on the loaded modules, and register the
default-exported function or class as the name of the file.
This uses a heuristic to determine if it's a constructor function
(function Database() {...}
); if the function name starts with a capital
letter, it will be new
ed!
Args:
globPatterns
: Array of glob patterns that match JS files to load.opts.cwd
: The cwd
being passed to glob
. Defaults to process.cwd()
.opts.formatName
: Can be either 'camelCase'
, or a function that takes the
current name as the first parameter and returns the new name. Default is to
pass the name through as-is. The 2nd parameter is a full module descriptor.opts.resolverOptions
: An object
passed to the resolvers. Used to configure
the lifetime, injection mode and more of the loaded modules.
Example:
container.loadModules(['services/*.js', 'repositories/*.js', 'db/db.js'])
container.cradle.userService.getUser(123)
container.loadModules([
'services/*.js',
'repositories/*.js',
'db/db.js'
], {
resolverOptions: {
lifetime: Lifetime.SINGLETON
}
})
container.cradle.userService.getUser(123)
container.loadModules([
['services/*.js', Lifetime.SCOPED],
'repositories/*.js',
'db/db.js'
], {
resolverOptions: {
lifetime: Lifetime.SINGLETON
}
)
container.cradle.userService.getUser(123)
container.loadModules(['repositories/account-repository.js', 'db/db.js'], {
formatName: 'camelCase'
})
container.cradle.accountRepository.getUser(123)
container.loadModules(['repository/account.js', 'service/email.js'], {
formatName: (name, descriptor) => {
const splat = descriptor.path.split('/')
const namespace = splat[splat.length - 2]
const upperNamespace =
namespace.charAt(0).toUpperCase() + namespace.substring(1)
return name + upperNamespace
}
})
container.cradle.accountRepository.getUser(123)
container.cradle.emailService.sendEmail('test@test.com', 'waddup')
The ['glob', Lifetime.SCOPED]
syntax is a shorthand for passing in resolver
options like so: ['glob', { lifetime: Lifetime.SCOPED }]
Important: loadModules
depends on glob
and is therefore not supported in
module bundlers like Webpack, Rollup and Browserify.
container.createScope()
Creates a new scope. All registrations with a Lifetime.SCOPED
will be cached
inside a scope. A scope is basically a "child" container.
let counter = 1
container.register({
counterValue: asFunction(() => counter++).scoped()
})
const scope1 = container.createScope()
const scope2 = container.createScope()
const scope1Child = scope1.createScope()
scope1.cradle.counterValue === 1
scope1.cradle.counterValue === 1
scope2.cradle.counterValue === 2
scope2.cradle.counterValue === 2
scope1Child.cradle.counterValue === 1
Be careful! If a scope's parent has already resolved a scoped value, that
value will be returned.
let counter = 1
container.register({
counterValue: asFunction(() => counter++).scoped()
})
const scope1 = container.createScope()
const scope2 = container.createScope()
container.cradle.counterValue === 1
scope1.cradle.counterValue === 1
scope1.cradle.counterValue === 1
scope2.cradle.counterValue === 1
scope2.cradle.counterValue === 1
A scope may also register additional stuff - they will only be available within
that scope and it's children.
container.register({
scopedValue: asFunction(cradle => 'Hello ' + cradle.someValue)
})
const scope = container.createScope()
scope.register({
someValue: asValue('scope')
})
scope.cradle.scopedValue === 'Hello scope'
container.cradle.someValue
Things registered in the scope take precedence over it's parent.
const scope = container.createScope()
container.register({
value: asValue('root'),
usedValue: asFunction(cradle => cradle.value)
})
scope.register({
value: asValue('scope')
})
container.cradle.usedValue === 'root'
scope.cradle.usedValue === 'scope'
container.build()
Builds an instance of a class (or a function) by injecting dependencies, but
without registering it in the container.
It's basically a shortcut for asClass(MyClass).resolve(container)
.
Args:
targetOrResolver
: A class, function or resolver (example: asClass(..)
,
asFunction(..)
)opts
: Resolver options.
Returns an instance of whatever is passed in, or the result of calling the
resolver.
Important: if you are doing this often for the same class/function, consider
using the explicit approach and save the resolver, especially if you are
using classic resolution because it scans the class constructor/function when
calling asClass(Class)
/ asFunction(func)
.
class MyClass {
constructor({ ping }) {
this.ping = ping
}
pong() {
return this.ping
}
}
const createMyFunc = ({ ping }) => ({
pong: () => ping
})
container.register({
ping: asValue('pong')
})
const myClass = container.build(MyClass)
const myFunc = container.build(createMyFunc)
const myClassResolver = asClass(MyClass)
const myFuncResolver = asFunction(MyFunc)
const myClass = container.build(myClassResolver)
const myFunc = container.build(myFuncResolver)
container.dispose()
Returns a Promise
that resolves when all disposers of cached resolutions have
resolved. Only cached values will be disposed, meaning they must have a
Lifetime
of SCOPED
or SINGLETON
, or else they are not cached by the
container and therefore can't be disposed by it.
This also clears the container's cache.
const pg = require('pg')
container.register({
pool: asFunction(() => new pg.Pool())
.disposer(pool => pool.end())
.singleton()
})
const pool = container.resolve('pool')
pool.query('...')
container.dispose().then(() => {
console.log('All dependencies disposed, you can exit now. :)')
})
Universal Module (Browser Support)
As of v3, Awilix ships with official support for browser environments!
The package includes 4 flavors.
- CommonJS, the good ol' Node format -
lib/awilix.js
- ES Modules, for use with module bundlers in Node -
lib/awilix.module.js
- ES Modules, for use with module bundlers in the browser -
lib/awilix.browser.js
- UMD, for dropping it into a script tag -
lib/awilix.umd.js
The package.json
includes the proper fields for bundlers like Webpack, Rollup
and Browserify to pick the correct version, so you should not have to configure
anything. 😎
Important: the browser builds do not support loadModules
or listModules
,
because they depend on Node-specific packages.
Also important: due to using Proxy
+ various Reflect
methods, Awilix is only supposed to work in:
- Chrome >= 49
- Firefox >= 18
- Edge >= 12
- Opera >= 36
- Safari >= 10
- Internet Explorer is not supported
Contributing
Clone repo, run npm i
to install all dependencies, and then
npm run test -- --watchAll
to start writing code.
For code coverage, run npm run cover
.
If you submit a PR, please aim for 100% code coverage and no linting errors.
Travis will fail if there are linting errors. Thank you for considering
contributing. :)
What's in a name?
Awilix is the mayan goddess of the moon, and also my favorite character in the
game SMITE.
Author
Jeff Hansen - @Jeffijoe