Setting up InversifyJs for lambda

Introduction

There are huge benefits to be had in serverless technologies.

  • Low cost for infrequent or short running functions
  • Ease of adoption
  • Little to no infrastructure maintenance

Whilst there are undoubted benefits, there are also pitfalls that can trip up unexperienced developers.

  • Slow startup times
  • Code fragmentation
  • Violation of DRY principles

I've experimented in the past with running a .NETCore service pipeline with good results but unfortunately, the cold start time is atrocious, leading me to adopt Node.js for the backend.
Unfortunately, that means ditching all the DI and pipeline goodness that we get for free with .NET, unless we use Express which unfortunately I found also to be too slow for cold starts.
So for a while, I just developed bare, this of course becomes somewhat un-maintainable once we have more than a couple of services which all require repository access, logging, http etc..
So, back to the search for a good, lightweight DI container, after a brief search, Inversify.JS looks to fit the bill. 2019 03 01 11 01 10 InversifyJS

Setup

Install the following 2 packages

// cmd
npm install inversify reflect-metadata

InversifyJS requires TypeScript 2.0+ and the experimentalDecorators, emitDecoratorMetadata, types and lib compilation options in your tsconfig.json file (source)

// tsconfig.json
{
  "compilerOptions": {
    "target": "es5",
    "lib": ["es6", "dom"],
    "types": ["reflect-metadata"],
    "module": "commonjs",
    "moduleResolution": "node",
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

Interfaces

Next we'll create interfaces to be injected into our IOC container

// i-logger.ts
export interface ILogger {
  debug(message: string): void;
  info(message: string): void;
}
// i-repository.ts
export interface IRepository {
  get<T>(type: new () => T): T;
}

Identifiers

InversifyJS need to use the type as identifiers at runtime. We use symbols as identifiers but you can also use classes and or string literals. (source)

Note: It is recommended to use Symbols but InversifyJS also support the usage of Classes and string literals (source)

// types.ts
export const TYPES = {
  Repository: Symbol("Repository"),
  Logger: Symbol("Logger")
};

export default TYPES;

Service implementations

Now we'll create our concrete implementations of our interfaces

// logger.ts
import { ILogger } from "./interfaces/i-logger";
import { injectable } from "inversify";

@injectable()
export class Logger implements ILogger {
  debug(message: string): void {
    console.log(`DEBUG: ${message}`);
  }
  info(message: string): void {
    console.log(`INFO: ${message}`);
  }
}

Note how we've marked our class with an @injectable() attribute

// repository.ts
import { IRepository } from "./interfaces/i-repository";
import { injectable, inject } from "inversify";
import { TYPES } from "../types";
import { ILogger } from "./interfaces/i-logger";

@injectable()
export class Repository implements IRepository {
  constructor(@inject(TYPES.Logger) private _logger: ILogger) {}

  get<T>(type: new () => T): T {
    // get data from repository
    this._logger.debug(`Repository. Get data`);
    return new type();
  }
}

Note how we've marked our _logger parameter for with the @inject(Type) attribute for constructor injection

InversifyJS supports both constructor and property injection, personally I prefer constructor injection as it's then crystal clear you should not just new() up an instance of the class.
See Field Dependency Injection Considered Harmful and Dependency Injection: Constructor vs Property for analysis of constructor and property injection.

Container setup

Now we need to setup our IOC container to store and resolve our dependencies
InversifyJS recommends we call our config file inversify.config.ts

NOTE: Per the instructions
The reflect-metadata polyfill should be imported only once in your entire application (source) we need to import reflect-metadata once and only once, so we'll import it in our inversify.config.ts file

// inversify.config.ts
import "reflect-metadata";

import { Container } from "inversify";
import TYPES from "./types";
import { ILogger } from "./services/interfaces/i-logger";
import { Logger } from "./services/logger";
import { IRepository } from "./services/interfaces/i-repository";
import { Repository } from "./services/repository";

const container: Container = new Container();

container.bind<ILogger>(TYPES.Logger).to(Logger);
container.bind<IRepository>(TYPES.Repository).to(Repository);

export default container;

Dependency resolution

Now we've set everything up we should be able to resolve our dependencies
Manual resolution should only be done in our Composition Root, in the case of lambda, our function entry point in handler.ts. In all other cases, resolution should be done automatically via our container.

// handler.ts
import { APIGatewayProxyHandler } from "aws-lambda";
import container from "./inversify.config";
import { IRepository } from "./services/interfaces/i-repository";
import TYPES from "./types";
import { ILogger } from "./services/interfaces/i-logger";
import { Account } from "./models/account";

export const hello: APIGatewayProxyHandler = async event => {
  console.log("hello");

  const repo = container.get<IRepository>(TYPES.Repository);
  console.log("create repo");

  const logger = container.get<ILogger>(TYPES.Logger);
  console.log("create logger");

  const account = repo.get(Account);
  logger.debug(`Account ok: ${account != null}`);
  logger.debug("Created account via DI resolved repository ok. Woot!");

  return {
    statusCode: 200,
    body: JSON.stringify({
      message:
        "Go Serverless Webpack (Typescript) v1.0! Your function executed successfully!",
      input: event
    })
  };
};

F5

Now lets run this baby and see if it all works ok

  1. serverless deploy
  2. Go to our endpoint in Chrome or Postman
    https://52hcvqfpr0.execute-api.us-east-1.amazonaws.com/dev/hello
  3. Presuming everything is ok, we should get a json blob back in our browser and can confirm everything worked as expected via the CloudWatch console logs for our lambda function

cloudwatch

Conclusion

Setting up a DI container seems like a lot of work for a simple function and is not necessary for all cases.
For larger projects however, with a large function base it can be invaluable for avoiding code fragmentation and improving maintainability.

Happy injecting

comment

Comments

arrow_back

Previous

JSON Schema Generation

Next

Serverless with Typescript on AWS with Lambda and NodeJS
arrow_forward