Skip to content

maou-shonen/hono-simple-DI

Repository files navigation

Hono simple DI

npm version npm downloads bundle Codecov License

A small, type-safe DI library optimized for Hono.js.

Important

This package is optimized for Hono.js and is not designed for large projects. If you require advanced DI features such as automatic circular injection, dynamic binding, and multi-binding, etc. you may need a dedicated DI library.

Installation

# npm
npm install hono-simple-di
# pnpm
pnpm add hono-simple-di
# bun
bun add hono-simple-di

Usage

Basic usage

1. Define a service

First, you define a service that you want to inject. This could be any class or function that handles your business logic.

// services/UserService.ts
export class UserService {
  findOne(id: number) {
    return { id, name: `User ${id}` };
  }
}

2. Create a Dependency

Next, you create a dependency for your service, specifying how it should be initialized. You can also choose whether it should be a singleton (default) or multi-instance (per request).

import { Dependency } from "hono-simple-di";
import { UserService } from "./services/UserService";

// Define the dependency for UserService
const userServiceDep = new Dependency(() => new UserService());

3. Inject dependency via middleware

Use the middleware method to inject the dependency into your Hono.js context. Once injected, the service will be accessible through the context's c.get method.

import { Hono } from "hono";
import { userServiceDep } from "./dependencies";

const app = new Hono()
  // Use the dependency as middleware
  .use(userServiceDep.middleware("userService"))

  .get("/", (c) => {
    // Retrieve the injected service
    const { userService } = c.var;
    // or const userService = c.get('userService')

    const user = userService.findOne(1);

    return c.json(user);
  });

4. Override Service with injection

You can override the service instance at runtime using the injection method. This is useful in testing or when dynamically providing service instances.

// Inject a custom service instance
userServiceDep.injection({
  findOne(id: number) {
    return { id, name: "Injected User" };
  },
});

Reference another dependency

const postServiceDep = new Dependency(
  async (c) => new PostService(await userServiceDep.resolve(c)),
);

A service can also be something other than a class

For example, using headers from c.req.headers.

const uaDep = new Dependency(
  (c) => new UAParser(c.req.header("User-Agent") ?? ""),
  {
    scope: "request",
  },
);

const app = new Hono()
  .use(uaDep.middleware("ua"))

  .get("/", (c) => {
    const ua = c.get("ua");
    return c.text(`You are running on ${ua.getOS().name}!`);
  });

Using request scope service

If you need a new instance of the service for each request (multi-instance), set the scope option to request.

const requestIdDep = new Dependency((c) => Math.random(), {
  scope: "request",
});

const app = new Hono()
  // Inject a unique ID for each request
  .use(requestIdDep.middleware("requestId"))

  .get("/id", (c) => {
    const requestId = c.get("requestId");
    return c.text(`Request ID: ${requestId}`);
  });

Do not provide an initialization function

const userServiceDep = new Dependency<UserService | null>(() => null);

API

Dependency Interface

interface Dependency<Service> {
  constructor(
    /** A function to initialize the service. */
    private serviceInitializer: (c: Context) => MaybePromise<Service>,
    private opts?: {
      /**
       * The scope of the dependency.
       * @default 'default'
       * @remarks
       * - 'default': Service will be initialized only once.
       * - 'request': Service is initialized once per request and reused across requests.
       */
      scope?: Scope
    },
  ): Dependency

  /**
   * Injects a service instance directly. Useful for overriding the default service.
   * @param service - The service instance to be injected.
   * @returns this - The instance of the dependency for chaining.
   */
  injection(service: Service): this

  /**
   * Clear injected service.
   */
  clearInjected(): this {
    this.service = undefined;
    return this;
  }

  /**
   * Creates a middleware that injects the service into the context.
   * @param contextKey - Optionally override the key used to store the service in the context.
   * @returns MiddlewareHandler - A Hono.js middleware function.
   */
  middleware<ContextKey extends string>(
    /** The key used to store the service in the context. */
    contextKey?: ContextKey,
  ): MiddlewareHandler<{
    Variables: {
      [key in ContextKey]: Service
    }
  }>
}