Skip to content

Latest commit

 

History

History
143 lines (108 loc) · 3.7 KB

dependency-injection.md

File metadata and controls

143 lines (108 loc) · 3.7 KB

Dependency injection

Overview

Dependencies are managed using awilix and awilix-manager.

Configuration

You can find configuration for the dependencies in parentDiConfig.ts.

When adding a new module, you have to create a new diConfig for it (e.g. userDiConfig.ts) that:

  1. Exposes a method to resolve the new dependencies:
export function resolveUsersConfig(options: DIOptions): UsersDiConfig {
  return {
    userService: asClass(UserService, SINGLETON_CONFIG),
  }
}
  1. Adds new fields to the DiConfig interface:
export type UsersModuleDependencies = {
  userService: UserService
}

// dependencies injectable within the module
export type UsersInjectableDependencies = UsersModuleDependencies & CommonDependencies

// dependencies injectable across different modules
export type UsersPublicDependencies = Pick<UsersInjectableDependencies, 'userService'>

Additionally, the new resolver should be added to the parentDiConfig.ts:

const diConfig: DiConfig = {
  ...resolveCommonDiConfig(dependencies, options),
  ...resolveUsersConfig(options),
}

Resolve dependencies

In order to make a class a module, you have to implement a constructor which accepts dependencies: Dependencies as its only constructor parameter (asClass definition). Example:

export class UserService {
  private readonly userRepository: UserRepository

  constructor({ userRepository }: Dependencies) {
    this.userRepository = userRepository
  }
}

Alternatively, you can instantiate a module yourself inside a function that accepts dependencies: Dependencies as its only parameter (asFunction definition). Example:

;({ config }: Dependencies) => {
  return new PrismaClient({
    datasources: {
      db: {
        url: config.db.databaseUrl,
      },
    },
  })
}

Injection

Declared dependencies can be injected from the request, application, or globally:

import { diContainer } from '@fastify/awilix'

// accessing the DI context on the request
export const postCreateUser = async (req: FastifyRequest, reply: FastifyReply) => {
  const { userService } = req.diScope.cradle
}

// accessing the DI context on the app
function plugin(fastify: FastifyInstance, opts: unknown, done: () => void) {
  const { prisma, redis } = fastify.diContainer.cradle
  done()
}

// accessing the DI context globally
const { userService } = diContainer.cradle

Notable details

  • All dependencies are lazily resolved whenever first requested either via cradle, or as a dependency for some other module;
  • All dependencies are resolved synchronously, all asynchronous activation logic needs to be implemented separately;
  • You should define a dispose function in DI options for the module if it has some resources to cleanup (e. g. a DB connection);

Mocking the dependencies

For testing purposes you can override any modules in the dependency graph.

When instantiating full app in a test:

import { getApp } from './app'

let app: FastifyInstance
beforeAll(async () => {
  app = await getApp(
    {},
    // All dependencies passed here will replace the real ones
    {
      userService: asClass(FakeUserService, SINGLETON_CONFIG),
    },
  )
})

When instantiating just the test context:

import { createTestContext } from '../../../../test/TestContext'

let testContext: TestContext
beforeAll(async () => {
  testContext = createTestContext(
    // All dependencies passed here will replace the real ones
    {
      userService: asClass(FakeUserService, SINGLETON_CONFIG),
    },
  )
})