Hono で Dependency Injection する

Icon

70_10

Hono で Dependency Injection をしたかったので、シンプルな DI コンテナを作りました。

di-container.ts
export class DIContainer<DependencyTypes> {
  private registry = new Map<
    keyof DependencyTypes,
    DependencyTypes[keyof DependencyTypes]
  >();

  register<Key extends keyof DependencyTypes, Args extends unknown[]>(
    key: Key,
    Constructor: new (...args: Args) => DependencyTypes[Key],
    ...args: Args
  ): void {
    const instance = new Constructor(...args);
    this.registry.set(key, instance);
  }

  get<K extends keyof DependencyTypes>(key: K): DependencyTypes[K] {
    const instance = this.registry.get(key);
    if (!instance) {
      throw new Error(`No instance found for key: ${String(key)}`);
    }
    return instance as DependencyTypes[K];
  }
}

使い方

UserService と UserRepository を登録して利用する例です。

import { DIContainer } from "./di-container";

interface User {
  id: number;
  name: string;
}

interface IUserRepository {
  findUser(id: number): User;
}

class UserRepository implements IUserRepository {
  findUser(id: number) {
    return {
      id,
      name: `User${id}`,
    } satisfies User;
  }
}

interface IUserService {
  getUser(id: number): User;
}

class UserService implements IUserService {
  constructor(private userRepository: IUserRepository) {}

  getUser(id: number) {
    return this.userRepository.findUser(id);
  }
}

// DIコンテナで管理するオブジェクトの型宣言
interface DependencyTypes {
  UserRepository: IUserRepository;
  UserService: IUserService;
}

// DIコンテナの初期化
const diContainer = new DIContainer<DependencyTypes>();

// オブジェクトの登録
diContainer.register("UserRepository", UserRepository);
diContainer.register(
  "UserService",
  UserService,
  diContainer.get("UserRepository"),
);

// オブジェクトの利用
const userService = diContainer.get("UserService");
console.log(userService.getUser(1)); // { id: 1, name: "User1" }

Hono で使う

DIContainer を Hono で使えるようにします。
Context の set()/get() を通じて DIContainer へアクセスします。

1. Variables に DIContainer を指定する

Hono の Variables の型に DIContainer を指定します。

const app = new Hono<{
  Variables: {
    diContainer: DIContainer<DependencyTypes>;
  };
}>();

2. context.set() でどこからでもアクセスできるようにする

すべてのエンドポイントからアクセスできるように context.set() で DIContainer をセットします。

app.use("*", (c, next) => {
  c.set("diContainer", diContainer);
  return next();
});

DIContainer を使うには cotext.get() から取得します。

app.get("/users/:id", (c) => {
  const di = c.get("diContainer");
  const id = parseInt(c.req.param("id"));

  const userService = di.get("UserService");
  const user = userService.getUser(id);

  return c.json(user);
});

完成形

index.ts
import { Hono } from "hono";
import { DIContainer } from "./di-container";

interface User {
  id: number;
  name: string;
}

interface IUserRepository {
  findUser(id: number): User;
}

class UserRepository implements IUserRepository {
  findUser(id: number) {
    return {
      id,
      name: `User${id}`,
    } satisfies User;
  }
}

interface IUserService {
  getUser(id: number): User;
}

class UserService implements IUserService {
  constructor(private userRepository: IUserRepository) {}

  getUser(id: number) {
    return this.userRepository.findUser(id);
  }
}

interface DependencyTypes {
  UserRepository: IUserRepository;
  UserService: IUserService;
}

const diContainer = new DIContainer<DependencyTypes>();

diContainer.register("UserRepository", UserRepository);
diContainer.register("UserService", UserService, diContainer.get("UserRepository"));

const app = new Hono<{
  Variables: {
    diContainer: DIContainer<DependencyTypes>;
  };
}>();

app.use("*", (c, next) => {
  c.set("diContainer", diContainer);
  return next();
});

app.get("/users/:id", (c) => {
  const di = c.get("diContainer");
  const id = parseInt(c.req.param("id"));

  const userService = di.get("UserService");
  const user = userService.getUser(id);

  return c.json(user);
});

export default app;

リポジトリ

今回作った DI コンテナを使ったサンプルは、以下のリポジトリにあります。

参考情報

Dependency Injection とは

Hono で DI を利用する