awilix vs inversify vs tsyringe
TypeScript 依存注入ライブラリの選定
awilixinversifytsyringe類似パッケージ:

TypeScript 依存注入ライブラリの選定

awilixinversifytsyringe は、JavaScript および TypeScript アプリケーションにおける依存注入(DI)を実現するための主要なライブラリです。これらは、クラス間の結合を緩やかにし、テストのしやすさやコードの保守性を高めるために使用されます。awilix はシンプルさと関数型アプローチを重視し、デコレータを必須としません。inversify は Java や C# の DI コンテナに似た強力な機能とデコレータベースの設定を提供します。tsyringe は Microsoft によって開発され、TypeScript ネイティブな体験と軽量さを両立させています。

npmのダウンロードトレンド

3 年

GitHub Starsランキング

統計詳細

パッケージ
ダウンロード数
Stars
サイズ
Issues
公開日時
ライセンス
awilix04,144327 kB32ヶ月前MIT
inversify023032.7 kB101ヶ月前MIT
tsyringe05,933149 kB761年前MIT

TypeScript 依存注入ライブラリ:awilix vs inversify vs tsyringe

大規模な TypeScript アプリケーションでは、クラス間の依存関係を管理することが重要になります。awilixinversifytsyringe はいずれも依存注入(DI)コンテナを提供しますが、その設計思想と使い勝手は大きく異なります。開発者が直面する具体的な課題に対して、これらがどのようにアプローチするかを比較します。

📝 依存関係の登録方法:明示的設定 vs デコレータ

依存関係をコンテナに登録する方法は、ライブラリごとに異なります。これはコードの記述量と可読性に直接影響します。

awilix は、登録を明示的なオブジェクト定義で行います。

  • デコレータは不要です。
  • 何に登録されているかがファイル内で一目でわかります。
// awilix: 明示的な登録
const { asClass, Lifetime } = require('awilix');

container.register({
  userService: asClass(UserService).setLifetime(Lifetime.SINGLETON),
  apiClient: asClass(ApiClient).setLifetime(Lifetime.TRANSIENT)
});

inversify は、デコレータとバインディング構文を組み合わせます。

  • クラス自体に @injectable() を付与します。
  • 別途バインディング設定が必要です。
// inversify: デコレータとバインディング
@injectable()
class UserService { /*...*/ }

container.bind<UserService>('UserService').to(UserService).inSingletonScope();

tsyringe は、デコレータだけで完結させることも可能です。

  • @injectable() をクラスに付与するだけで、多くの場合登録が完了します。
  • 追加のバインディングコードが最小限で済みます。
// tsyringe: デコレータ中心
@injectable()
class UserService { /*...*/ }

// 多くの場合、デコレータだけで解決可能ですが、明示的登録もできます
container.registerSingleton(UserService);

🔍 依存関係の解決方法:名前 vs クラス型

インスタンスを取得する際、何をキーとして使うかも重要な違いです。

awilix は文字列の名前をキーとして使用します。

  • 型安全ではありませんが、柔軟性があります。
  • 名前の変更時にリファクタリングツール頼みになります。
// awilix: 文字列名で解決
const userService = container.resolve('userService');

inversify は識別子(シンボルまたは文字列)を使用します。

  • Symbol を使うことで名前の衝突を防げます。
  • TypeScript との相性を考慮した設計です。
// inversify: 識別子で解決
const TYPES = { UserService: Symbol('UserService') };
const userService = container.get<UserService>(TYPES.UserService);

tsyringe はクラスコンストラクタ自体をキーとして使用できます。

  • 文字列ミスタイプが起きません。
  • TypeScript の型推論が最も効きやすいです。
// tsyringe: クラス型で解決
const userService = container.resolve(UserService);

⏳ ライフサイクル管理:スコープの扱い

インスタンスの生存期間(シングルトンか、毎回新規か)をどう制御するかは、パフォーマンスと状態管理に直結します。

awilix はスコープ機能を強くサポートしています。

  • リクエストごとのスコープ作成が容易です。
  • 明示的にスコープを生成して解決します。
// awilix: スコープ生成
const scope = container.createScope();
const user = scope.resolve('userService');
// スコープ破棄でリソース解放
scope.dispose();

inversify はバインディング時にスコープを定義します。

  • inSingletonScopeinTransientScope を設定します。
  • リクエストスコープには追加の設定が必要です。
// inversify: バインディング時にスコープ指定
container.bind('UserService').to(UserService).inSingletonScope();
const user = container.get('UserService');

tsyringe は登録時にライフサイクルを指定します。

  • registerSingletonregisterTransient など関数で区別します。
  • シンプルで直感的です。
// tsyringe: 登録関数で区別
container.registerSingleton(UserService);
const user = container.resolve(UserService);

🛠 TypeScript との親和性:デコレータの必須度

TypeScript プロジェクトにおいて、デコレータの設定負担は無視できません。

awilix はデコレータを必要としません。

  • tsconfig.json の設定がシンプルで済みます。
  • 反射メタデータ(reflect-metadata)の導入が不要です。
// awilix: 設定なしで動作
class UserService {
  constructor(private db: Database) {}
}
// デコレータ不要

inversify は反射メタデータを必須とします。

  • emitDecoratorMetadata: true が必要です。
  • reflect-metadata パッケージのインポートが必須です。
// inversify: メタデータ設定が必要
import 'reflect-metadata';
@injectable()
class UserService { /*...*/ }

tsyringe も反射メタデータを使用しますが、軽量です。

  • reflect-metadata が必要ですが、設定は inversify よりシンプルです。
  • TypeScript との統合がスムーズです。
// tsyringe: 標準的なデコレータ設定
import 'reflect-metadata';
@injectable()
class UserService { /*...*/ }

🤝 共通点:DI コンテナとしての役割

これら 3 つのライブラリは、根本的な目的を共有しています。ここでは、共通して提供される機能を確認します。

1. 🔄 依存関係の自動注入

  • コンストラクタの引数を解析して、必要な依存関係を自動で渡します。
  • 手動での new 呼び出しを減らせます。
// 3 つとも共通の概念
class OrderService {
  // 依存関係が自動で注入される
  constructor(private payment: PaymentGateway) {}
}

2. 🧪 テスト容易性の向上

  • 本物の実装ではなく、モック(偽物)を注入することでテストが容易になります。
  • 単体テストでの孤立化を支援します。
// 3 つとも共通のテストパターン
const mockPayment = { process: jest.fn() };
container.register('PaymentGateway', asValue(mockPayment));
// テスト実行

3. 📦 モジュール性の確保

  • 関心ごとの分離を促し、コードの再利用性を高めます。
  • 大規模プロジェクトでの構造維持に役立ちます。
// 3 つとも共通の利点
// 実装を差し替えても、利用側は変更不要
class UserController {
  constructor(private service: UserService) {}
}

📊 比較サマリー

特徴awilixinversifytsyringe
登録スタイル📝 明示的オブジェクト🏷️ デコレータ+バインディング🏷️ デコレータ中心
解決キー🔤 文字列名🔣 シンボル/文字列🏗️ クラス型
デコレータ必須❌ 不要✅ 必須✅ 必須
メタデータ❌ 不要✅ 必要✅ 必要
スコープ管理🎯 明示的スコープ⚙️ バインディング設定📝 登録関数で指定
学習コスト🟢 低い🔴 高い🟡 中程度

💡 結論:プロジェクトに合った選択を

awilix は、魔法のような仕組みを好まないチームに最適です 🪄。設定がすべてコードに現れるため、動作の予測が容易で、Node.js サーバーアプリケーションなどで重宝されます。

inversify は、機能の豊富さを求める場合に選ばれます 🛠️。複雑なバインディングルールや、既存の Java/C# 知識を活かしたい大規模システムに向いています。

tsyringe は、TypeScript の力を最大限に活かしたい場合に推奨されます 📘。デコレータの利便性を享受しつつ、inversify よりも軽量でモダンな体験を提供します。

最終的なアドバイス:小さなプロジェクトやシンプルさを求めるなら awilix、TypeScript 中心で標準的な DI を求めるなら tsyringe、特殊なバインディング要件があるなら inversify を検討してください。いずれも成熟したライブラリであり、正解はチームの好みとプロジェクトの規模によります。

選び方: awilix vs inversify vs tsyringe

  • awilix:

    設定ファイルを明示的に書くことを好み、デコレータやメタデータ反射に依存したくない場合に awilix を選択してください。Node.js バックエンドや、シンプルで透過的な DI 構成を重視するプロジェクトに適しています。

  • inversify:

    複雑なバインディング(名前付きバインディング、コンテキストバインディングなど)が必要で、C# や Java の DI 経験があるチームには inversify が適しています。大規模で構造化されたエンタープライズアプリケーションに向いています。

  • tsyringe:

    TypeScript プロジェクトで、デコレータ構文を活用しつつ、inversify よりも軽量でモダンなライブラリを求める場合に tsyringe を選んでください。Microsoft 生態系や、標準的な DI パターンで十分な場合に最適です。

awilix のREADME

Awilix

npm CI Coveralls npm npm node JavaScript Style Guide

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 intro to Dependency Injection with Awilix

Table of Contents

Installation

Install with npm

npm install awilix

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')

// Create the container and set the injectionMode to PROXY (which is also the default).
// Enable strict mode for extra correctness checks (highly recommended).
const container = awilix.createContainer({
  injectionMode: awilix.InjectionMode.PROXY,
  strict: true,
})

// 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.

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 }),
})

// 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!

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 its lifetime! Similarly, if a scoped module is resolved, and it depends on a transient registration, that remains in the scoped module for its lifetime. In the example above, if messageService was a singleton, it would be cached in the root container, and would always have the currentUser from the first request. Modules should generally not have a longer lifetime than their dependencies, as this can cause issues of stale data.

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')()

If you want a mismatched configuration like this to error, set strict in the container options. This will trigger the following error at runtime when the singleton printTime is resolved: AwilixResolutionError: Could not resolve 'time'. Dependency 'time' has a shorter lifetime than its ancestor: 'printTime'

In addition, registering a singleton on a scope other than the root container results in unpredictable behavior. In particular, if two different singletons are registered on two different scopes, they will share a cache entry and collide with each other. To throw a runtime error when a singleton is registered on a scope other than the root container, enable strict mode.

Read the documentation for container.createScope() for more examples.

Strict mode

Strict mode is a new feature in Awilix 10. It enables additional correctness checks that can help you catch bugs early.

In particular, strict mode enables the following checks:

  • When a singleton or scoped registration depends on a transient non-value registration, an error is thrown. This detects and prevents the issue where a shorter lifetime dependency can leak outside its intended lifetime due to its preservation in a longer lifetime module.
  • Singleton registrations on any scopes are disabled. This prevents the issue where a singleton is registered on a scope other than the root container, which results in unpredictable behavior.
  • Singleton resolution is performed using registrations from the root container only, which prevents potential leaks in which scoped registrations are preserved in singletons.

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. 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.

    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.

    class Car {
      constructor(engine) {
        this.engine = engine
      }
    }
    
    class Porsche extends Car {
      vroom() {
        console.log(this.engine) // whatever "engine" is
      }
    }
    

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(),
  // or..
  emailService: asFunction(makeEmailService).proxy()
  // or..
  notificationService: asClass(NotificationService).setInjectionMode(InjectionMode.CLASSIC)
})

// or..
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.

// CLASSIC
function database(connectionString, timeout, logger) {
  // ...
}

// Shorter, but less readable, order-sensitive
const db = database('localhost:1337;user=123...', 4000, new LoggerMock())

// PROXY
function database({ connectionString, timeout, logger }) {
  // ...
}

// Longer, more readable, order does not matter
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.

Important: auto-loading looks at a file's default export, which can be:

  • module.exports = ...
  • module.exports.default = ...
  • export default ...

To load a non-default export, set the [RESOLVER] property on it:

const { RESOLVER } = require('awilix')
export class ServiceClass {}
ServiceClass[RESOLVER] = {}

Or even more concise using TypeScript:

// TypeScript
import { RESOLVER } from 'awilix'
export class ServiceClass {
  static [RESOLVER] = {}
}

Note that multiple services can be registered per file, i.e. it is possible to have a file with a default export and named exports and for all of them to be loaded. The named exports do require the RESOLVER token to be recognized.

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()

// Load our modules!
container.loadModules(
  [
    // Globs!
    [
      // To have different resolverOptions for specific modules.
      'models/**/*.js',
      {
        register: awilix.asValue,
        lifetime: Lifetime.SINGLETON,
      },
    ],
    'services/**/*.js',
    'repositories/**/*.js',
  ],
  {
    // We want to register `UserService` as `userService` -
    // by default loaded modules are registered with the
    // name of the file (minus the extension)
    formatName: 'camelCase',
    // Apply resolver options to all modules.
    resolverOptions: {
      // We can give these auto-loaded modules
      // the deal of a lifetime! (see what I did there?)
      // By default it's `TRANSIENT`.
      lifetime: Lifetime.SINGLETON,
      // We can tell Awilix what to register everything as,
      // instead of guessing. If omitted, will inspect the
      // module to determine what to register as.
      register: awilix.asClass,
    },
  },
)

// We are now ready! We now have a userService, userRepository and emailService!
container.resolve('userService').getUser(1)

Important: Auto-loading relies on glob and therefore does not work 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()
  // Using the fluid variant:
  .register({
    userRepository: asFunction(createUserRepository)
      // Provide an injection function that returns an object with locals.
      // The function is called once per resolve of the registration
      // it is attached to.
      .inject(() => ({ timeout: 2000 })),
  })

  // Shorthand variants
  .register({
    userRepository: asFunction(createUserRepository, {
      injector: () => ({ timeout: 2000 }),
    }),
  })

  // Stringly-typed shorthand
  .register(
    'userRepository',
    asFunction(createUserRepository, {
      injector: () => ({ timeout: 2000 }),
    }),
  )

  // with `loadModules`
  .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
  }
}

// `RESOLVER` is a Symbol.
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) // 'SCOPED'
console.log(container.registrations.awesomeService.injectionMode) // 'CLASSIC'

Additionally, if we add a name field and use loadModules, the name is used for registration (ignoring formatName if provided).

// `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) // 'SCOPED'
console.log(container.registrations.superService.injectionMode) // 'CLASSIC'

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())
      // Disposables must be either `scoped` or `singleton`.
      .singleton()
      // This is called when the pool is going to be disposed.
      // If it returns a Promise, it will be awaited by `dispose`.
      .disposer((pool) => pool.end()),
  })
}

const container = configureContainer()
const todoStore = container.resolve('todoStore')

// Later...
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)
  // Dispose container when the server closes.
  server.on('close', () => container.dispose())
  return server
}

test('server does server things', async () => {
  const server = createServer()
  server.listen(3000)

  /// .. run your tests..

  // Disposes everything, and your process no
  // longer hangs on to zombie connections!
  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.
  • InferCradleFromResolvers (TypeScript utility type)
  • InferCradleFromContainer (TypeScript utility type)
  • InferResolverType (TypeScript utility type)

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 injections
  • register: Only used in loadModules, determines how to register a loaded module explicitly
  • isLeakSafe: true if this resolver should be excluded from lifetime-leak checking performed in strict mode. Defaults to false.

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.
    • options.strict: Enables strict mode. Defaults to false.

InferCradleFromResolvers

A TypeScript utility type that extracts the cradle type from an object of resolvers. Useful when you want to type a module's dependencies from a resolvers object without having access to the container itself — for example, when the container is assembled in one module but consumed in another:

import { type InferCradleFromResolvers, asClass, asValue } from 'awilix'

const resolvers = {
  userService: asClass(UserService),
  logger: asValue(new Logger()),
}

type MyCradle = InferCradleFromResolvers<typeof resolvers>
// => { userService: UserService; logger: Logger }

InferResolverType is also exported for extracting the resolved type from a single resolver:

import { type InferResolverType, asClass } from 'awilix'

const resolver = asClass(UserService).singleton()
type T = InferResolverType<typeof resolver>
// => UserService

InferCradleFromContainer

A TypeScript utility type that extracts the Cradle type from an AwilixContainer type. This is useful when you need to reference the cradle type of an existing container without manually redeclaring it.

Important: One of the core principles of Awilix is that it should be transparent to your application code. The recommended approach is to define standalone options types for your services (e.g. MyServiceOptions) that declare only the dependencies they need, without any reference to Awilix or the container's cradle type. This keeps your application code fully decoupled from the DI container and preserves true inversion of control.

That said, if you want a fully type-safe container and are aware of the trade-offs involved (coupling to a "god type", losing transparency, and difficulty mixing singleton and scoped registrations), the inference utilities below make that possible.

import { createContainer, type InferCradleFromContainer, asClass, asValue } from 'awilix'

const container = createContainer()
  .register({
    userService: asClass(UserService),
    logger: asValue(new Logger()),
  })

// Extract the cradle type from the container
type MyCradle = InferCradleFromContainer<typeof container>
// => { userService: UserService; logger: Logger }

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)
// << [{ name: 'someService', path: 'path/to/services/someService.js' }]

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.

AwilixRegistrationError

This is a special error thrown when Awilix is unable to register a dependency due to a strict mode violation. You can catch this error and use err instanceof AwilixRegistrationError if you wish.

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) // 'CLASSIC'

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 | symbol, resolver: Resolver): this
  • register(nameAndResolverPair): AwilixContainer<Cradle & InferCradleFromResolvers<R>>

The object overload returns a container whose cradle type includes the newly registered types. This means the cradle type accumulates automatically when you chain .register() calls — no manual interface needed:

const container = createContainer()
  .register({
    userService: asClass(UserService),
    logger: asValue(new Logger()),
  })

container.cradle.userService // => UserService ✓
container.cradle.logger      // => Logger ✓

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 support chaining! register returns the container!

// name-resolver
container.register('connectionString', asValue('localhost:1433;user=...'))
container.register('mailService', asFunction(makeMailService))
container.register('context', asClass(SessionContext))

// object
container.register({
  connectionString: asValue('localhost:1433;user=...'),
  mailService: asFunction(makeMailService, { lifetime: Lifetime.SINGLETON }),
  context: asClass(SessionContext, { lifetime: Lifetime.SCOPED }),
})

// `asClass` and `asFunction` also supports a fluid syntax.
// This...
container.register(
  'mailService',
  asFunction(makeMailService).setLifetime(Lifetime.SINGLETON),
)
// .. is the same as this:
container.register('context', asClass(SessionContext).singleton())

// .. and here are the other `Lifetime` variants as fluid functions.
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.hasRegistration()

  • container.hasRegistration(name: string | symbol): boolean

Determines if the container has a registration with the given name. Also checks ancestor containers.

container.loadModules()

Given an array of globs, registers the modules and returns the container.

💡 When using opts.esModules, a Promise is returned due to using the asynchronous import().

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 newed!

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.
  • opts.esModules: Loads modules using Node's native ES modules. This makes container.loadModules asynchronous, and will therefore return a Promise! This is only supported on Node 14.0+ and should only be used if you're using the Native Node ES modules

Example:

// index.js
container.loadModules(['services/*.js', 'repositories/*.js', 'db/db.js'])

container.cradle.userService.getUser(123)

// to configure lifetime for all modules loaded..
container.loadModules([
  'services/*.js',
  'repositories/*.js',
  'db/db.js'
], {
  resolverOptions: {
    lifetime: Lifetime.SINGLETON
  }
})

container.cradle.userService.getUser(123)

// to configure lifetime for specific globs..
container.loadModules([
  ['services/*.js', Lifetime.SCOPED], // all services will have scoped lifetime
  'repositories/*.js',
  'db/db.js'
], {
  resolverOptions: {
    lifetime: Lifetime.SINGLETON // db and repositories will be singleton
  }
)

container.cradle.userService.getUser(123)

// to use camelCase for modules where filenames are not camelCase
container.loadModules(['repositories/account-repository.js', 'db/db.js'], {
  formatName: 'camelCase'
})

container.cradle.accountRepository.getUser(123)

// to customize how modules are named in the container (and for injection)
container.loadModules(['repository/account.js', 'service/email.js'], {
  // This formats the module name so `repository/account.js` becomes `accountRepository`
  formatName: (name, descriptor) => {
    const splat = descriptor.path.split('/')
    const namespace = splat[splat.length - 2] // `repository` or `service`
    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 fast-glob and is therefore not supported in module bundlers like Webpack, Rollup, esbuild 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.

  • returns AwilixContainer
// Increments the counter every time it is resolved.
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 === 3

A Scope maintains it's own cache of Lifetime.SCOPED registrations, meaning it does not use the parent's cache for scoped registrations.

let counter = 1
container.register({
  counterValue: asFunction(() => counter++).scoped(),
})
const scope1 = container.createScope()
const scope2 = container.createScope()

// The root container is also a scope.
container.cradle.counterValue === 1
container.cradle.counterValue === 1

// This scope resolves and caches it's own.
scope1.cradle.counterValue === 2
scope1.cradle.counterValue === 2

// This scope resolves and caches it's own.
scope2.cradle.counterValue === 3
scope2.cradle.counterValue === 3

A scope may also register additional stuff - they will only be available within that scope and it's children.

// Register a transient function
// that returns the value of the scope-provided dependency.
// For this example we could also use scoped lifetime.
container.register({
  scopedValue: asFunction((cradle) => 'Hello ' + cradle.someValue),
})

// Create a scope and register a value.
const scope = container.createScope()
scope.register({
  someValue: asValue('scope'),
})

scope.cradle.scopedValue === 'Hello scope'
container.cradle.someValue
// throws AwilixResolutionException
// because the root container does not know
// of the resolver.

Things registered in the scope take precedence over registrations in the parent scope(s). This applies to both the registration directly requested from the scope container, and any dependencies that the registration uses.

// It does not matter when the scope is created,
// it will still have anything that is registered
// in its parent.
const scope = container.createScope()

container.register({
  value: asValue('root'),
  usedValue: asFunction((cradle) => `hello from ${cradle.value}`),
})

scope.register({
  value: asValue('scope'),
})

container.cradle.value === 'root'
scope.cradle.value === 'scope'
container.cradle.usedValue === 'hello from root'
scope.cradle.usedValue === 'hello from scope'

Registering singletons in a scope results in unpredictable behavior and should be avoided. Having more than one singleton with the same name in different scopes will result in them sharing a cache entry and colliding with each other. To disallow such registrations, enable strict mode in the container options.

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).

// The following are equivelant..
class MyClass {
  constructor({ ping }) {
    this.ping = ping
  }

  pong() {
    return this.ping
  }
}

const createMyFunc = ({ ping }) => ({
  pong: () => ping,
})

container.register({
  ping: asValue('pong'),
})

// Shorthand
// This uses `utils.isClass()` to determine whether to
// use `asClass` or `asFunction`. This is fine for
// one-time resolutions.
const myClass = container.build(MyClass)
const myFunc = container.build(createMyFunc)

// Explicit
// Save the resolver if you are planning on invoking often.
// **Especially** if you're using classic resolution.
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())
    // IMPORTANT! Must be either singleton or scoped!
    .singleton(),
})

const pool = container.resolve('pool')
pool.query('...')

// Later..
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.mjs
  • ES Modules, for use with module bundlers in the browser - lib/awilix.browser.mjs
  • 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

Ecosystem

Contributing

Please see our contributing.md

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