Resolving Dependencies Once per HTTP Request with InversifyJS Scoped Containers
Learn how to resolve dependencies once per HTTP request with InversifyJS, while still maintaining application-wide singleton dependencies.
Hey there! Do you need to resolve dependencies once per HTTP request, but still want to have some application-wide singleton dependencies? In this post, I'll show you how to use InversifyJS to achieve this.
You may have heard of InversifyJS's inRequestScope()
function, but did you know that it doesn't work for the container-per-HTTP-request case? Well, fear not, because I've got a solution for you!
I'll walk you through the steps to create a custom ScopedContainer
class that allows you to resolve dependencies once per HTTP request, with the option to still have some application-wide singleton dependencies.
⚡ Using the code
Before we dive into writing the ScopedContainer
class, let's take a look at how it will be used in the context of optimizing HTTP requests with efficient dependency injection in InversifyJS. This will help you better evaluate if this solution meets your needs.
Registering application-wide singletons
First, it is crucial to keep the ability to register application-wide singletons, and you can do so in your dependency registration file. With the following code template, you can make sure that your singletons are always available to all containers that use the ScopedContainer
class.
ScopedContainer.registerGlobalDependencies((container) => {
container
.bind<SomeSingletonDep>(TOKENS.SomeSingletonDep)
.to(SomeSingletonDep)
.inSingletonScope();
})();
Registering scoped dependencies
Next, we need a way to register dependencies that will only be created once per HTTP request. With the Scoped Container, you still register these dependencies as singletons, but the Scoped Container ensures that they are scoped to the specific request.
This means that every time the Scoped Container is used to resolve a dependency, it will only create a single instance of that dependency for the current HTTP request or whatever scope you've defined.
ScopedContainer.registerScopedDependencies((container) => {
container
.bind<RequestSpecificDep>(TOKENS.RequestSpecificDep)
.to(RequestSpecificDep)
.inSingletonScope(); // this is right; use the singleton scope
});
So, keep in mind that as long as you register your dependencies inside the registerScopedDependencies
function using the inSingletonScope
method, they will be tied to the specific HTTP request. This is because the ScopedContainer class ensures that these dependencies are only created once per request, even though they're registered as singletons.
Resolving dependencies in your HTTP request
// lambda-handler.ts
import "register-scoped-dependencies";
handler = (event, context) => {
const requestId = event.requestContext.requestId;
const container = ScopedContainer.for(requestId);
try {
// This will be the same for every request
const singletonDep = container.get(TOKENS.SomeSingletonDep);
// And this will be a new instance for every request
const requestSpecificDep = container.get(TOKENS.RequestSpecificDep);
}
finally {
ScopedContainer.remove(requestId);
}
}
This is an example usage in a lambda handler function.
TL;DR:
Create a new Scoped Container using the request ID
Resolve dependencies
Remove the Scoped Container for the request ID
In the beginning of the request, we create a new container for each HTTP request using the ScopedContainer.for
method, passing in a unique scope ID for that request. This allows you to efficiently manage dependencies that are specific to each request.
Inside the try
block, you can see two examples of how to resolve dependencies. The singletonDep
is registered as a singleton in the global container and will be the same for every request. The requestSpecificDep
is registered using the inSingletonScope
method and will be a new instance for every request, but it is guaranteed to be the same instance for the entire request.
With the finally
block, we remove the container for that specific scope ID, so that it can be garbage collected and does not interfere with any future requests.
✨ Creating the ScopedContainer
class
The ScopedContainer
class provides a way to create and manage separate containers for each scope ID, allowing you to resolve dependencies once per scope ID.
This is particularly useful in the context of HTTP requests, where you can use the Scoped Container to ensure that each request has its own container, allowing you to efficiently manage dependencies that are specific to each request.
import { Container, interfaces } from "inversify";
const DEFAULT_SCOPE_ID = "__default__";
type ScopedContainerCache = {
[id: string]: Container;
};
export type RegisterScopedDependenciesAction = (container: Container) => void;
export class ScopedContainer {
private static _dependencyRegistrations: RegisterScopedDependenciesAction[] = [];
private static _globalContainer: Container;
private static readonly _containerInstances: ScopedContainerCache = {};
/**
* Options object to use when creating a new container for a
* scope ID.
*/
static scopedContainerOptions: interfaces.ContainerOptions;
/**
* A function to register global dependencies.
* This creates a global container instance, which enables truly
* singleton instances when using a scoped container. All scoped
* containers reference the global container as parent.
*
* @example
* ScopedContainer.registerGlobalDependencies((container) => {
* container
* .bind<SomeSingletonDep>(TOKENS.SomeSingletonDep)
* .to(SomeSingletonDep)
* .inSingletonScope();
* })();
*/
static registerGlobalDependencies: (container: Container) => void;
/**
* Returns a @see Container that is unique to the specified scope.
* If this is the first time getting the container for the scope, then a
* new container will be created using the provided factory. Any post configure
* actions will also be applied to the new container instance.
* @param scopeId Any string to identify the scope (e.g. current request ID).
* @returns A @see Container that is unique to the specified scope.
*/
static for(scopeId = DEFAULT_SCOPE_ID): Container {
let container = this._containerInstances[scopeId];
if (!container) {
container = this.makeNewContainer();
this._containerInstances[scopeId] = container;
}
return container;
}
/**
* Registers dependencies that should be tied to a scope,
* e.g. HTTP request.
* @remarks
* ✔️ DO use `inSingletonScope` to register scoped dependencies.
* ❌ DO NOT use `inRequestScope`, as this will create a new instance for
* separate calls to `get` even within the same scope.
* @see https://stackoverflow.com/a/71180025
* @param fn A function that registers scoped dependencies.
* @returns The @see ScopedContainer itself, to allow chaining.
* @example
* ScopedContainer.registerScopedDependencies((container) => {
* container
* .bind<SomeScopedDep>(TOKENS.SomeScopedDep)
* .to(SomeScopedDep)
* .inSingletonScope(); // this is right; use the singleton scope
* })();
*/
static registerScopedDependencies(fn: RegisterScopedDependenciesAction): ScopedContainer {
this._dependencyRegistrations.push(fn);
return this;
}
private static makeNewContainer(): Container {
const container = this.ensureGlobalContainer().createChild(this.scopedContainerOptions);
this._dependencyRegistrations.forEach((action) => action(container));
return container;
}
private static ensureGlobalContainer(): Container {
if (!this._globalContainer) {
const container = new Container(this.scopedContainerOptions);
this.registerGlobalDependencies?.(container);
this._globalContainer = container;
}
return this._globalContainer;
}
}
Just to give you the full story, the inRequestScope()
is no good in for the container-per-http-request case because InversifyJS's request scope is actually tied to a single call to get
, that is, each call to get
is considered a Request (aka Composition Root), and it will only work as intended for an HTTP context if you only have a single call to get per request.
Conclusion
With the ScopedContainer
class, you can now resolve dependencies once per request, ensuring that each request has its own container with dependencies specific to that request, while still allowing for application-wide singleton dependencies.
I hope this post has helped you better understand how to use the ScopedContainer class and apply it to your own projects.
If you have any questions or comments, please let me know below. Happy coding!