{"id":25445806,"url":"https://github.com/divmgl/nwire","last_synced_at":"2025-11-01T21:30:33.994Z","repository":{"id":32005926,"uuid":"35576897","full_name":"divmgl/nwire","owner":"divmgl","description":"Simplified dependency injection in TypeScript","archived":false,"fork":false,"pushed_at":"2023-10-30T15:23:16.000Z","size":237,"stargazers_count":9,"open_issues_count":0,"forks_count":1,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-02-12T06:00:59.109Z","etag":null,"topics":["dependency-injection","inversion-of-control","typescript"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/divmgl.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2015-05-13T22:17:15.000Z","updated_at":"2023-10-30T04:38:05.000Z","dependencies_parsed_at":"2022-08-26T08:50:46.879Z","dependency_job_id":"1678ccbd-240f-465f-a75e-cb08c1f1ac0e","html_url":"https://github.com/divmgl/nwire","commit_stats":{"total_commits":75,"total_committers":5,"mean_commits":15.0,"dds":0.6,"last_synced_commit":"e093594376f6a90ae80fba305a10c08ed8d3af34"},"previous_names":[],"tags_count":8,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/divmgl%2Fnwire","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/divmgl%2Fnwire/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/divmgl%2Fnwire/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/divmgl%2Fnwire/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/divmgl","download_url":"https://codeload.github.com/divmgl/nwire/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":238930125,"owners_count":19554127,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["dependency-injection","inversion-of-control","typescript"],"created_at":"2025-02-17T16:50:25.059Z","updated_at":"2025-11-01T21:30:33.944Z","avatar_url":"https://github.com/divmgl.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# nwire\n\n![Tests](https://github.com/divmgl/nwire/actions/workflows/tests.yml/badge.svg)\n\n`nwire` is a dependency injection container with a strongly-typed fluent API that makes it easier to write large type-safe applications.\n\n```tsx\nimport { Container, Service } from \"nwire\"\n\ntype MyTypedContext = {\n  banner: string\n  my: MyService\n}\n\nexport class MyService extends Service\u003cMyTypedContext\u003e() {\n  helloWorld() {\n    return this.banner\n  }\n}\n\nconst context = Container.new()\n  .register(\"banner\", () =\u003e \"Hello world!\")\n  .singleton(\"my\", MyService)\n  .context()\n\nconsole.log(context.my.helloWorld()) // =\u003e console output: \"Hello world!\"\n```\n## Installation\n\n```shell\nnpm i nwire\nyarn add nwire\npnpm add nwire\n```\n\n## Getting Started\n\n#### 1. Create a type for your `Context`:\n\n```tsx\nimport Mailgun from \"mailgun.js\"\n\ntype AppContext = {\n  mailgun: Mailgun\n  users: UsersService\n  email: EmailService\n  registration: RegistrationService\n}\n```\n\n\u003e 💡 `nwire` can infer the context type for you, but this is a simplified example.\n\n#### 2. Create services. \n\nA service is a class that has a dependency on your `AppContext`. `nwire` provides a `Service` class factory for you to use right away:\n\n```tsx\nimport { Service } from \"nwire\"\n\nexport class UsersService extends Service\u003cAppContext\u003e() {\n  // Dependencies in your container are now available as getters on this.\n  async findOne(id: string) {\n    return await this.db.users.findOne({ where: { id } })\n  }\n}\n\nexport class EmailService extends Service\u003cAppContext\u003e() {\n  async send(to: string, subject: string, body: string) {\n    return this.mailgun.createMessage({ to, subject, body })\n  }\n}\n\n// ...\n```\n\n#### 3. Create a `Container`, register your dependencies and generate a `Context`:\n\n```tsx\n// createAppContext.ts\nimport Mailgun from require(\"mailgun.js\")\nimport formData from \"form-data\"\n\nfunction createAppContext() {\n  return Container.new\u003cAppContext\u003e()\n    .register(\"mailgun\", () =\u003e new Mailgun(formData))\n    .singleton(\"users\", UsersService)\n    .singleton(\"email\", EmailService)\n    .singleton(\"registration\", RegistrationService)\n    .context()\n}\n\nexport { createAppContext }\n\n// Note: you can also try to omit the type and let TypeScript infer it for\n// you. Using this approach will avoid having to modify multiple places to \n// introduce/remove dependencies.\n\nfunction createAppContext() {\n  return Container.new()\n    .register(\"mailgun\", () =\u003e new Mailgun(formData))\n    .singleton(\"users\", UsersService)\n    .singleton(\"email\", EmailService)\n    .singleton(\"registration\", RegistrationService)\n    .context()\n}\n\ntype AppContext = ReturnType\u003ctypeof createAppContext\u003e\n\nexport { createAppContext, AppContext }\n\n// ⚠️ Use this approach with caution as it can lead to circular references.\n```\n\n#### 4. Pass it everywhere:\n\nUse this `Context` in your entire app. It's meant to be passed around to all of your entrypoints and classes that need dependencies:\n\n```tsx\n// server.ts\nimport { createAppContext } from \"./createAppContext\"\n// ...\n\nconst context = createAppContext()\n\nconst server = new AwesomeHttpFrameworkServer()\n\nserver.use((req, res, next) =\u003e {\n  // You decide when the context is added to the request to be used downstream\n  req.context = context\n  next()\n})\n\nserver.get(\"/users/:id\", async (req, res) =\u003e {\n  const user = await req.context.users.findOne(req.params.id)\n  res.send(user)\n})\n```\n```tsx\n// EmailWorker.ts\nimport { AppContext } from \"./AppContext\"\nimport { Service } from \"nwire\"\n// ...\n\nexport class EmailWorker extends Service\u003cAppContext\u003e() // Note the parens\n  worker: Worker\n\n  constructor(context: AppContext) {\n    super(context)\n    \n    this.worker = new Worker({ \n      connection: redis, \n      handler: async (payload) =\u003e {\n        await this.email.send(payload.to, payload.subject, payload.body)\n      }\n    })\n  }\n```\n```tsx\n// Pass the container to literally anything\n```\n\n## API\n\n`nwire` has two high-level concepts: the `Container` and the `Context`. The `Container` allows you to compose a strongly-typed `Context`, and the `Context` is the proxy that resolves dependencies for you lazily. The `Context` lives within the `Container` (as a closure) and interacts with the registration of your dependencies behind the scenes.\n\nWhen using the library you likely won't have to think about these semantics, but we figured it's important to understand how it works under the hood.\n\n### `Container`\n\nThe `Container` class is the main entrypoint for `nwire`. It provides a fluent API for registering\ndependencies and creating `Context`s from them.\n\n#### Creating a `Container`\n\nYou can use `new Container()` to create a container:\n\n```tsx\nconst container = new Container()\n\ncontainer.register(\"prisma\", () =\u003e new PrismaClient())\ncontainer.register(\"redis\", () =\u003e new Redis())\n\nconst context = container.context()\n```\n\nIn a majority of cases you'll be creating a single container, registering a bunch of dependencies, and then grabbing the generated `Context`. For this reason we've included static methods that return a new container and are chainable, so you can write your code like this instead:\n\n```tsx\nconst context = Container.new()\n  .register(\"prisma\", () =\u003e new PrismaClient())\n  .register(\"redis\", () =\u003e new Redis())\n  .context()\n```\n\nThe choice is yours: you can keep the `Container` around in case you want to register more dependencies later, or you can create the `Context` immediately and use that everywhere.\n\n#### `Container.register`\n\nRegisters a dependency with the container. The first argument is an accessor key that you'll use to access the dependency later, and the second argument is a factory function that returns the dependency:\n\n```tsx\nContainer.register(\"prisma\", () =\u003e new PrismaClient()) // =\u003e Container\n```\n\nThe factory function is called with the fully resolved `Context` as the first argument. This allows you to pass the `Context` to your dependencies:\n\n```tsx\nContainer.register(\"users\", (context) =\u003e new UsersService(context)) // =\u003e Container\n```\nThe `Context` that's sent to the dependency will be fully setup.\n\n**TypeScript**\n\nThis works out of the box for JavaScript users, but what about TypeScript users? The first thing you'll run into is that while the `Context` is fully resolved, TypeScript doesn't yet know about all of the registrations. For instance, the following results in a compiler error:\n\n```tsx\nconst context = Container.new()\n  .register(\"tasksCreator\", (context) =\u003e new TasksCreator(context))\n  // Argument of type '{}' is not assignable to parameter of type 'AppContext'.\n  // Type '{}' is missing the following properties from type 'AppContext': tasks, tasksCreator\n  .register(\"tasks\", (context) =\u003e new SQLiteTaskStore(context))\n```\nThis is because `TasksCreator` is asking for a fully typed context but the context at the time of registration is empty (`{}`). There's two main ways to overcome this:\n\n* If you prefer to keep a static type with your dependencies explicitly listed out, use `Container.new\u003cYourContext\u003e()` when creating the container to explicitly define the context from the beginning.\n* Use `Container.singleton` which handles this out of the box. You can read more about it in the [Singletons](#Container.singleton) section.\n\n#### `Container.singleton`\n\nYour goal will often be to pass in the fully resolved `Context` to classes. For this reason `nwire` provides a function that will create a new instance of your class with a fully resolved `Context` whenever the dependency is resolved:\n\n```tsx\nContainer.new().singleton(\"users\", UsersService) // =\u003e Container\n```\n\nNow when `context.users` is accessed, `nwire` will call `new UsersService(context)` where `context` is a fully resolved context from your `Container`. It'll take the resulting instance and register it under the `users` name as a singleton. Follow-up calls will access the singleton instance that was created with the dependency was first resolved.\n\nUsing `single`\nThis avoids the TypeScript typing issues as the interface expects the `Container` to be fully registered at the time of resolution.\n\n\n```tsx\nconst user = await context.users.findOne(\"123\")\n\n// Equivalent without nwire, sans singleton setup:\nconst users = new UsersService(container.context())\nconst user = await users.findOne(\"123\")\n```\n\nYou can also pass in additional arguments to the constructor:\n\n```tsx\nContainer.new().singleton(\"users\", UsersService, { cookieSecret: process.env.COOKIE_SECRET })\n```\n\n#### `Container.instance` [deprecated]\n\nAn alias for `Container.singleton`.\n\n\n#### `Container.group`\n\nSometimes you'll want to group things together within the `Container`. You could technically do this:\n\n```tsx\nconst context = Container.new()\n  .register(\"services\", (context) =\u003e ({\n    users: new UsersService(context),\n    tasks: new TasksService(context),\n  }))\n  .context()\n```\n\nAnd now all services will be nested under `services`:\n\n```tsx\ncontext.services.users.findOne(\"123\")\n```\n\nHowever, this has a big issue: once you access `service` for the first time you make an instance of every single class all at once.\n\n`nwire` provides a solution for this: `Container.group`. `Container.group` creates a nested `Container` that will only resolve when you access properties within it. The nested container will be passed as the first argument to the function you pass in:\n\n```tsx\nconst context = Container.new()\n  .group(\"services\", (services) =\u003e\n    services\n      .singleton(\"users\", UsersService)\n      .singleton(\"tasks\", TasksService)\n  )\n  .context()\n\ntype AppContext = typeof context\n```\n```tsx\ntype AppContext = {\n  services: {\n    users: UsersService\n    tasks: TasksService\n  }\n}\n```\n\n```tsx\n// Two containers are used for resolution here: the root container and the nested `services` container\ncontext.services.users.findOne(\"123\")\n```\n\n### `Context`\n\nThe `Context` class is the proxy that the `Container` produces. This class allows you to access your dependencies using the names you registered them with:\n\n```tsx\nconst context = Container.new()\n  .register(\"users\" /** Registry name */, () =\u003e new UsersService())\n  .context() // Proxy created at this point\n\nconst user = await context.users.findOne(\"123\")\n```\n\nThe object returned by `Context` is a shallow object one-level deep with getters. For instance, considering the following type:\n\n```tsx\ntype AppContext = {\n  services: {\n    users: UsersService\n    tasks: TasksService\n  },\n  events: {\n    profileUpdated: ProfileUpdatedEvent\n  }\n}\n```\n\nThe shallow object returned by `Context` will be:\n\n```tsx\n{ services: [Getter], events: [Getter] }\n```\n\nIt's designed like this to circumvent situations where the underlying operations done to your packages could enumerate them.\n\n#### `Container.context`\n\nCreates a new `Context` class. This is the class you're meant to pass around to all of your dependencies. It's responsible for resolving dependencies:\n\n```tsx\nconst context = Container.new()\n  // ... lots of registrations here\n  .register(\"users\", () =\u003e new UsersService())\n  .context()\n\nconst user = await context.users.findOne(\"123\")\n// `users` is resolved lazily.\n```\n\n`nwire` will only resolve dependencies when they're needed. This is an intentional design decision to avoid having to instantiate the entire `Container`, which is especially useful for tests. However, the type that `.context()` outputs will always be the _fully_ typed `Context`.\n\n#### `Container.context\u003cT\u003e`: ⚠️ Needed for TypeScript\n\nIt's recommended you pass an explicit type to the `context` function.\n\n```tsx\nexport type AppContext = {\n  users: UsersService\n  tasks: TasksService\n  tasksCreator: TasksCreator\n}\n\nconst context = Container.new()\n  .register(\"tasksCreator\", (context) =\u003e new TasksCreator(context))\n  .register(\"tasks\", (context) =\u003e new SQLiteTaskStore(context))\n  .context\u003cAppContext\u003e()\n```\n\nDoing so helps you avoid circular dependencies.\n\n### Lifetime of a dependency\n\n`nwire` will resolve dependencies for you lazily and keep an instance of the dependency as a singleton by default.\n\n```tsx\ncontainer.register(\"randomizer\", () =\u003e new RandomizerClass())\ncontainer.resolve\u003cRandomizerClass\u003e(\"randomizer\").id // =\u003e 353\ncontainer.resolve\u003cRandomizerClass\u003e(\"randomizer\").id // =\u003e 353\n```\n\nUnless unregistered, the dependency will be kept in memory for the lifetime of the `Container`.\n\nHowever, you can create transient dependencies by specifying the `{ transient: true }` option:\n\n```tsx\ncontainer.register(\"randomizer\", () =\u003e new RandomizerClass(), {\n  transient: true,\n})\ncontainer.resolve\u003cRandomizerClass\u003e(\"randomizer\").id // =\u003e 964\ncontainer.resolve\u003cRandomizerClass\u003e(\"randomizer\").id // =\u003e 248\n```\n\n`nwire` will invoke this function when the `randomizer` dependency is either resolved through the `Container` using `Container.resolve` or through the `Context` using `context.randomizer`.\n\nThere is currently no API for transient `instance` registrations, so if you do want to create a unique instance on every call you'll need to do it using `register`:\n\n```tsx\nconst context = Container.new\u003cAppContext\u003e()\n  .register(\"users\", (context) =\u003e new UsersService(context), { transient: true }))\n  .context()\n```\n\n### `Service`\n\nGenerally you want to pass your `Context` around to all of your class constructors and services to enable the lazy resolution of dependencies. You can do this manually by passing the `Context` as the first argument to your constructor:\n\n```ts\nexport class MyService {\n  constructor(protected context: MyTypedContext) {}\n}\n```\n\nNow you can register the `MyService` class in `nwire` as a `singleton` and `nwire` will take care of the rest.\n\nThis will work fine for a while but then you'll run into several issues:\n\n* You'll have to remember to instrument the `Context` around to all of your dependencies\n* You'll need to call `this.context` to access your dependencies every time, which looks very verbose \n* `this.context` lives in the class and can be replaced at any moment. \n\nTo overcome these challenges you'll eventually land on a pattern that looks like this:\n\n```tsx\nexport class Service {\n  constructor(private _context: MyTypedContext) {}\n  \n  // No longer possible to clobber `context`\n  get context() {\n    return this._context\n  }\n\n  // Can use `this.users` instead of `this.context.users`\n  get users() {\n    return this.context.users\n  }\n}\n\nexport class MyService extends Service {\n  // Optional constructor but you'll need to remember it in situations where arguments are involved\n  constructor(private _context: MyTypedContext, private serviceName: string) {\n    super(_context)\n    // Do something with `serviceName`\n  }\n}\n```\n\nThis works, but again you'll outgrow this once you add more dependencies and your container gets more complex.\n\nFor this reason `nwire` provides a base class named `Service` which takes care of all of these concerns for you:\n\n```tsx\nimport { Service } from \"nwire\"\n\nexport class MyService extends Service\u003cMyTypedContext\u003e() { // Note the parens\n  helloWorld() {\n    return this.context.banner;\n  }\n}\n```\n\nClasses that extend the `Service` class will fit neatly into the `Container.prototype.singleton` API:\n\n```tsx\nconst context = Container.new()\n  .register(\"banner\", () =\u003e \"Hello world!\")\n  .singleton(\"my\", MyService) // No type errors\n  .context()\n\ncontext.my.helloWorld() // =\u003e console output: \"Hello world!\"\n```\n\n`Service` is a class factory that will take your `Context` and create getters for it. This way you don't have to write `context` getters for all of your dependencies:\n\n```tsx\nexport class UsersService extends Service\u003cMyTypedContext\u003e() {\n  async findOne(id: string) {\n    // No need to call `this.context.prisma.users.findOne`\n    return await this.db.users.findOne({ where: { id } })\n  }\n}\n\nexport class UserUpdaterService extends Service\u003cMyTypedContext\u003e() {\n  async update(id: string, name: string) {\n    const existingUser = await this.users.findOne(id)\n    if (!existingUser) throw new Error(\"User not found\")\n    return await this.db.users.update({ where: { id }, data: { name }})\n  }\n}\n\nconst context = Container.new()\n  .singleton(\"users\", UsersService)\n  .singleton(\"userUpdater\", UserUpdaterService)\n  .context()\n```\n\n## What is dependency injection?\n\nDependency injection is the process of keeping your components loosely coupled. This pattern makes\nit easy to swap out the underlying implementations of dependencies as long as the contracts stay\nthe same.\n\n### An example\n\nConsider a `UsersService`:\n\n```tsx\nclass UsersService {\n  constructor(private psql: Postgres) {}\n\n  async find(id: string) {\n    return await this.psql.query(\"SELECT * FROM id WHERE id = ?\", [id])\n  }\n\n  // ... other functions that use this.psql //\n}\n```\n\nIn this contrived example, the users service is tightly coupled to the Postgres client library. This\nmeans that if in the future you wanted to change the underlying access implementation, you'd have to\nintroduce another dependency in the constructor, thus now coupling both libraries tightly to the\nservice:\n\n```tsx\nclass UsersService {\n  constructor(private psql: Postgres, private prisma: Prisma) {}\n\n  async find(id: string) {\n    return await this.prisma.users({ where: { id } })\n  }\n}\n```\n\nYou can overcome this by using a repository:\n\n```tsx\nclass UsersService {\n  constructor(private users: UserRepository /* Interface or type */) {}\n\n  async find(id: string) {\n    return await this.users.findOne(id)\n  }\n}\n```\n\nBy passing in a repository to the constructor that encapsulates the data access concerns we can\nchange the underlying implementation without affecting dependent services.\n\nHowever, we still have an issue: we have to manually pass in `UserRepository` every single time:\n\n```tsx\nconst userRepository = new UserRepository()\nconst usersService = new UsersService(userRepository)\n```\n\nAdditionally, if you pass in a variety of different dependencies often to services, you'll end up\nwith a god object of all of your dependencies:\n\n```tsx\nconst dependencies = {\n  usersService: new UsersService(userRepository),\n  ticketingService: new TicketingService(),\n}\n\nconst server = new Server(dependencies) // Massive object that's already been resolved\n```\n\nThis is where **dependency injection** comes in. Dependency injection is the process of resolving a\nnecessary dependency when needed. It works by registering all of your dependencies within a container\nand when a dependency is needed the framework will resolve it for you.\n\n### With `nwire`\n\nConsider the previous example in `nwire`:\n\n```tsx\nconst context = Container.new()\n  .singleton(\"users\", UsersService)\n  .register(\"prisma\", new PrismaClient())\n  .register(\"psql\", new Postgres())\n\nconst user = await context.users.find(\"123\")\n```\n\n`nwire` keeps a list of your registrations and makes them available to your services as needed. When\nthe `UsersService` calls `users.findOne`, `nwire` will **lazily** return an instance of\n`UserRepository`.\n\n\u003e ⚠️ `nwire` contexts are not normal objects. `nwire` uses a proxy under the hood to evaluate your dependencies as needed. This is an intentional design decision to avoid having to instantiate the entire `Container` for tests.\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdivmgl%2Fnwire","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdivmgl%2Fnwire","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdivmgl%2Fnwire/lists"}