{"id":14384408,"url":"https://github.com/devthefuture-org/nctx","last_synced_at":"2026-01-23T05:29:28.404Z","repository":{"id":57309304,"uuid":"452846843","full_name":"devthefuture-org/nctx","owner":"devthefuture-org","description":"NodeJS Contextual Dependency Injection using native async_hooks - IoC","archived":false,"fork":false,"pushed_at":"2023-09-07T16:38:41.000Z","size":102,"stargazers_count":4,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2024-12-14T10:48:25.793Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"JavaScript","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/devthefuture-org.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}},"created_at":"2022-01-27T21:14:50.000Z","updated_at":"2023-09-07T13:08:21.000Z","dependencies_parsed_at":"2023-09-07T15:19:25.554Z","dependency_job_id":null,"html_url":"https://github.com/devthefuture-org/nctx","commit_stats":null,"previous_names":["devthefuture-org/nctx","devthejo/nctx"],"tags_count":17,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/devthefuture-org%2Fnctx","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/devthefuture-org%2Fnctx/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/devthefuture-org%2Fnctx/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/devthefuture-org%2Fnctx/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/devthefuture-org","download_url":"https://codeload.github.com/devthefuture-org/nctx/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":230716496,"owners_count":18269778,"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":[],"created_at":"2024-08-28T18:01:21.922Z","updated_at":"2026-01-23T05:29:28.354Z","avatar_url":"https://github.com/devthefuture-org.png","language":"JavaScript","readme":"# nctx\n\n[![npm version](https://img.shields.io/npm/v/nctx.svg)](https://www.npmjs.com/package/nctx)\n[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)\n\n## What is nctx?\n\n**nctx** is a lightweight, powerful Dependency Injection (DI) container for Node.js applications, enhanced with native `async_hooks` capabilities. It implements the Inversion of Control (IoC) pattern, allowing you to decouple components and manage dependencies throughout your application's asynchronous execution flow.\n\n### Inversion of Control (IoC) and Dependency Injection\n\nInversion of Control is a design principle where the control flow of a program is inverted: instead of your code controlling when and how dependencies are created and used, this control is delegated to an external container. Dependency Injection is a specific implementation of IoC where dependencies are \"injected\" into components rather than created within them.\n\nnctx provides an elegant solution for implementing these patterns in Node.js applications, making it easier to:\n\n- Share request-scoped data across your application without passing it through function parameters\n- Isolate execution contexts in concurrent operations\n- Implement clean dependency injection patterns\n- Avoid callback hell and parameter pollution\n\nBuilt on Node.js's native [async_hooks API](https://nodejs.org/api/async_hooks.html), nctx maintains context across asynchronous boundaries automatically, with minimal overhead.\n\n## Table of Contents\n\n- [What is nctx?](#what-is-nctx)\n  - [Inversion of Control (IoC) and Dependency Injection](#inversion-of-control-ioc-and-dependency-injection)\n- [Table of Contents](#table-of-contents)\n- [Installation](#installation)\n- [Core Concepts](#core-concepts)\n  - [Async Context](#async-context)\n  - [Context](#context)\n  - [Registry](#registry)\n  - [Providing and Accessing Context](#providing-and-accessing-context)\n- [Basic Usage](#basic-usage)\n  - [JavaScript Example](#javascript-example)\n  - [TypeScript Example](#typescript-example)\n- [Common Use Cases](#common-use-cases)\n  - [Forking Contexts](#forking-contexts)\n    - [Why Fork Contexts?](#why-fork-contexts)\n    - [Simple Forking Example](#simple-forking-example)\n    - [Deep vs. Shallow Forking](#deep-vs-shallow-forking)\n  - [Express Integration](#express-integration)\n  - [Logging and Error Handling](#logging-and-error-handling)\n- [API Reference](#api-reference)\n  - [Context Creation](#context-creation)\n  - [Context Methods](#context-methods)\n  - [Static Methods](#static-methods)\n- [Advanced Usage](#advanced-usage)\n  - [Context Relationships](#context-relationships)\n    - [Following Contexts](#following-contexts)\n    - [Fallback Contexts](#fallback-contexts)\n  - [Sharing Contexts](#sharing-contexts)\n  - [Extending Contexts](#extending-contexts)\n    - [JavaScript Example](#javascript-example-1)\n    - [TypeScript Example](#typescript-example-1)\n- [Best Practices](#best-practices)\n  - [Do's](#dos)\n  - [Don'ts](#donts)\n- [Running the Examples](#running-the-examples)\n- [Related Libraries](#related-libraries)\n- [Contributing](#contributing)\n\n## Installation\n\n```sh\n# Using npm\nnpm install nctx\n\n# Using yarn\nyarn add nctx\n```\n\n**Requirements**: Node.js 16 or higher\n\n## Core Concepts\n\n### Async Context\n\nIn asynchronous applications, tracking the execution context across callbacks, promises, and event handlers can be challenging. nctx leverages Node.js's `AsyncLocalStorage` to maintain context throughout the entire asynchronous execution tree.\n\n### Context\n\nA `Context` is a container for storing and retrieving values within an asynchronous execution flow. Each context:\n\n- Has a unique identifier (name)\n- Can store values using keys\n- Maintains isolation between different execution paths\n- Can be forked to create isolated sub-contexts\n\n### Registry\n\nA `Registry` is the internal storage mechanism for a context. It contains:\n\n- An object store for string/number keys\n- A Map for non-primitive keys (like Symbols)\n- Optional parent reference for hierarchical lookups\n\n### Providing and Accessing Context\n\nThe core workflow with nctx involves:\n\n1. **Creating** a context\n2. **Providing** the context for an async operation\n3. **Setting** values in the context\n4. **Getting** values from the context within the async tree\n\n## Basic Usage\n\n### JavaScript Example\n\n```javascript\n// CommonJS\nconst nctx = require('nctx');\n\n// Create a context\nconst myContext = nctx.create(Symbol('myContext'));\n\nasync function main() {\n  // Provide a context for the async operation\n  await myContext.provide(async () =\u003e {\n    // Set a value in the context\n    myContext.set('message', 'Hello, World!');\n    \n    // The value is available anywhere in this async tree\n    await someAsyncOperation();\n  });\n}\n\nasync function someAsyncOperation() {\n  // Get the value from the context\n  const message = myContext.get('message');\n  console.log(message); // Outputs: Hello, World!\n}\n\nmain().catch(console.error);\n```\n\n### TypeScript Example\n\n```typescript\nimport nctx from 'nctx';\nimport { Context } from 'nctx';\n\n// Create a context with type annotation\nconst myContext: Context = nctx.create(Symbol('myContext'));\n\n// Define an interface for your context data (optional but recommended)\ninterface AppContext {\n  message: string;\n  count: number;\n  user?: {\n    id: string;\n    role: 'admin' | 'user';\n  };\n}\n\nasync function main(): Promise\u003cvoid\u003e {\n  await myContext.provide(async () =\u003e {\n    // Set values with proper types\n    myContext.set('message', 'Hello, TypeScript!');\n    myContext.set('count', 42);\n    myContext.set('user', {\n      id: 'user-123',\n      role: 'admin' as const\n    });\n    \n    await someAsyncOperation();\n  });\n}\n\nasync function someAsyncOperation(): Promise\u003cvoid\u003e {\n  // Get values with type assertions\n  const message = myContext.get('message') as string;\n  const count = myContext.get('count') as number;\n  const user = myContext.get('user') as AppContext['user'];\n  \n  console.log(message); // Hello, TypeScript!\n  console.log(`Count: ${count}`); // Count: 42\n  \n  if (user) {\n    console.log(`User: ${user.id}, Role: ${user.role}`);\n  }\n}\n\nmain().catch((error: Error) =\u003e {\n  console.error('Error:', error.message);\n});\n```\n\n## Common Use Cases\n\n### Forking Contexts\n\nForking allows you to create isolated copies of a context, which is particularly useful for handling concurrent operations where each needs its own context values.\n\n#### Why Fork Contexts?\n\n- Run parallel operations with different context values\n- Isolate changes to prevent them from affecting the parent context\n- Create temporary context modifications\n\n#### Simple Forking Example\n\n```javascript\nconst nctx = require('nctx');\n\nconst userContext = nctx.create(Symbol('userContext'));\n\nasync function processUsers(users) {\n  await userContext.provide(async () =\u003e {\n    // Set a default value\n    userContext.set('role', 'guest');\n    \n    // Process each user in parallel with isolated contexts\n    const results = await Promise.all(\n      users.map(user =\u003e \n        // Fork the context for each parallel operation\n        nctx.fork([userContext], async () =\u003e {\n          // This change only affects this forked context\n          userContext.set('userId', user.id);\n          userContext.set('role', user.role);\n          \n          return processUserData(user);\n        })\n      )\n    );\n    \n    // Here, userContext still has role='guest' and no userId\n    console.log(userContext.get('role')); // 'guest'\n    \n    return results;\n  });\n}\n\nasync function processUserData(user) {\n  // Access the forked context values\n  const userId = userContext.get('userId');\n  const role = userContext.get('role');\n  \n  console.log(`Processing user ${userId} with role ${role}`);\n  // ... processing logic\n}\n```\n\n#### Deep vs. Shallow Forking\n\nnctx supports two forking modes:\n\n```javascript\n// Shallow fork (default) - Object references are shared\nnctx.fork([myContext], () =\u003e { /* ... */ });\n\n// Deep fork - Creates deep copies of objects\nnctx.fork([myContext], () =\u003e { /* ... */ }, true);\n```\n\nWith shallow forking (the default), object references are shared between the parent and forked context. With deep forking, objects are deeply cloned, allowing you to modify nested properties without affecting the parent context.\n\n### Express Integration\n\nnctx is particularly useful in web applications where you need to maintain request-scoped data. Here's how to integrate it with Express:\n\n```javascript\n// ctx/req.js\nconst nctx = require('nctx');\n\nconst reqCtx = nctx.create(Symbol('req'));\n\n// Create middleware to establish the request context\nreqCtx.createAppMiddleware = () =\u003e {\n  return (req, res, next) =\u003e {\n    reqCtx.provide(() =\u003e {\n      // Share the context with the request object\n      reqCtx.share(req);\n      \n      // Clean up when the response is finished\n      res.on('finish', () =\u003e {\n        reqCtx.endShare(req);\n      });\n      \n      // Store the request object in the context\n      reqCtx.set('req', req);\n      next();\n    });\n  };\n};\n\n// Middleware for routers to ensure they have access to the context\nreqCtx.createRouterMiddleware = () =\u003e {\n  return (req, _res, next) =\u003e {\n    reqCtx.share(req);\n    if (next) {\n      next();\n    }\n  };\n};\n\nmodule.exports = reqCtx;\n```\n\n```javascript\n// app.js\nconst express = require('express');\nconst reqCtx = require('./ctx/req');\n\nconst app = express();\n\n// Apply the context middleware\napp.use(reqCtx.createAppMiddleware());\n\n// Add request-specific data to the context\napp.use(async (req, _res, next) =\u003e {\n  const logger = createLogger().child({ requestId: req.id, path: req.path });\n  reqCtx.set('logger', logger);\n  \n  // You could also add user info after authentication\n  // reqCtx.set('user', req.user);\n  \n  next();\n});\n\nconst router = express.Router();\nrouter.use(reqCtx.createRouterMiddleware());\napp.use(router);\n\n// Now you can access the context anywhere in your route handlers\nrouter.get('/api/data', async (req, res) =\u003e {\n  // Get the request-scoped logger\n  const logger = reqCtx.get('logger');\n  logger.info('Processing request');\n  \n  // Business logic...\n  const data = await fetchData();\n  \n  res.json(data);\n});\n\n// Even in deeply nested service functions\nasync function fetchData() {\n  const logger = reqCtx.get('logger');\n  logger.debug('Fetching data');\n  \n  // The logger is specific to the current request\n  return { /* ... */ };\n}\n\napp.listen(3000);\n```\n\n### Logging and Error Handling\n\nnctx makes it easy to implement consistent logging with request-specific information:\n\n```javascript\n// Create a logger context\nconst loggerCtx = nctx.create(Symbol('logger'));\n\n// Middleware to set up the logger\nfunction loggerMiddleware(req, res, next) {\n  loggerCtx.provide(() =\u003e {\n    const requestId = generateRequestId();\n    \n    // Create a request-specific logger\n    const logger = createBaseLogger().child({\n      requestId,\n      path: req.path,\n      method: req.method\n    });\n    \n    // Store in context\n    loggerCtx.set('logger', logger);\n    loggerCtx.set('requestId', requestId);\n    \n    // Add requestId to response headers\n    res.setHeader('X-Request-ID', requestId);\n    \n    // Log the request\n    logger.info(`Received ${req.method} request to ${req.path}`);\n    \n    // Track timing\n    const startTime = Date.now();\n    res.on('finish', () =\u003e {\n      const duration = Date.now() - startTime;\n      logger.info(`Request completed in ${duration}ms with status ${res.statusCode}`);\n    });\n    \n    next();\n  });\n}\n\n// Now you can access the logger anywhere\nfunction businessLogic() {\n  const logger = loggerCtx.get('logger');\n  logger.debug('Executing business logic');\n  \n  try {\n    // ... logic\n  } catch (error) {\n    // Log with request context already included\n    logger.error('Error in business logic', { error: error.message });\n    throw error;\n  }\n}\n```\n\n## API Reference\n\n### Context Creation\n\n```javascript\n// Create a new context\nconst myContext = nctx.create(Symbol('myContext'));\n```\n\n### Context Methods\n\n| Method | Description | Example |\n|--------|-------------|---------|\n| `provide(callback, ref?, syncFollowers?, forceOverride?)` | Establishes a context for the async operation | `myContext.provide(() =\u003e { /* async operations */ })` |\n| `get(key)` | Retrieves a value from the context | `const value = myContext.get('key')` |\n| `set(key, value)` | Sets a value in the context | `myContext.set('key', 'value')` |\n| `require(key, strict?)` | Gets a value, throws if not found | `const value = myContext.require('key')` |\n| `fork(callback, deepFork?, syncFollowers?)` | Creates an isolated copy of the context | `myContext.fork(() =\u003e { /* operations with isolated context */ })` |\n| `isProvided()` | Checks if the context is provided | `if (myContext.isProvided()) { /* ... */ }` |\n| `share(ref)` | Shares context with a reference | `myContext.share(req)` |\n| `endShare(ref)` | Ends context sharing | `myContext.endShare(req)` |\n| `follow(ctx)` | Makes this context follow another | `myContext.follow(otherContext)` |\n| `unfollow(ctx)` | Stops following another context | `myContext.unfollow(otherContext)` |\n| `fallback(ctx)` | Sets a fallback context | `myContext.fallback(defaultContext)` |\n| `merge(...params)` | Merges values into the context | `myContext.merge({ key1: 'value1', key2: 'value2' })` |\n| `assign(obj)` | Assigns an object to the context | `myContext.assign({ key1: 'value1', key2: 'value2' })` |\n| `replace(key, callback)` | Updates a value using a callback | `myContext.replace('counter', count =\u003e count + 1)` |\n\n### Static Methods\n\n| Method | Description | Example |\n|--------|-------------|---------|\n| `nctx.create(name?)` | Creates a new context | `const ctx = nctx.create(Symbol('name'))` |\n| `nctx.provide(ctxArr, callback, ref?, syncFollowers?, forceOverride?)` | Provides multiple contexts | `nctx.provide([ctx1, ctx2], () =\u003e { /* ... */ })` |\n| `nctx.fork(ctxArr, callback, deepFork?, syncFollowers?)` | Forks multiple contexts | `nctx.fork([ctx1, ctx2], () =\u003e { /* ... */ })` |\n\n## Advanced Usage\n\n### Context Relationships\n\nnctx allows you to establish relationships between contexts:\n\n#### Following Contexts\n\nWhen context A follows context B, operations on B will also affect A:\n\n```javascript\nconst contextA = nctx.create(Symbol('A'));\nconst contextB = nctx.create(Symbol('B'));\n\n// Make A follow B\ncontextA.follow(contextB);\n\n// Now when you provide B, A is also provided\ncontextB.provide(() =\u003e {\n  contextB.set('key', 'value');\n  \n  // A can access the value\n  console.log(contextA.get('key')); // 'value'\n});\n```\n\n#### Fallback Contexts\n\nYou can set a fallback context to use when a key isn't found:\n\n```javascript\nconst mainContext = nctx.create(Symbol('main'));\nconst defaultContext = nctx.create(Symbol('default'));\n\n// Set up the default context\ndefaultContext.provide(() =\u003e {\n  defaultContext.set('theme', 'dark');\n  defaultContext.set('language', 'en');\n  \n  // Set main context to fall back to default\n  mainContext.fallback(defaultContext);\n  \n  mainContext.provide(() =\u003e {\n    // Override just one setting\n    mainContext.set('theme', 'light');\n    \n    // This comes from main context\n    console.log(mainContext.get('theme')); // 'light'\n    \n    // This falls back to default context\n    console.log(mainContext.get('language')); // 'en'\n  });\n});\n```\n\n### Sharing Contexts\n\nThe `share` method allows you to associate a context with a reference (like a request object):\n\n```javascript\nconst reqCtx = nctx.create(Symbol('req'));\n\nfunction middleware(req, res, next) {\n  reqCtx.provide(() =\u003e {\n    // Associate this context with the request\n    reqCtx.share(req);\n    \n    // Later, in another middleware or route handler\n    // that has the same req object:\n    reqCtx.share(req); // This will reuse the same context\n    \n    // Clean up when done\n    res.on('finish', () =\u003e {\n      reqCtx.endShare(req);\n    });\n    \n    next();\n  });\n}\n```\n\n### Extending Contexts\n\nYou can extend contexts with custom getter and setter methods to create a more intuitive and type-safe API:\n\n#### JavaScript Example\n\n```javascript\nconst nctx = require('nctx');\n\n// Create a base context\nconst appContext = nctx.create(Symbol('app'));\n\n// Extend the context with custom getters\nappContext.getLogger = function() {\n  return this.get('logger');\n};\n\nappContext.getConfig = function() {\n  return this.get('config');\n};\n\n// Add custom setters\nappContext.setLogger = function(logger) {\n  this.set('logger', logger);\n  return this; // For method chaining\n};\n\nappContext.setConfig = function(config) {\n  this.set('config', config);\n  return this; // For method chaining\n};\n\n// Usage\nappContext.provide(() =\u003e {\n  // Use the setter\n  appContext.setLogger(createLogger());\n  \n  // Use the getter\n  const logger = appContext.getLogger();\n  logger.info('Application started');\n  \n  // Method chaining\n  appContext\n    .setConfig({ env: 'production' })\n    .set('version', '1.0.0');\n});\n```\n\n#### TypeScript Example\n\n```typescript\nimport nctx from 'nctx';\nimport { Context } from 'nctx';\nimport { Logger } from 'your-logger-library';\n\n// Define extended context interface\ninterface AppContext extends Context {\n  // Getters\n  getLogger(): Logger;\n  getConfig(): Record\u003cstring, any\u003e;\n  \n  // Setters\n  setLogger(logger: Logger): this;\n  setConfig(config: Record\u003cstring, any\u003e): this;\n}\n\n// Create and extend the context\nconst baseContext = nctx.create(Symbol('app'));\nconst appContext = baseContext as AppContext;\n\n// Implement the getters\nappContext.getLogger = function(this: AppContext): Logger {\n  return this.get('logger') as Logger;\n};\n\nappContext.getConfig = function(this: AppContext): Record\u003cstring, any\u003e {\n  return this.get('config') as Record\u003cstring, any\u003e;\n};\n\n// Implement the setters\nappContext.setLogger = function(this: AppContext, logger: Logger): AppContext {\n  this.set('logger', logger);\n  return this;\n};\n\nappContext.setConfig = function(this: AppContext, config: Record\u003cstring, any\u003e): AppContext {\n  this.set('config', config);\n  return this;\n};\n\n// Usage with proper typing\nappContext.provide(() =\u003e {\n  // Use the setter\n  appContext.setLogger(createLogger());\n  \n  // Use the getter with proper type\n  const logger: Logger = appContext.getLogger();\n  logger.info('Application started with typed logger');\n  \n  // Method chaining with type safety\n  appContext\n    .setConfig({ env: 'production' })\n    .set('version', '1.0.0');\n});\n```\n\nThis approach provides several benefits:\n1. Type safety for both getting and setting values\n2. Method chaining for a more fluent API\n3. Better encapsulation of the underlying implementation\n4. Improved developer experience with IDE autocompletion\n\n## Best Practices\n\n### Do's\n\n- **Use symbols for context names** to avoid naming collisions\n- **Clean up shared contexts** when they're no longer needed\n- **Use TypeScript interfaces** to define your context structure\n- **Keep contexts focused** on specific concerns (e.g., request context, user context)\n- **Use `require()` instead of `get()`** when a value must be present\n\n### Don'ts\n\n- **Don't forget to provide a context** before using it\n- **Don't rely on context outside of its async tree** without explicit sharing\n\n\n## Running the Examples\n\nThe package includes examples for both CommonJS and TypeScript usage:\n\n```sh\n# Run the CommonJS example\nnpm run example:js\n\n# Run the TypeScript example (requires ts-node)\nnpm run example:ts\n\n# Check TypeScript types (verify that the types compile correctly)\nnpm run check-types\n```\n\n## Related Libraries\n\n- [node-simple-context](https://github.com/maxgfr/node-simple-context) by [@maxgfr](https://github.com/maxgfr)\n\n## Contributing\n\nWe welcome contributions! If you encounter a bug or have a feature suggestion, please open an issue. To contribute code, simply fork the repository and submit a pull request.\n\nThis repository is mirrored on both GitHub and Codeberg. Contributions can be made on either platform, as the repositories are synchronized bidirectionally. \n- Codeberg: [https://codeberg.org/devthefuture/nctx](https://codeberg.org/devthefuture/nctx)\n- GitHub: [https://github.com/devthefuture-org/nctx](https://github.com/devthefuture-org/nctx)\n\nFor more information:\n- [Why mirror to Codeberg?](https://codeberg.org/Recommendations/Mirror_to_Codeberg#why-should-we-mirror-to-codeberg)\n- [GitHub to Codeberg mirroring tutorial](https://codeberg.org/Recommendations/Mirror_to_Codeberg#github-codeberg-mirroring-tutorial)\n","funding_links":[],"categories":["JavaScript"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdevthefuture-org%2Fnctx","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdevthefuture-org%2Fnctx","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdevthefuture-org%2Fnctx/lists"}