🌟
FastifyにInversifyJSを導入する
Fastifyで開発する時に、InversifyJSを使用してDIコンテナに登録できるようにする。
サンプルのプロジェクトを作成したので、実際の動作はそちらを参照。
ディレクトリ構成
.
└── 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);
};
参考
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();
});
};
呼び出す順番
- registerPlugins:DIコンテナの登録を行い、DIコンテナをデコレータに登録する。
- setupDb:デコレータに登録されたDIコンテナを呼び出し、ORMの設定を登録する。
- 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