Dependency Injection in GraphQL-Modules

Arda Tanrikulu

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

For 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-providers

Comparisons 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

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.