This article was published on Friday, January 11, 2019 by Arda Tanrikulu @ The Guild Blog
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.## EncapsulationWhile 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.## HierarchyWe 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 CFor 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 ProvidersIn 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 InversifyTo 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 asparent
in that feature. But, we need is more than that. We want to have multipleparent
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 DIIn 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. ```typescript 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.
```typescript
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'sloadChildren
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 untilwindow
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 ImplementationNestJS 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 plaingraphqljs
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 TogetherWe 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
Top comments (0)