Awilix
Extremely powerful Inversion of Control (IoC) container for Node.JS
README
Awilix
Extremely powerful, performant, & battle-tested Dependency Injection (DI) container for JavaScript/Node,
written in TypeScript.
Awilix enables you to write composable, testable software using dependency injection without special annotations, which in turn decouples your core application code from the intricacies of the DI mechanism.
💡 Check out this
Installation
Install with npm
- ```
- npm install awilix
- ```
Or yarn
- ```
- yarn add awilix
- ```
You can also use the UMD build fromunpkg
- ```html
- <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
- ```javascript
- const awilix = require('awilix')
- // Create the container and set the injectionMode to PROXY (which is also the default).
- const container = awilix.createContainer({
- injectionMode: awilix.InjectionMode.PROXY
- })
- // This is our app code... We can use
- // factory functions, constructor functions
- // and classes freely.
- class UserController {
- // We are using constructor injection.
- constructor(opts) {
- // Save a reference to our dependency.
- this.userService = opts.userService
- }
- // imagine ctx is our HTTP request context...
- getUser(ctx) {
- return this.userService.getUser(ctx.params.id)
- }
- }
- container.register({
- // Here we are telling Awilix how to resolve a
- // userController: by instantiating a class.
- userController: awilix.asClass(UserController)
- })
- // Let's try with a factory function.
- const makeUserService = ({ db }) => {
- // Notice how we can use destructuring
- // to access dependencies
- return {
- getUser: id => {
- return db.query(`select * from users where id=${id}`)
- }
- }
- }
- container.register({
- // the `userService` is resolved by
- // invoking the function.
- userService: awilix.asFunction(makeUserService)
- })
- // Alright, now we need a database.
- // Let's make that a constructor function.
- // Notice how the dependency is referenced by name
- // directly instead of destructuring an object.
- // This is because we register it in "CLASSIC"
- // injection mode below.
- function Database(connectionString, timeout) {
- // We can inject plain values as well!
- this.conn = connectToYourDatabaseSomehow(connectionString, timeout)
- }
- Database.prototype.query = function(sql) {
- // blah....
- return this.conn.rawSql(sql)
- }
- // We use register coupled with asClass to tell Awilix to
- // use `new Database(...)` instead of just `Database(...)`.
- // We also want to use `CLASSIC` injection mode for this
- // registration. Read more about injection modes below.
- container.register({
- db: awilix.asClass(Database).classic()
- })
- // Lastly we register the connection string and timeout values
- // as we need them in the Database constructor.
- container.register({
- // We can register things as-is - this is not just
- // limited to strings and numbers, it can be anything,
- // really - they will be passed through directly.
- connectionString: awilix.asValue(process.env.CONN_STR),
- timeout: awilix.asValue(1000)
- })
- // We have now wired everything up!
- // Let's use it! (use your imagination with the router thing..)
- router.get('/api/users/:id', container.resolve('userController').getUser)
- // Alternatively, using the `cradle` proxy..
- router.get('/api/users/:id', container.cradle.userController.getUser)
- // Using `container.cradle.userController` is actually the same as calling
- // `container.resolve('userController')` - the cradle is our proxy!
- ```
That example is rather lengthy, but if you extract things to their proper files
it becomes more manageable.
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.
- ```js
- const Lifetime = awilix.Lifetime
- ```
To register a module with a specific lifetime:
- ```js
- const { asClass, asFunction, asValue } = awilix
- class MailService {}
- container.register({
- mailService: asClass(MailService, { lifetime: Lifetime.SINGLETON })
- })
- // or using the chaining configuration API..
- container.register({
- mailService: asClass(MailService).setLifetime(Lifetime.SINGLETON)
- })
- // or..
- container.register({
- mailService: asClass(MailService).singleton()
- })
- // or.......
- 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!
- ```js
- const { createContainer, asClass, asValue } = awilix
- const container = createContainer()
- class MessageService {
- constructor({ currentUser }) {
- this.user = currentUser
- }
- getMessages() {
- const id = this.user.id
- // wee!
- }
- }
- container.register({
- messageService: asClass(MessageService).scoped()
- })
- // imagine middleware in some web framework..
- app.use((req, res, next) => {
- // create a scoped container
- req.scope = container.createScope()
- // register some request-specific data..
- req.scope.register({
- currentUser: asValue(req.user)
- })
- next()
- })
- app.get('/messages', (req, res) => {
- // for each request we get a new message service!
- const messageService = req.scope.resolve('messageService')
- messageService.getMessages().then(messages => {
- res.send(200, messages)
- })
- })
- // The message service can now be tested
- // without depending on any request data!
- ```
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!
- ```js
- const makePrintTime = ({ time }) => () => {
- console.log('Time:', time)
- }
- const getTime = () => new Date().toString()
- container.register({
- printTime: asFunction(makePrintTime).singleton(),
- time: asFunction(getTime).transient()
- })
- // Resolving `time` 2 times will
- // invoke `getTime` 2 times.
- container.resolve('time')
- container.resolve('time')
- // These will print the same timestamp at all times,
- // because `printTime` is singleton and
- // `getTime` was invoked when making the singleton.
- container.resolve('printTime')()
- container.resolve('printTime')()
- ```
Read the documentation for [container.createScope()](#containercreatescope)
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.
js
class UserService {
constructor(opts) {
this.emailService = opts.emailService
this.logger = opts.logger
}
}
or with destructuring:
js
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. CLASSIC mode has a
slightly higher initialization cost as it has to parse the function/class
to figure out the dependencies at the time of registration, however resolving
them will be much faster than when using PROXY. _Don't use CLASSIC if
you minify your code!_ We recommend using CLASSIC in Node and PROXY in
environments where minification is needed.
js
class UserService {
constructor(emailService, logger) {
this.emailService = emailService
this.logger = logger
}
}
Additionally, if the class has a base class but does not declare a constructor of its own, Awilix
simply invokes the base constructor with whatever dependencies it requires.
js
class Car {
constructor(engine) {
this.engine = engine
}
}
class Porsche extends Car {
vroom() {
console.log(this.engine) // whatever "engine" is
}
}