🌟

FastifyにInversifyJSを導入する

2024/01/18に公開

Fastifyで開発する時に、InversifyJSを使用してDIコンテナに登録できるようにする。

サンプルのプロジェクトを作成したので、実際の動作はそちらを参照。
https://github.com/sato-dev1234/fastify-drizzle-example

ディレクトリ構成
.
└── src
    ├── factories
    │   ├── db
    │   │   └── db.setup.ts
    │   └── app.setup.ts
    └── plugins
        ├── container
        │   ├── container.plugin.ts
        │   ├── index.ts
        │   └── types.ts
        └── index.ts
  • index.ts:DIコンテナに登録するしたいクラスを管理する。
  • container.plugin.ts:DIコンテナの登録する。
  • db.setup.ts:ORMの設定を行い、DIコンテナに登録する。

クラスをDIコンテナに登録

index.ts

index.tsはクラスInjectしたいクラスをまとめてexportするだけ。

index.ts
import ProfileService from "@/application/usecase/profile/profile.service";
import ProfileController from "@/controller/profile/profile.controller";
import MultiTableRepositoryImpl from "@/infrastructure/repositories/multi.table.repository";
import UserRepositoryImpl from "@/infrastructure/repositories/user.reposirory";

const controller = [ProfileController];
const service = [ProfileService];
const repository = [UserRepositoryImpl, MultiTableRepositoryImpl];

export default [...controller, ...service, ...repository];

types.ts

  • DIコンテナに登録する際に、固定値を設定するものはTYPES、クラス名を登録したい場合はclassToSymbolを使用するようにする。
types.ts
export const TYPES = {
  DB: Symbol.for("db"),
};

export const classToSymbol = (targetClass: { name: string }) =>
  Symbol.for(targetClass.name);

container.plugin.ts

  • targetsはindex.tsからexportしたクラスの配列になる。
  • クラス名でbindするので、classToSymbolを呼び出す。
  • スコープは今回はSingletonを指定する。
container.plugin.ts
import "reflect-metadata";
import {
  FastifyInstance,
  FastifyPluginOptions,
  FastifyPluginAsync,
} from "fastify";
import { fastifyPlugin } from "fastify-plugin";
import { Container } from "inversify";

import targets from "@/plugins/container";

const containerPlugin: FastifyPluginAsync = async (
  fastify: FastifyInstance,
  _opts: FastifyPluginOptions,
): Promise<void> => {
  const container = new Container();
  targets.forEach((targetClass) => {
    /* eslint new-cap: "off" */
    container
      .bind(Symbol.for(targetClass.name))
      .to(targetClass)
      .inSingletonScope();
  });

  fastify.decorate("container", container);
};

参考

https://github.com/inversify/InversifyJS?tab=readme-ov-file#step-3-create-and-configure-a-container

ORMをDIコンテナに登録

db.setup.ts

  • container.plugin.tsでcontainerをデコレータに登録しているので、FastifyInstanceから呼び出す。
db.setup.ts
import { NodePgDatabase, drizzle } from "drizzle-orm/node-postgres";
import { FastifyInstance } from "fastify";
import { Client } from "pg";

import * as schema from "@/infrastructure/db/schema";
import { TYPES } from "@/plugins/container/types";

export const setupDb = async (fastify: FastifyInstance): Promise<void> => {
  const client = new Client({
    connectionString: fastify.config.DATABASE_URL,
  });
  try {
    await client.connect();
    fastify.decorate("pg", client);
    const drizzleClient: NodePgDatabase<typeof schema> = drizzle(fastify.pg, {
      schema,
    });
    fastify.container
      .bind(TYPES.DB)
      .toDynamicValue(() => drizzleClient)
      .inSingletonScope();
  } catch (error) {
    fastify.log.error(error);
    throw error;
  }

  fastify.addHook("onClose", async () => {
    await fastify.pg.end();
  });
};

呼び出す順番

  1. registerPlugins:DIコンテナの登録を行い、DIコンテナをデコレータに登録する。
  2. setupDb:デコレータに登録されたDIコンテナを呼び出し、ORMの設定を登録する。
  3. registerRoutes:DIコンテナの登録が終了したら、ルートの登録を行う。
  • DIコンテナの登録する前にルーティングするとエラーが発生するので注意すること。

app.setup.ts

app.setup.ts
import fastifyEnv from "@fastify/env";
import { FastifyInstance } from "fastify";

import { setupDb } from "./db/db.setup";

import { envOptions } from "@/infrastructure/config/env";

const registerPlugins = async (fastify: FastifyInstance, plugins: any) => {
  await fastify.register(fastifyEnv, envOptions());
  const registerPromises = plugins.map((plugin: any) =>
    fastify.register(plugin),
  );
  await Promise.all(registerPromises);
};

const registerRoutes = async (fastify: FastifyInstance, routes: any) => {
  const registerPromises = routes.map(async (route: any) => {
    /* eslint new-cap: "off" */
    const router = new route();
    await fastify.register(router.routes, { prefix: router.prefixRoute });
  });
  await Promise.all(registerPromises);
};

export const setup = async (
  fastify: FastifyInstance,
  plugins: any,
  routes: any,
): Promise<void> => {
  await registerPlugins(fastify, plugins);
  await setupDb(fastify);
  await registerRoutes(fastify, routes);
};

DIコンテナに登録されたクラス・ORMを呼び出す。

route -> cotroller -> usecase -> repositoryの順で処理を行うので、それぞれ記載する。

  • route:デコレータに登録されたDIコンテナを経由して呼び出す。
  • それ以外:Property Injectionで呼び出す。

profile.route.ts

  • デコレータに登録されたDIコンテナを呼び出して、ProfileControllerを呼び出す。
  • クラス名で登録されているので、classToSymbolを使用して呼び出す。
profile.route.ts
class ProfileRoute {
  public prefixRoute = "/profile";

  async routes(
    fastify: FastifyInstance,
    _options: FastifyPluginOptions,
  ): Promise<void> {
    const profileController: ProfileController =
      fastify.container.get<ProfileController>(
        classToSymbol(ProfileController),
      );

    fastify.post(
      "/create",
      {
        schema: {
          body: profileInsertSchema,
        },
      },
      async (request: FastifyRequest<{ Body: ProfileInsertSchema }>, reply) => {
        await profileController.create(request);
        return reply.code(201).send();
      },
    );
  }
}

profile.controller.ts

  • Property Injectionを使用する。
  • クラス名で登録されているので、classToSymbolを使用して呼び出す。
profile.controller.ts
@injectable()
class ProfileController {
  @inject(classToSymbol(ProfileService))
  private readonly profileService: ProfileService;

  async create(request: FastifyRequest<{ Body: ProfileInsertSchema }>) {
    await this.profileService.create(request.body);
  }
}

profile.service.ts

  • Property Injectionを使用する。
  • クラス名で登録されているので、classToSymbolを使用して呼び出す。
profile.service.ts
@injectable()
class ProfileService {
  @inject(classToSymbol(UserRepositoryImpl))
  private readonly userRepository: UserRepository;

  async create(profileInsertSchema: ProfileInsertSchema): Promise<void> {

  }
}

user.reposirory.ts

  • Property Injectionを使用する。
  • ORMは固定値で登録されているので、TYPES.DBで呼び出す。
user.reposirory.ts
@injectable()
class UserRepositoryImpl implements UserRepository {
  @inject(TYPES.DB) private db: NodePgDatabase<typeof schema>;

  async findMany(
    userCondition?: SQL | undefined,
    contactCondition?: SQL | undefined,
  ): Promise<ProfileSelectSchema[]> {
    const entities = await this.db.query.user.findMany({
      where: and(isNull(user.deletedAt), userCondition),
      with: {
        contacts: {
          where: and(isNull(contact.deletedAt), contactCondition),
        },
      },
    });
    if (entities == undefined) {
      return [];
    }
    return toUserSelectSchemas(entities);
  }
}

Discussion