Advanced DI & Testing
Circular dependency detection, reserved names, modular containers, and mocking in tests.
This guide covers advanced dependency injection patterns, safety guarantees, and testing strategies in Nooh.
Circular Dependency Detection
Circular dependencies occur when service A depends on B, and B depends back on A (directly or through an intermediate chain).
Nooh detects cycles at two stages:
- Compilation Phase: The compiler builds an adjacency graph of all declared dependencies and emits diagnostic error
NOOH023if a cycle is found. - Runtime Resolution: If dynamic circular references occur during resolution, Nooh throws a
DependencyCycleErrorwith the full dependency chain in the stack:
DependencyCycleError: Circular dependency detected: serviceA -> serviceB -> serviceC -> serviceA.
Naming Rules & Reserved Names
When declaring dependencies, Nooh validates the keys:
- Duplicate Names: Nooh prevents duplicate keys in dependency arrays at compile time using
DuplicateDependency<Name>. - Reserved Context Keys: Dependency names cannot collide with built-in handler context parameters. Reserved keys include
c,next,error,errors,onError,response, and validation targets (json,form,query,param,header,cookie). Nooh emitsReservedDependencyName<Name>if a collision is detected.
Modular Containers
You can split dependencies across multiple containers and modules to keep domains cleanly isolated:
import { container, singleton } from "@nooh-ts/nooh";
import { PrismaClient } from "@prisma/client";
export const dbContainer = container({
prisma: singleton(() => new PrismaClient()),
});import { container, singleton } from "@nooh-ts/nooh";
import { dbContainer } from "./database";
import { AuthService } from "../services/auth";
export const authContainer = container({
authService: singleton({
deps: [dbContainer.prisma],
factory: ({ prisma }) => new AuthService(prisma),
}),
});In route files, you can inject dependencies from multiple different containers in the same route deps array:
import { post } from "@router/auth/login";
import { dbContainer } from "@/dependencies/database";
import { authContainer } from "@/dependencies/auth";
export default post({
deps: [authContainer.authService, dbContainer.prisma],
handler: async ({ c, authService, prisma }) => {
// Both dependencies are destructured directly from the handler parameter
const user = await authService.validateLogin(c);
return c.json({ user });
},
});
Testing & Mocking
Because Nooh handlers receive dependencies destructured in their input argument without relying on global state, testing handler logic is simple:
import { describe, it, expect, vi } from "vitest";
describe("GET /users", () => {
it("returns users from the mock service", async () => {
const mockUsers = [{ id: "1", name: "Alice" }];
const mockUserService = {
getAll: vi.fn().mockResolvedValue(mockUsers),
};
// Pass destructured mocks directly into handler function
const mockContext = { json: (data: unknown) => data } as any;
const result = await mockUserService.getAll();
expect(result).toEqual(mockUsers);
});
});