Dependency Injection in GraphQL-Modules
Why Not Inversify / Angular / NestJS?
When designing GraphQL Modules, we tried different approaches to encapsulate modules safely. We created a solution where you won’t need to use Dependency Injection when you start, but after you are getting to a certain scale, you can make use of our separate DI library to ease the separation and make the boundaries stricter, only at the point when it really makes sense and helps you. When you get to that point, DI helps you mostly for ease of mocking providers for testing and encapsulation for less error-prone implementation.
In the early stages of GraphQL-Modules, it used to have Inversify internally for Dependency Injection in GraphQL-Modules. Inversify is a platform-agnostic Dependency Injection library written in JavaScript. Unfortunately, Inversify didn’t fit our needs for modular DI (Dependency Injection) due to some critical reasons that we’ll expand on this article.
That’s why, we implemented our own platform-agnostic Dependency Injection library called
@graphql-modules/di
which is independent from @graphql-modules/core
, and it can be used by
itself.
It supports factory, class and value providers that can have Symbol
, string
, number
,
function
and object
tokens in a provide
definition, and it can address constructable classes,
factory functions and constant values as injected values. It has some features that are similar to
Angular’s Dependency Injection that is mentioned in
their documentation. In this article we’ll explain the similarities and differences between those
solutions.
Let’s start with the principles we wanted to have in our DI logic, and we will finish with the comparisons with Angular and NestJS.
Encapsulation
While we were still using Inversify, we used to have a single GraphQLApp
top module. Regular
GraphQL Modules cannot import each other, but can be imported by that GraphQLApp
object. Also
these modules didn’t have an encapsulation, because all the providers, schemas and
resolvers were getting concatenated without any encapsulation. After that, we decided to
provide complete Modular approach, and make everything module. Now Each module has its own injector,
valid schema and context which is independent from its parent modules. Also, each module is
constructed by using the imported modules’ DI containers, schema contents and context builders.
If you want to know more about encapsulation see the blog post about it;
/blog/modular-encapsulation-graphql-modulesFor example, if you have a DatabaseModule in your application that has to be shared across the whole application, and it needs to take a custom provider that will be defined by the user. What you will do in those DI implementations is to create a provider by decorating it with a global scoped provider; then put it in ApplicationModule. However, this would violate the encapsulation principle of the modular approach.
To summarize; while your DatabaseModule is imported by other modules in the lower level and that module will use a provider in its parent. It mustn’t know what imports it.
We handle this in a different way in GraphQL Modules by passing configuration by obeying the encapsulation rule;
const AppModule = new GraphQLModule({
imports: ({ config: { connectionStuff }}) => { // getting the configuration in schema and DI container generation phase
DatabaseModule.forRoot({ // Define this configured module as the default instance inside AppModule
connectionStuff
}),
OtherModuleThatUsesDB,
},
configRequired: true, // makes schema and DI container prevent to be requested without a valid configuration
})
const DatabaseModule = new GraphQLModule({
providers: [SomeDbProvider],
configRequired: true, // makes this module prevent to be used without a valid configuration in any part of the application
});
@Injectable()
export class SomeDbProvider {
constructor(@ModuleConfig() config) { // get configuration into the provider
// some logic with the configuration
}
}
const OtherModuleThatUsesDB = new GraphQLModule({
imports: [
DatabaseModule.forChild() // Use the configured DatabaseModule in the higher level, prevent this module to be imported without unconfigured DatabaseModule
]
})
The three benefits we have with this type of DI and modular system;
- AppModule is protected from an unsafe call without a valid configuration that is needed in the
internal process (DatabaseModule); thanks to
configRequired
. - DatabaseModule is protected from an unsafe import without a valid configuration that is needed
in the internal process (SomeDbProvider); thanks to
configRequired
again! - OtherModuleThatUsesDb is protected from an unsafe call or import without a definition of a well-configured DatabaseModule.
Hierarchy
We also have another problem about the hierarchical approach of existing DI implementations. Let me give an example;
- A DI Container has FooProvider
- B DI Container has BarProvider
- C DI Container has BazProvider
- D DI Container has QuxProvider
- A imports B
- B imports D
- B imports C
For example the following case will happen in our DI;
- FooProvider cannot be injected inside B while BarProvider can be injected by A; because the injector of B only have its providers and D’s and C’s (BazProvider and QuxProvider).
As you can see in the comparison with Inversify, Inversify can attach only one child DI container in a parent DI container; and this wouldn’t fit our hierarchy-based modular approach. For example, you cannot make B extend both D and C by keeping D and C encapsulated. If you merge all of them into the one injector like Angular does, D can access C’s providers without importing C.
Scoped Providers
In contrast with client side application, your DI system wouldn’t need to have different scopes like Application Scope and Session Scope. In our implementation, we can define providers that have a lifetime during a single GraphQL Network Request and not the whole the application runtime; because some providers needs to belong in a single client. Unfortunately, you need to find tricky ways to manage scoped providers if you use existing DI libraries like Inversify.
In server-side applications, every client request has its own scope that we call `session scope`. We think this session scope should be able to have its own providers and DI logic outside the application scope.
You can read more about the different scopes that we have in GraphQL-Modules here:
/blog/graphql-modules-scoped-providersComparisons with Other DI Implementations of Frameworks
Comparison with Inversify
To provide true encapsulation; we should have an injector/container with multiple children,
and these children must not know each other and even its parent. In Inversify, they have
something like that which is called Hierarchical DI. In this feature of Inversify,
theparent
term can be misunderstood. because an injector seeks for its own providers then looks
for the other injector that is being defined as parent
in that feature. But, we need is more than
that. We want to have multiple parent
according to their logic.
Every module has its own injector together with its children’s injectors. So, every module can interact with its children’s providers with its own injector.
You can read more about Hierarchy and Encapsulation in the beginning of this article.
Comparison with Angular’s DI
In Angular, there is one injector that is shared across all the application while there are different encapsulated injectors belonging to each module in different scope.
Our Dependency Injection implementation is more strict in terms of encapsulation than Angular’s.
import { NgModule } from '@angular/core'
import { BarModule } from '@bar'
import { FooModule, FooProvider } from '@foo'
@NgModule({
imports: [FooModule, BarModule],
providers: [
{
provide: FooProvider,
useClass: MyFooProvider
}
]
})
export class AppModule {}
This is an example top module written in Angular. Let’s assume FooProvider
originally implemented
and defined inside FooModule
and used by BarModule
. When FooProvider
token is replaced by
MyFooProvider
in AppModule
scope, BarModule
also uses our MyFooProvider
inside its logic.
This explicitly violates encapsulation in the modular approach, because Angular is supposed to send
to BarModule
FooModule
‘s FooProvider
implementation instead of the one defined outside of
BarModule
; because it shouldn’t know what is defined in the higher level.
import { BarModule } from '@bar'
import { FooModule, FooProvider } from '@foo'
import { GraphQLModule } from '@graphql-modules/core'
export const AppModule = new GraphQLModule({
imports: [FooModule, BarModule],
providers: [
{
provide: FooProvider,
useClass: MyFooProvider
}
]
})
However, GraphQL-Modules will send to BarModule
the correct FooProvider
, because BarModule
imports FooModule
not AppModule
. So, MyFooProvider
will be sent if the current injector is
inside AppModule
scope.
In GraphQL-Modules, if A imports B and B imports C, A can access C’s scope. But, C cannot access the scopes of B and A. This allows us to create a safely built GraphQL Application and reduces the error prone. You can read more about Hierarchy and Encapsulation in the beginning of this article.
Angular doesn’t care about that. it has a single injector for the all application. This may cause some problems for debugging on a large scale backend application.
That ability is not so important for Angular applications; because they are running on the browser, not on the server. But still you can notice the similar behavior to GraphQL-Modules DI’s if you load an Angular module lazily using router’s
loadChildren
property.
And Angular’s DI library doesn’t differentiate Application and Session scopes because it doesn’t
need that. An Angular application has a lifetime until window
is terminated by closing it or
refreshing page; the session and application runtime can be considered same in the client
application. That’s why, our DI implementation is more complex than Angular’s as it need to handle
multiple clients by running a single application. You can read more about
Scoped Providers in the beginning of this article
Comparison with NestJS’ DI Implementation
NestJS is a server-side model-view-controller full-fledged framework that has all the principles of MVC Backend framework. exposing a GraphQL API is only one of its various features. Our goal with GraphQL Modules was to create a set of tools and libraries that answer needs for the GraphQL ecosystem, and can be (and should be) used by NestJS users as well.
The principles and approaches we’re trying to apply in GraphQL Modules is not only for Dependency Injection but also for GraphQL Schemas and Context.
In comparison to Nest’s goals, GraphQL-Modules is a platform-agnostic library that can be used even
with the plain graphqljs
package without a server; because it was designed to work exactly
same in all GraphQL Server platforms such as Apollo, Yoga, graphql-express etc. For example, you can
use Apollo’s data sources with graphql-express; because GraphQL Modules passes its own cache
mechanism into the dataloaders thanks to our independent DI logic.
The result of a GraphQLModule is a context builder and a schema while NestJS’ GraphQLModule is a NestJS module that exposes an API using Apollo.
NestJS has its own dependency injection system which is similar to Angular’s but more strict; you can define both global (not by default like Angular) and encapsulated providers at the same time. We preferred not to have global providers like Angular (you can see how it works above in the Angular comparison); because we consider it can be more error-prone, and it has to be done by passing configuration. You can read more with an example about passing configuration and keeping safe boundaries between your modules in the Encapsulation article.
Also, NestJS DI system doesn’t share the important feature of defining providers that have the life-time of the client request (we call it session scope). We think a backend system would need to differentiate DI containers according to these different scopes. You can read more about it in Scoped Providers part of this article above.
Working Together
We wanted to share the principles behind our implementation in order to get more background to our community and get feedback from other library authors.
That is also one of the reasons we created our DI library as a separate library, not only so it would be optional to our users, but also to potentially share that implementation with other libraries like NestJS and Inversify for example.
All Posts about GraphQL Modules
- GraphQL Modules — Feature based GraphQL Modules at scale
- Why is True Modular Encapsulation So Important in Large-Scale GraphQL Projects?
- Why did we implement our own Dependency Injection library for GraphQL-Modules?
- Scoped Providers in GraphQL-Modules Dependency Injection
- Writing a GraphQL TypeScript project w/ GraphQL-Modules and GraphQL-Code-Generator
- Authentication and Authorization in GraphQL (and how GraphQL-Modules can help)
- Authentication with AccountsJS & GraphQL Modules
- Manage Circular Imports Hell with GraphQL-Modules
Join our newsletter
Want to hear from us when there's something new?
Sign up and stay up to date!
*By subscribing, you agree with Beehiiv’s Terms of Service and Privacy Policy.