Skip to content

Dependency injection

When your serverless app becomes bigger, it might be useful to adopt dependency injection principles.

Benefits

  • Loose coupling: classes depend on abstractions (interfaces) rather than concrete implementations, making the code more flexible and maintainable.
  • Easier testing: real dependencies can easily be replaced with mocks or fakes for isolated unit testing.
  • Better organization: encourages separation of concerns, leading to cleaner, modular code.
  • Reusability: classes become more modular and can work with different dependencies in various contexts.
  • Improved maintainability: dependencies can be swapped or updated with minimal code changes.

Tips & tricks

For serverless projects, we have the following recommendations:

  • Make sure to create and bootstrap your DI container during the cold start of your lambda, to utilize performance boosts;
  • Reuse services by providing them as a "singleton": this will make sure instances are reused between separate lambda invocations;
  • If using classes, use constructor injection as much as possible, to maximize type-safety and to easily see what dependencies a class needs;
  • Using a single shared DI container for multiple lambdas may lead to bundles sizes that are larger than necessary. This can be solved by using tree-shakable tokens (if the chosen library allows it), or by using separate DI containers. Read more about analyzing your bundle size.
  • Needle DI: features tree-shakable tokens, and uses native ECMAScript decorators, eliminating the need for decorator metadata.
  • InversifyJS: an older but wider used library. Requires TypeScript decorator metadata. Make sure to configure it with defaultScope: 'Singleton'.

Example with Needle DI

Here is a simple example using @needle-di/core as DI library:

ts
import { bootstrapAsync } from "@needle-di/core";
import { defineRestLambda } from "@backpack/aws-lambda";

// on cold start, bootstrap a new instance with dependency injection
const addBookLambda = await bootstrapAsync(AddBookLambda);

// AWS Lambda handler
export const handler = defineRestLambda((event) => {
  return addBookLambda.handle(event);
});
ts
import { injectable, inject } from "@needle-di/core";
import type { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda";

import { Book } from "../models";

@injectable()
export class AddBookLambda {
  constructor(private readonly bookStore = inject(BookStoreService)) {}

  public async handle(event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> {
    const book = parseBody(event).json({ validated: Book });

    await this.bookStore.insertBook(book);

    return jsonOk(book);
  }
}
ts
import { inject, injectable } from "@needle-di/core";

import type { Book } from "./models";

/**
 * The BookStore service. Example of a service that contains domain logic.
 */
@injectable()
export class BookStoreService {
  constructor(private readonly booksRepository = inject(BooksRepository)) {}

  /**
   * Inserts a book.
   */
  public async insertBook(book: Book): Promise<void> {
    // additional logic

    await this.booksRepository.insert(book);
  }

  // ...
}

Check out the documentation of Needle DI to learn more.