Scoped Providers in GraphQL-Modules

Arda Tanrikulu

We recently released a new version of GraphQL-Modules with a new feature called Scoped Providers in the dependency injection system of GraphQL-Modules.

Dependency injection in GraphQL-Modules is optional, and you can use it according to your preference. It provides a solution for writing Providers, which are just classes, you can use to write your business-logic, and this way to separate it from your API declaration, reuse it, and communicate between modules easily.

This new feature allows you to define Providers to be used in different scopes;

Application Scope

If you define a provider in this scope which is default, the provider will be instantiated on application-start and will be same in the entire application and all the following requests. The providers in this scope can be considered as a shared state across all users’ interactions with our application. It’s basically means that the instance will be treated as Singleton.

For example, you have a provider called ExampleApplicationProvider , then this provider has a counter in it;

import { Injectable, ProviderScope } from '@graphql-modules/di'
 
@Injectable({
  scope: ProviderScope.Application
})
export class ExampleApplicationProvider {
  counter = 0
  increment() {
    counter++
    return counter
  }
}

And let’s assume that we have a module declaration something like below;

import { GraphQLModule } from '@graphql-modules/core'
import { ExampleApplicationProvider } from './ExampleApplicationProvider'
 
export const ExampleModule = new GraphQLModule({
  providers: [ExampleApplicationProvider],
  typeDefs: `
   type Mutation {
    increment: Int
   }
 `,
  resolvers: {
    Mutation: {
      increment: (root, args, context) =>
        context.injector.get(ExampleApplicationProvider).increment()
    }
  }
})

Finally, let’s try to call the following GraphQL Request in multiple times;

mutation ExampleMutation {
  increment
}

In first call, you will get 1 , but in the second call you will get 2 . And in every request this value will be incremented; because Application-Scoped Providers are kept in memory until the application is terminated; and the same instance of that provider will be used on each request. Let’s see other types of providers to understand this one better.

Session Scope

When a network request is arriving to your GraphQL-Server, GraphQL-Server calls the context factory of the parent module. The parent module creates a session injector together with instantiating session-scoped providers with that session object which contains the current context, session injector and network request. This session object is passed through module’s resolvers using module’s context.

In other words, providers defined in the session scope are constructed in the beginning of the network request, then kept until the network request is closed. While application-scoped providers is kept during the application runtime, and shared between all the following network requests and resolvers inside these requests, this type of providers would not be shared between different requests but in resolver calls those belong to same network request.

Let’s try the same thing in that scope by creating a new provider called ExampleSessionProvider .

import { Injectable, ProviderScope } from '@graphql-modules/di'
 
@Injectable({
  scope: ProviderScope.Session
})
export class ExampleSessionProvider {
  counter = 0
  increment() {
    counter++
    return counter
  }
}

Then change our module declaration to use this provider;

import { GraphQLModule } from '@graphql-modules/core'
import { ExampleSessionProvider } from './ExampleApplicationProvider'
 
export const ExampleModule = new GraphQLModule({
  providers: [ExampleSessionProvider],
  typeDefs: `
   type Mutation {
    increment: Int
   }
 `,
  resolvers: {
    Mutation: {
      increment: (root, args, context) => context.injector.get(ExampleSessionProvider).increment()
    }
  }
})

So at this time, in every mutation call our Session-Scoped Provider’s increment method will be called and its value will be returned as a result of that mutation.

mutation ExampleMutation {
  increment
}

In first call, you will get 1 , but in the second call you will get 1 again. But why? Because on each request ExampleSessionProvider is instantiated from scratch specifically for that network request which is called session in our DI system. But to see the point of the session scope, let’s assume we have two more resolvers called multiply . First let’s add another method called multiply that takes one argument to be multiplied by our counter value;

import { Injectable, ProviderScope } from '@graphql-modules/di'
 
@Injectable({
  scope: ProviderScope.Session
})
export class ExampleSessionProvider {
  counter = 0
  increment() {
    counter++
    return counter
  }
  multiply(times: number) {
    counter = counter * times
    return counter
  }
}

After that, let’s use this new method in our new resolver;

import { GraphQLModule } from '@graphql-modules/core'
import { ExampleApplicationProvider } from './ExampleApplicationProvider'
 
export const ExampleModule = new GraphQLModule({
  providers: [ExampleApplicationProvider],
  typeDefs: `
   type Mutation {
    increment: Int
    multiply(times: Int): Int
   }
 `,
  resolvers: {
    Mutation: {
      increment: (root, args, context) =>
        context.injector.get(ExampleApplicationProvider).increment(),
      multiply: (root, args, context) =>
        context.injector.get(ExampleApplicationProvider).multiply(args.times)
    }
  }
})

When we call the following GraphQL request,

mutation ExampleMutation {
  increment
  multiply(times: 2)
}

In first call, the initial counter value 0 will be incremented . Then the result becomes 1 and this value will be returned as a result of increment mutation, but on the same request we have multiply as well. So the same counter value will be multiplied by 2 , and this value will be returned as a result of multiply mutation.

So, on every request the result will be:

{
  "increment": 1,
  "multiply": 2
}

But if the provider is in ApplicationScope, the result will be [3,6] in the second call.

Request Scope

If you have request-scoped providers in your GraphQL Module, these providers are generated in every injection. This means a request-scoped provider is never kept neither application state, nor session state. So, this type of providers works just like Factory. It creates an instance each time you request from the injector.

Let’s assume we do the similar changes for RequestScope. The results will be like below on each request:

{
  "increment": 1,
  "multiply": 0
}

Because request-scoped providers are constructed on each injector.get call. They are kept in the current function scope, they’re not kept in any session or DI container. That’s why, we can consider them factory .

As you can see the example and the results above, ExampleApplicationProvider is shared between different network requests, while ExampleSessionProvider is shared between resolver-calls inside the same network request. But, ExampleRequestProvider is only kept in the same resolver call.

New ModuleSessionInfo Built-In Provider

Let’s assume that you have a token required for your authentication process. And this token is probably stored in request header. And GraphQL-Modules provides you access to network request in a built-in provider called ModuleSessionInfo. This session object is the raw request/response object passed by your HTTP Server. For example, if you’re using Express, you will get something like { req: IncommingMessage, res: Response } .

Every GraphQL-Module creates a ModuleSessionInfo instance in each network request that contains raw Session from the GraphQL Server, SessionInjector that contains Session-scoped instances together with Application-scoped ones and Context object which is constructed with contextBuilder of the module. But, notice that you cannot use this built-in provider.

@Injectable({
  scope: ProviderScope.Session
})
class ExampleSessionScopeProvider {
  constructor(private moduleSessionInfo: ModuleSessionInfo) {
    moduleSessionInfo // { session, context, injector }
  }
}
 
@Injectable({
  scope: ProviderScope.Application
})
class ExampleInvalidApplicationScopeProvider {
  constructor(private moduleSessionInfo: ModuleSessionInfo) {} // Error: ModuleSessionInfo is not valid provider
}
 
@Injectable({
  scope: ProviderScope.Application
})
class ExampleApplicationScopeProvider implements OnRequest {
  onRequest(moduleSessionInfo) {
    moduleSessionInfo // { session, context, injector }
  }
}

As you can see, you cannot use ModuleSessionInfo in Application Scope, because application-scoped providers belong to the whole application, and application-scoped providers are completely independent of the network request.

However, you can use onRequest hook to have ModuleSessionInfo in your Application-Scoped provider which is called on each network request.

What’s Next?

We are constantly trying to improve the set of tools provided by GraphQL-Modules, and we welcome you to try it and share your experience and thoughts.

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.

Recent issues of our newsletter

Similar articles