Skip to content

New skill: monotemplate dependency injection (Inversify) #21

Description

@firstloop-jake

Skill

monotemplate:di (new skill)

Problem

There is no skill documenting the dependency injection pattern used in the monotemplate. When Claude adds new entities (repositories, controllers, services), it needs to understand how to wire them into the Inversify DI container correctly. Without this guidance, Claude may create classes without @injectable() decorators, forget @inject() on constructor parameters, skip registering bindings in the container, or misunderstand the simulated auth/__dev__ gating patterns.

Proposed Solution

Create a monotemplate:di skill that documents the Inversify DI pattern as implemented in the template. The skill should cover:

Core Pattern

The template uses Inversify v7 with reflect-metadata for decorator-based dependency injection. All DI configuration lives in apps/server/src/di/container.ts, which exports a singleton container instance.

Key Concepts to Document

1. Making a class injectable

  • Every Repository, Controller, and Service class must be decorated with @injectable()
  • Constructor dependencies use @inject(ClassName) parameter decorators
  • Import the class itself (not type-only) so the class token is available at runtime
import { injectable, inject } from "inversify";
import { SomeRepository } from "@server/repositories/SomeRepository";

@injectable()
export class SomeController {
  constructor(@inject(SomeRepository) private someRepository: SomeRepository) {}
}

2. Registering bindings in container.ts

  • Classes that auto-resolve their own deps: container.bind(ClassName).toSelf().inSingletonScope()
  • External/constant values (PrismaClient, env-gated services): container.bind(Token).toConstantValue(value)
  • Abstract → concrete (auth): container.bind(AbstractClass).to(ConcreteClass).inSingletonScope()
  • Multi-bindings (middleware): container.bind(SYMBOL_TOKEN).toConstantValue(value) (called multiple times)

3. Resolving dependencies

  • In routers/handlers: container.get(ClassName) — import both container and the class
  • In server.ts: container.get(AuthContextFactory), container.getAll<T>(EXPRESS_MIDDLEWARE)
  • Never destructure from a container object; always use container.get()

4. Binding categories and order in container.ts

// Database
container.bind(PrismaClient).toConstantValue(prisma);

// Middleware (multi-binding via Symbol token)
container.bind(EXPRESS_MIDDLEWARE).toConstantValue(helmet());
container.bind(EXPRESS_MIDDLEWARE).toConstantValue(express.json());

// Auth (abstract → concrete, with simulated mode gating)
if (isSimulated()) {
    container.bind(AuthContextFactory).to(SimulatedAuthContextFactory).inSingletonScope();
} else {
    container.bind(AuthContextFactory).to(ClerkAuthContextFactory).inSingletonScope();
}

// Repositories
container.bind(FooRepository).toSelf().inSingletonScope();

// Controllers
container.bind(FooController).toSelf().inSingletonScope();

5. Symbol tokens (di/tokens.ts)

  • Used for multi-bindings where multiple values share one key (e.g., EXPRESS_MIDDLEWARE)
  • Defined as export const TOKEN_NAME = Symbol.for("TokenName");

6. Simulated / E2E auth gating

  • __dev__/ directory code is dynamically imported inside isSimulated() checks (Bun macro)
  • Never use static imports for __dev__/ code in production paths
  • Pattern: const { SimClass } = await import("@server/__dev__/SimClass");

7. tsconfig requirements

  • experimentalDecorators: true and emitDecoratorMetadata: true must be set
  • import "reflect-metadata" must be the first import in the entry point (index.ts)

8. When adding a new entity (checklist)

  1. Add @injectable() to the class
  2. Add @inject(Dep) to each constructor parameter
  3. Import classes (not type-only) for injected dependencies
  4. Register binding in container.ts under the appropriate section
  5. In routers, resolve via container.get(ClassName)

Trigger Description

Use when adding new repositories, controllers, services, or middleware to the server. Use when wiring dependencies, configuring the DI container, or understanding how classes are resolved.

Examples

Before (manual wiring — old pattern):

export const createContainer = () => {
    const prisma = new PrismaClient({ adapter });
    const usersRepository = new UsersRepository(prisma);
    const usersController = new UsersController(usersRepository);
    return { prisma, usersRepository, usersController };
};

After (Inversify DI — current pattern):

// container.ts
const container = new Container();
container.bind(PrismaClient).toConstantValue(prisma);
container.bind(UsersRepository).toSelf().inSingletonScope();
container.bind(UsersController).toSelf().inSingletonScope();
export { container };

// router.ts
import { container } from "@server/di/container";
import { UsersController } from "@server/controllers/UsersController";
const result = await container.get(UsersController).getUserById(id);

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions