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.
Recommended libraries
- 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.