Fastify Integration

This guide shows how to integrate ts-ioc-container with Fastify applications. Fastify’s plugin architecture and decorator system work well with dependency injection patterns.

Fastify integration provides:

Scope Hierarchy

Create the container configuration:

TypeScript src/container/index.ts
// src/container/index.ts
import 'reflect-metadata';
import {
Container,
MetadataInjector,
Registration,
singleton,
scope,
} from 'ts-ioc-container';
import type { IContainer } from 'ts-ioc-container';
import { DatabasePool } from '../services/DatabasePool';
import { ConfigService } from '../services/ConfigService';
import { Logger } from '../services/Logger';
import { UserRepository } from '../repositories/UserRepository';
import { RequestContext } from '../services/RequestContext';
import { AuditLogger } from '../services/AuditLogger';

export function createAppContainer(): IContainer {
return new Container(new MetadataInjector(), { tags: ['application'] })
  .addRegistration(
    // Application-scoped services (singletons)
    Registration.fromClass(ConfigService)
      .pipe(singleton())
      .to('IConfigService'),
    Registration.fromClass(DatabasePool)
      .pipe(singleton())
      .to('IDatabasePool'),
    Registration.fromClass(Logger)
      .pipe(singleton())
      .to('ILogger'),
  )
  .addRegistration(
    // Request-scoped services
    Registration.fromClass(UserRepository)
      .pipe(scope((s) => s.hasTag('request')))
      .pipe(singleton())
      .to('IUserRepository'),
    Registration.fromClass(RequestContext)
      .pipe(scope((s) => s.hasTag('request')))
      .pipe(singleton())
      .to('IRequestContext'),
    Registration.fromClass(AuditLogger)
      .pipe(scope((s) => s.hasTag('request')))
      .pipe(singleton())
      .to('IAuditLogger'),
  );
}
// src/container/index.ts
import 'reflect-metadata';
import {
Container,
MetadataInjector,
Registration,
singleton,
scope,
} from 'ts-ioc-container';
import type { IContainer } from 'ts-ioc-container';
import { DatabasePool } from '../services/DatabasePool';
import { ConfigService } from '../services/ConfigService';
import { Logger } from '../services/Logger';
import { UserRepository } from '../repositories/UserRepository';
import { RequestContext } from '../services/RequestContext';
import { AuditLogger } from '../services/AuditLogger';

export function createAppContainer(): IContainer {
return new Container(new MetadataInjector(), { tags: ['application'] })
  .addRegistration(
    // Application-scoped services (singletons)
    Registration.fromClass(ConfigService)
      .pipe(singleton())
      .to('IConfigService'),
    Registration.fromClass(DatabasePool)
      .pipe(singleton())
      .to('IDatabasePool'),
    Registration.fromClass(Logger)
      .pipe(singleton())
      .to('ILogger'),
  )
  .addRegistration(
    // Request-scoped services
    Registration.fromClass(UserRepository)
      .pipe(scope((s) => s.hasTag('request')))
      .pipe(singleton())
      .to('IUserRepository'),
    Registration.fromClass(RequestContext)
      .pipe(scope((s) => s.hasTag('request')))
      .pipe(singleton())
      .to('IRequestContext'),
    Registration.fromClass(AuditLogger)
      .pipe(scope((s) => s.hasTag('request')))
      .pipe(singleton())
      .to('IAuditLogger'),
  );
}

Create a Fastify plugin to manage the container:

TypeScript src/plugins/container.ts
// src/plugins/container.ts
import { FastifyPluginAsync, FastifyRequest } from 'fastify';
import fp from 'fastify-plugin';
import type { IContainer } from 'ts-ioc-container';

declare module 'fastify' {
interface FastifyInstance {
  container: IContainer;
}
interface FastifyRequest {
  container: IContainer;
}
}

export interface ContainerPluginOptions {
container: IContainer;
}

const containerPlugin: FastifyPluginAsync<ContainerPluginOptions> = async (
fastify,
options
) => {
const { container } = options;

// Decorate fastify instance with app container
fastify.decorate('container', container);

// Decorate request with request-scoped container
fastify.decorateRequest('container', null);

// Create request scope for each request
fastify.addHook('onRequest', async (request) => {
  request.container = container.createScope({ tags: ['request'] });
});

// Dispose request scope after response
fastify.addHook('onResponse', async (request) => {
  request.container.dispose();
});

// Dispose on error as well
fastify.addHook('onError', async (request) => {
  if (request.container) {
    request.container.dispose();
  }
});

// Cleanup on close
fastify.addHook('onClose', async () => {
  container.dispose();
});
};

export default fp(containerPlugin, {
name: 'container',
fastify: '5.x',
});
// src/plugins/container.ts
import { FastifyPluginAsync, FastifyRequest } from 'fastify';
import fp from 'fastify-plugin';
import type { IContainer } from 'ts-ioc-container';

declare module 'fastify' {
interface FastifyInstance {
  container: IContainer;
}
interface FastifyRequest {
  container: IContainer;
}
}

export interface ContainerPluginOptions {
container: IContainer;
}

const containerPlugin: FastifyPluginAsync<ContainerPluginOptions> = async (
fastify,
options
) => {
const { container } = options;

// Decorate fastify instance with app container
fastify.decorate('container', container);

// Decorate request with request-scoped container
fastify.decorateRequest('container', null);

// Create request scope for each request
fastify.addHook('onRequest', async (request) => {
  request.container = container.createScope({ tags: ['request'] });
});

// Dispose request scope after response
fastify.addHook('onResponse', async (request) => {
  request.container.dispose();
});

// Dispose on error as well
fastify.addHook('onError', async (request) => {
  if (request.container) {
    request.container.dispose();
  }
});

// Cleanup on close
fastify.addHook('onClose', async () => {
  container.dispose();
});
};

export default fp(containerPlugin, {
name: 'container',
fastify: '5.x',
});

Configure Fastify with the container plugin:

TypeScript src/app.ts
// src/app.ts
import Fastify from 'fastify';
import { createAppContainer } from './container';
import containerPlugin from './plugins/container';
import usersRoutes from './routes/users';
import ordersRoutes from './routes/orders';

export async function buildApp() {
const fastify = Fastify({
  logger: true,
});

// Create and register container
const container = createAppContainer();
await fastify.register(containerPlugin, { container });

// Register routes
await fastify.register(usersRoutes, { prefix: '/api/users' });
await fastify.register(ordersRoutes, { prefix: '/api/orders' });

return fastify;
}

// Start server
async function start() {
const app = await buildApp();

try {
  await app.listen({ port: 3000 });
} catch (err) {
  app.log.error(err);
  process.exit(1);
}
}

start();
// src/app.ts
import Fastify from 'fastify';
import { createAppContainer } from './container';
import containerPlugin from './plugins/container';
import usersRoutes from './routes/users';
import ordersRoutes from './routes/orders';

export async function buildApp() {
const fastify = Fastify({
  logger: true,
});

// Create and register container
const container = createAppContainer();
await fastify.register(containerPlugin, { container });

// Register routes
await fastify.register(usersRoutes, { prefix: '/api/users' });
await fastify.register(ordersRoutes, { prefix: '/api/orders' });

return fastify;
}

// Start server
async function start() {
const app = await buildApp();

try {
  await app.listen({ port: 3000 });
} catch (err) {
  app.log.error(err);
  process.exit(1);
}
}

start();

Resolve dependencies in route handlers:

TypeScript src/routes/users.ts
// src/routes/users.ts
import { FastifyPluginAsync } from 'fastify';
import type { IUserRepository } from '../repositories/UserRepository';
import type { IAuditLogger } from '../services/AuditLogger';
import type { IRequestContext } from '../services/RequestContext';

interface UserBody {
name: string;
email: string;
}

interface UserParams {
id: string;
}

const usersRoutes: FastifyPluginAsync = async (fastify) => {
// GET /api/users
fastify.get('/', async (request, reply) => {
  const userRepo = request.container.resolve<IUserRepository>('IUserRepository');
  const auditLogger = request.container.resolve<IAuditLogger>('IAuditLogger');

  const users = await userRepo.findAll();

  await auditLogger.log('users_listed', { count: users.length });

  return users;
});

// GET /api/users/:id
fastify.get<{ Params: UserParams }>('/:id', async (request, reply) => {
  const userRepo = request.container.resolve<IUserRepository>('IUserRepository');

  const user = await userRepo.findById(request.params.id);

  if (!user) {
    return reply.status(404).send({ error: 'User not found' });
  }

  return user;
});

// POST /api/users
fastify.post<{ Body: UserBody }>('/', async (request, reply) => {
  const userRepo = request.container.resolve<IUserRepository>('IUserRepository');
  const auditLogger = request.container.resolve<IAuditLogger>('IAuditLogger');
  const context = request.container.resolve<IRequestContext>('IRequestContext');

  const user = await userRepo.create(request.body);

  await auditLogger.log('user_created', {
    userId: user.id,
    requestId: context.requestId,
  });

  return reply.status(201).send(user);
});
};

export default usersRoutes;
// src/routes/users.ts
import { FastifyPluginAsync } from 'fastify';
import type { IUserRepository } from '../repositories/UserRepository';
import type { IAuditLogger } from '../services/AuditLogger';
import type { IRequestContext } from '../services/RequestContext';

interface UserBody {
name: string;
email: string;
}

interface UserParams {
id: string;
}

const usersRoutes: FastifyPluginAsync = async (fastify) => {
// GET /api/users
fastify.get('/', async (request, reply) => {
  const userRepo = request.container.resolve<IUserRepository>('IUserRepository');
  const auditLogger = request.container.resolve<IAuditLogger>('IAuditLogger');

  const users = await userRepo.findAll();

  await auditLogger.log('users_listed', { count: users.length });

  return users;
});

// GET /api/users/:id
fastify.get<{ Params: UserParams }>('/:id', async (request, reply) => {
  const userRepo = request.container.resolve<IUserRepository>('IUserRepository');

  const user = await userRepo.findById(request.params.id);

  if (!user) {
    return reply.status(404).send({ error: 'User not found' });
  }

  return user;
});

// POST /api/users
fastify.post<{ Body: UserBody }>('/', async (request, reply) => {
  const userRepo = request.container.resolve<IUserRepository>('IUserRepository');
  const auditLogger = request.container.resolve<IAuditLogger>('IAuditLogger');
  const context = request.container.resolve<IRequestContext>('IRequestContext');

  const user = await userRepo.create(request.body);

  await auditLogger.log('user_created', {
    userId: user.id,
    requestId: context.requestId,
  });

  return reply.status(201).send(user);
});
};

export default usersRoutes;

Define services with dependency injection:

TypeScript src/services/RequestContext.ts
// src/services/RequestContext.ts
import { randomUUID } from 'crypto';

export interface IRequestContext {
requestId: string;
startTime: Date;
userId?: string;
setUserId(userId: string): void;
}

export class RequestContext implements IRequestContext {
requestId = randomUUID();
startTime = new Date();
userId?: string;

setUserId(userId: string) {
  this.userId = userId;
}
}
// src/services/RequestContext.ts
import { randomUUID } from 'crypto';

export interface IRequestContext {
requestId: string;
startTime: Date;
userId?: string;
setUserId(userId: string): void;
}

export class RequestContext implements IRequestContext {
requestId = randomUUID();
startTime = new Date();
userId?: string;

setUserId(userId: string) {
  this.userId = userId;
}
}
TypeScript src/services/AuditLogger.ts
// src/services/AuditLogger.ts
import { inject } from 'ts-ioc-container';
import type { ILogger } from './Logger';
import type { IRequestContext } from './RequestContext';

export interface IAuditLogger {
log(action: string, data: Record<string, unknown>): Promise<void>;
}

export class AuditLogger implements IAuditLogger {
constructor(
  @inject('ILogger') private logger: ILogger,
  @inject('IRequestContext') private context: IRequestContext,
) {}

async log(action: string, data: Record<string, unknown>): Promise<void> {
  this.logger.info({
    type: 'audit',
    action,
    requestId: this.context.requestId,
    userId: this.context.userId,
    timestamp: new Date().toISOString(),
    ...data,
  });
}
}
// src/services/AuditLogger.ts
import { inject } from 'ts-ioc-container';
import type { ILogger } from './Logger';
import type { IRequestContext } from './RequestContext';

export interface IAuditLogger {
log(action: string, data: Record<string, unknown>): Promise<void>;
}

export class AuditLogger implements IAuditLogger {
constructor(
  @inject('ILogger') private logger: ILogger,
  @inject('IRequestContext') private context: IRequestContext,
) {}

async log(action: string, data: Record<string, unknown>): Promise<void> {
  this.logger.info({
    type: 'audit',
    action,
    requestId: this.context.requestId,
    userId: this.context.userId,
    timestamp: new Date().toISOString(),
    ...data,
  });
}
}
TypeScript src/repositories/UserRepository.ts
// src/repositories/UserRepository.ts
import { inject } from 'ts-ioc-container';
import type { IDatabasePool } from '../services/DatabasePool';

export interface User {
id: string;
name: string;
email: string;
}

export interface IUserRepository {
findAll(): Promise<User[]>;
findById(id: string): Promise<User | null>;
create(data: Omit<User, 'id'>): Promise<User>;
}

export class UserRepository implements IUserRepository {
constructor(
  @inject('IDatabasePool') private db: IDatabasePool,
) {}

async findAll(): Promise<User[]> {
  return this.db.query<User>('SELECT * FROM users');
}

async findById(id: string): Promise<User | null> {
  const users = await this.db.query<User>(
    'SELECT * FROM users WHERE id = $1',
    [id]
  );
  return users[0] ?? null;
}

async create(data: Omit<User, 'id'>): Promise<User> {
  const [user] = await this.db.query<User>(
    'INSERT INTO users (name, email) VALUES ($1, $2) RETURNING *',
    [data.name, data.email]
  );
  return user;
}
}
// src/repositories/UserRepository.ts
import { inject } from 'ts-ioc-container';
import type { IDatabasePool } from '../services/DatabasePool';

export interface User {
id: string;
name: string;
email: string;
}

export interface IUserRepository {
findAll(): Promise<User[]>;
findById(id: string): Promise<User | null>;
create(data: Omit<User, 'id'>): Promise<User>;
}

export class UserRepository implements IUserRepository {
constructor(
  @inject('IDatabasePool') private db: IDatabasePool,
) {}

async findAll(): Promise<User[]> {
  return this.db.query<User>('SELECT * FROM users');
}

async findById(id: string): Promise<User | null> {
  const users = await this.db.query<User>(
    'SELECT * FROM users WHERE id = $1',
    [id]
  );
  return users[0] ?? null;
}

async create(data: Omit<User, 'id'>): Promise<User> {
  const [user] = await this.db.query<User>(
    'INSERT INTO users (name, email) VALUES ($1, $2) RETURNING *',
    [data.name, data.email]
  );
  return user;
}
}

Create a plugin for transaction-scoped containers:

TypeScript src/plugins/transaction.ts
// src/plugins/transaction.ts
import { FastifyPluginAsync, FastifyRequest } from 'fastify';
import fp from 'fastify-plugin';
import type { IContainer } from 'ts-ioc-container';
import type { IDatabasePool, IConnection } from '../services/DatabasePool';

declare module 'fastify' {
interface FastifyRequest {
  transactionScope?: IContainer;
  startTransaction(): Promise<IContainer>;
  commitTransaction(): Promise<void>;
  rollbackTransaction(): Promise<void>;
}
}

const transactionPlugin: FastifyPluginAsync = async (fastify) => {
fastify.decorateRequest('transactionScope', null);

fastify.decorateRequest('startTransaction', async function (this: FastifyRequest) {
  const dbPool = this.container.resolve<IDatabasePool>('IDatabasePool');
  const connection = await dbPool.getConnection();

  // Create transaction scope
  const txScope = this.container.createScope({ tags: ['transaction'] });

  // Register connection in transaction scope
  txScope.register('IConnection', () => connection);

  await connection.beginTransaction();

  this.transactionScope = txScope;
  return txScope;
});

fastify.decorateRequest('commitTransaction', async function (this: FastifyRequest) {
  if (!this.transactionScope) {
    throw new Error('No transaction in progress');
  }

  const connection = this.transactionScope.resolve<IConnection>('IConnection');
  await connection.commit();
  this.transactionScope.dispose();
  this.transactionScope = undefined;
});

fastify.decorateRequest('rollbackTransaction', async function (this: FastifyRequest) {
  if (!this.transactionScope) return;

  const connection = this.transactionScope.resolve<IConnection>('IConnection');
  await connection.rollback();
  this.transactionScope.dispose();
  this.transactionScope = undefined;
});

// Auto-rollback on error
fastify.addHook('onError', async (request) => {
  await request.rollbackTransaction();
});
};

export default fp(transactionPlugin, {
name: 'transaction',
dependencies: ['container'],
});
// src/plugins/transaction.ts
import { FastifyPluginAsync, FastifyRequest } from 'fastify';
import fp from 'fastify-plugin';
import type { IContainer } from 'ts-ioc-container';
import type { IDatabasePool, IConnection } from '../services/DatabasePool';

declare module 'fastify' {
interface FastifyRequest {
  transactionScope?: IContainer;
  startTransaction(): Promise<IContainer>;
  commitTransaction(): Promise<void>;
  rollbackTransaction(): Promise<void>;
}
}

const transactionPlugin: FastifyPluginAsync = async (fastify) => {
fastify.decorateRequest('transactionScope', null);

fastify.decorateRequest('startTransaction', async function (this: FastifyRequest) {
  const dbPool = this.container.resolve<IDatabasePool>('IDatabasePool');
  const connection = await dbPool.getConnection();

  // Create transaction scope
  const txScope = this.container.createScope({ tags: ['transaction'] });

  // Register connection in transaction scope
  txScope.register('IConnection', () => connection);

  await connection.beginTransaction();

  this.transactionScope = txScope;
  return txScope;
});

fastify.decorateRequest('commitTransaction', async function (this: FastifyRequest) {
  if (!this.transactionScope) {
    throw new Error('No transaction in progress');
  }

  const connection = this.transactionScope.resolve<IConnection>('IConnection');
  await connection.commit();
  this.transactionScope.dispose();
  this.transactionScope = undefined;
});

fastify.decorateRequest('rollbackTransaction', async function (this: FastifyRequest) {
  if (!this.transactionScope) return;

  const connection = this.transactionScope.resolve<IConnection>('IConnection');
  await connection.rollback();
  this.transactionScope.dispose();
  this.transactionScope = undefined;
});

// Auto-rollback on error
fastify.addHook('onError', async (request) => {
  await request.rollbackTransaction();
});
};

export default fp(transactionPlugin, {
name: 'transaction',
dependencies: ['container'],
});
TypeScript src/routes/orders.ts
// src/routes/orders.ts
import { FastifyPluginAsync } from 'fastify';
import type { IOrderRepository } from '../repositories/OrderRepository';
import type { IInventoryService } from '../services/InventoryService';
import type { IAuditLogger } from '../services/AuditLogger';

interface CreateOrderBody {
items: Array<{ productId: string; quantity: number }>;
}

const ordersRoutes: FastifyPluginAsync = async (fastify) => {
// POST /api/orders - with transaction
fastify.post<{ Body: CreateOrderBody }>('/', async (request, reply) => {
  const txScope = await request.startTransaction();

  try {
    const orderRepo = txScope.resolve<IOrderRepository>('IOrderRepository');
    const inventoryService = txScope.resolve<IInventoryService>('IInventoryService');
    const auditLogger = txScope.resolve<IAuditLogger>('IAuditLogger');

    // All operations within transaction
    const order = await orderRepo.create(request.body);
    await inventoryService.reserve(order.items);
    await auditLogger.log('order_created', { orderId: order.id });

    await request.commitTransaction();

    return reply.status(201).send(order);
  } catch (error) {
    await request.rollbackTransaction();
    throw error;
  }
});
};

export default ordersRoutes;
// src/routes/orders.ts
import { FastifyPluginAsync } from 'fastify';
import type { IOrderRepository } from '../repositories/OrderRepository';
import type { IInventoryService } from '../services/InventoryService';
import type { IAuditLogger } from '../services/AuditLogger';

interface CreateOrderBody {
items: Array<{ productId: string; quantity: number }>;
}

const ordersRoutes: FastifyPluginAsync = async (fastify) => {
// POST /api/orders - with transaction
fastify.post<{ Body: CreateOrderBody }>('/', async (request, reply) => {
  const txScope = await request.startTransaction();

  try {
    const orderRepo = txScope.resolve<IOrderRepository>('IOrderRepository');
    const inventoryService = txScope.resolve<IInventoryService>('IInventoryService');
    const auditLogger = txScope.resolve<IAuditLogger>('IAuditLogger');

    // All operations within transaction
    const order = await orderRepo.create(request.body);
    await inventoryService.reserve(order.items);
    await auditLogger.log('order_created', { orderId: order.id });

    await request.commitTransaction();

    return reply.status(201).send(order);
  } catch (error) {
    await request.rollbackTransaction();
    throw error;
  }
});
};

export default ordersRoutes;

Combine Fastify’s schema validation with dependency injection:

TypeScript src/routes/users-with-schema.ts
// src/routes/users-with-schema.ts
import { FastifyPluginAsync } from 'fastify';
import type { IUserRepository } from '../repositories/UserRepository';
import type { IAuditLogger } from '../services/AuditLogger';

const userSchema = {
type: 'object',
properties: {
  id: { type: 'string' },
  name: { type: 'string' },
  email: { type: 'string', format: 'email' },
},
required: ['name', 'email'],
} as const;

const createUserSchema = {
body: {
  type: 'object',
  properties: {
    name: { type: 'string', minLength: 1 },
    email: { type: 'string', format: 'email' },
  },
  required: ['name', 'email'],
},
response: {
  201: userSchema,
},
} as const;

const usersRoutes: FastifyPluginAsync = async (fastify) => {
fastify.post(
  '/',
  { schema: createUserSchema },
  async (request, reply) => {
    const userRepo = request.container.resolve<IUserRepository>('IUserRepository');
    const auditLogger = request.container.resolve<IAuditLogger>('IAuditLogger');

    const { name, email } = request.body as { name: string; email: string };
    const user = await userRepo.create({ name, email });

    await auditLogger.log('user_created', { userId: user.id });

    return reply.status(201).send(user);
  }
);
};

export default usersRoutes;
// src/routes/users-with-schema.ts
import { FastifyPluginAsync } from 'fastify';
import type { IUserRepository } from '../repositories/UserRepository';
import type { IAuditLogger } from '../services/AuditLogger';

const userSchema = {
type: 'object',
properties: {
  id: { type: 'string' },
  name: { type: 'string' },
  email: { type: 'string', format: 'email' },
},
required: ['name', 'email'],
} as const;

const createUserSchema = {
body: {
  type: 'object',
  properties: {
    name: { type: 'string', minLength: 1 },
    email: { type: 'string', format: 'email' },
  },
  required: ['name', 'email'],
},
response: {
  201: userSchema,
},
} as const;

const usersRoutes: FastifyPluginAsync = async (fastify) => {
fastify.post(
  '/',
  { schema: createUserSchema },
  async (request, reply) => {
    const userRepo = request.container.resolve<IUserRepository>('IUserRepository');
    const auditLogger = request.container.resolve<IAuditLogger>('IAuditLogger');

    const { name, email } = request.body as { name: string; email: string };
    const user = await userRepo.create({ name, email });

    await auditLogger.log('user_created', { userId: user.id });

    return reply.status(201).send(user);
  }
);
};

export default usersRoutes;

Test routes with mock containers:

TypeScript src/routes/users.test.ts
// src/routes/users.test.ts
import Fastify, { FastifyInstance } from 'fastify';
import { Container, MetadataInjector, Registration } from 'ts-ioc-container';
import containerPlugin from '../plugins/container';
import usersRoutes from './users';

describe('Users API', () => {
let app: FastifyInstance;
let mockUserRepo: jest.Mocked<IUserRepository>;
let mockAuditLogger: jest.Mocked<IAuditLogger>;

beforeEach(async () => {
  // Create mocks
  mockUserRepo = {
    findAll: jest.fn(),
    findById: jest.fn(),
    create: jest.fn(),
  };

  mockAuditLogger = {
    log: jest.fn(),
  };

  // Create test container
  const container = new Container(new MetadataInjector())
    .addRegistration(
      Registration.fromValue(mockUserRepo).to('IUserRepository'),
      Registration.fromValue(mockAuditLogger).to('IAuditLogger'),
      Registration.fromValue({ requestId: 'test-123' }).to('IRequestContext'),
    );

  // Build app with test container
  app = Fastify();
  await app.register(containerPlugin, { container });
  await app.register(usersRoutes, { prefix: '/api/users' });
  await app.ready();
});

afterEach(async () => {
  await app.close();
});

describe('GET /api/users', () => {
  it('should return all users', async () => {
    const users = [
      { id: '1', name: 'Alice', email: 'alice@example.com' },
      { id: '2', name: 'Bob', email: 'bob@example.com' },
    ];
    mockUserRepo.findAll.mockResolvedValue(users);

    const response = await app.inject({
      method: 'GET',
      url: '/api/users',
    });

    expect(response.statusCode).toBe(200);
    expect(JSON.parse(response.body)).toEqual(users);
    expect(mockAuditLogger.log).toHaveBeenCalledWith(
      'users_listed',
      { count: 2 }
    );
  });
});

describe('POST /api/users', () => {
  it('should create a new user', async () => {
    const newUser = { id: '3', name: 'Charlie', email: 'charlie@example.com' };
    mockUserRepo.create.mockResolvedValue(newUser);

    const response = await app.inject({
      method: 'POST',
      url: '/api/users',
      payload: { name: 'Charlie', email: 'charlie@example.com' },
    });

    expect(response.statusCode).toBe(201);
    expect(JSON.parse(response.body)).toEqual(newUser);
  });
});

describe('GET /api/users/:id', () => {
  it('should return user by id', async () => {
    const user = { id: '1', name: 'Alice', email: 'alice@example.com' };
    mockUserRepo.findById.mockResolvedValue(user);

    const response = await app.inject({
      method: 'GET',
      url: '/api/users/1',
    });

    expect(response.statusCode).toBe(200);
    expect(JSON.parse(response.body)).toEqual(user);
  });

  it('should return 404 when user not found', async () => {
    mockUserRepo.findById.mockResolvedValue(null);

    const response = await app.inject({
      method: 'GET',
      url: '/api/users/999',
    });

    expect(response.statusCode).toBe(404);
  });
});
});
// src/routes/users.test.ts
import Fastify, { FastifyInstance } from 'fastify';
import { Container, MetadataInjector, Registration } from 'ts-ioc-container';
import containerPlugin from '../plugins/container';
import usersRoutes from './users';

describe('Users API', () => {
let app: FastifyInstance;
let mockUserRepo: jest.Mocked<IUserRepository>;
let mockAuditLogger: jest.Mocked<IAuditLogger>;

beforeEach(async () => {
  // Create mocks
  mockUserRepo = {
    findAll: jest.fn(),
    findById: jest.fn(),
    create: jest.fn(),
  };

  mockAuditLogger = {
    log: jest.fn(),
  };

  // Create test container
  const container = new Container(new MetadataInjector())
    .addRegistration(
      Registration.fromValue(mockUserRepo).to('IUserRepository'),
      Registration.fromValue(mockAuditLogger).to('IAuditLogger'),
      Registration.fromValue({ requestId: 'test-123' }).to('IRequestContext'),
    );

  // Build app with test container
  app = Fastify();
  await app.register(containerPlugin, { container });
  await app.register(usersRoutes, { prefix: '/api/users' });
  await app.ready();
});

afterEach(async () => {
  await app.close();
});

describe('GET /api/users', () => {
  it('should return all users', async () => {
    const users = [
      { id: '1', name: 'Alice', email: 'alice@example.com' },
      { id: '2', name: 'Bob', email: 'bob@example.com' },
    ];
    mockUserRepo.findAll.mockResolvedValue(users);

    const response = await app.inject({
      method: 'GET',
      url: '/api/users',
    });

    expect(response.statusCode).toBe(200);
    expect(JSON.parse(response.body)).toEqual(users);
    expect(mockAuditLogger.log).toHaveBeenCalledWith(
      'users_listed',
      { count: 2 }
    );
  });
});

describe('POST /api/users', () => {
  it('should create a new user', async () => {
    const newUser = { id: '3', name: 'Charlie', email: 'charlie@example.com' };
    mockUserRepo.create.mockResolvedValue(newUser);

    const response = await app.inject({
      method: 'POST',
      url: '/api/users',
      payload: { name: 'Charlie', email: 'charlie@example.com' },
    });

    expect(response.statusCode).toBe(201);
    expect(JSON.parse(response.body)).toEqual(newUser);
  });
});

describe('GET /api/users/:id', () => {
  it('should return user by id', async () => {
    const user = { id: '1', name: 'Alice', email: 'alice@example.com' };
    mockUserRepo.findById.mockResolvedValue(user);

    const response = await app.inject({
      method: 'GET',
      url: '/api/users/1',
    });

    expect(response.statusCode).toBe(200);
    expect(JSON.parse(response.body)).toEqual(user);
  });

  it('should return 404 when user not found', async () => {
    mockUserRepo.findById.mockResolvedValue(null);

    const response = await app.inject({
      method: 'GET',
      url: '/api/users/999',
    });

    expect(response.statusCode).toBe(404);
  });
});
});

Here’s a complete working example:

TypeScript Complete Fastify Example
// Complete Fastify example with ts-ioc-container

import 'reflect-metadata';
import Fastify, { FastifyRequest } from 'fastify';
import fp from 'fastify-plugin';
import {
Container,
MetadataInjector,
Registration,
singleton,
scope,
inject,
} from 'ts-ioc-container';
import type { IContainer } from 'ts-ioc-container';

// ============ Type Definitions ============
declare module 'fastify' {
interface FastifyInstance {
  container: IContainer;
}
interface FastifyRequest {
  container: IContainer;
}
}

// ============ Services ============
interface ILogger {
info(message: string, data?: Record<string, unknown>): void;
}

class Logger implements ILogger {
info(message: string, data?: Record<string, unknown>) {
  console.log(`[INFO] ${message}`, data ?? '');
}
}

interface IUserRepository {
findAll(): Promise<User[]>;
create(data: { name: string; email: string }): Promise<User>;
}

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

class UserRepository implements IUserRepository {
private users: User[] = [
  { id: '1', name: 'Alice', email: 'alice@example.com' },
  { id: '2', name: 'Bob', email: 'bob@example.com' },
];

async findAll() {
  return this.users;
}

async create(data: { name: string; email: string }) {
  const user = { id: String(this.users.length + 1), ...data };
  this.users.push(user);
  return user;
}
}

interface IAuditLogger {
log(action: string, data: Record<string, unknown>): void;
}

class AuditLogger implements IAuditLogger {
constructor(@inject('ILogger') private logger: ILogger) {}

log(action: string, data: Record<string, unknown>) {
  this.logger.info(`AUDIT: ${action}`, data);
}
}

// ============ Container Setup ============
function createAppContainer(): IContainer {
return new Container(new MetadataInjector(), { tags: ['application'] })
  .addRegistration(
    Registration.fromClass(Logger).pipe(singleton()).to('ILogger')
  )
  .addRegistration(
    Registration.fromClass(UserRepository)
      .pipe(scope((s) => s.hasTag('request')))
      .pipe(singleton())
      .to('IUserRepository'),
    Registration.fromClass(AuditLogger)
      .pipe(scope((s) => s.hasTag('request')))
      .pipe(singleton())
      .to('IAuditLogger')
  );
}

// ============ Container Plugin ============
const containerPlugin = fp(async (fastify, opts: { container: IContainer }) => {
fastify.decorate('container', opts.container);
fastify.decorateRequest('container', null);

fastify.addHook('onRequest', async (request) => {
  request.container = opts.container.createScope({ tags: ['request'] });
});

fastify.addHook('onResponse', async (request) => {
  request.container.dispose();
});

fastify.addHook('onClose', async () => {
  opts.container.dispose();
});
});

// ============ Application ============
async function buildApp() {
const fastify = Fastify({ logger: true });
const container = createAppContainer();

await fastify.register(containerPlugin, { container });

// Routes
fastify.get('/users', async (request) => {
  const userRepo = request.container.resolve<IUserRepository>('IUserRepository');
  const auditLogger = request.container.resolve<IAuditLogger>('IAuditLogger');

  const users = await userRepo.findAll();
  auditLogger.log('users_listed', { count: users.length });

  return users;
});

fastify.post<{ Body: { name: string; email: string } }>('/users', async (request, reply) => {
  const userRepo = request.container.resolve<IUserRepository>('IUserRepository');
  const auditLogger = request.container.resolve<IAuditLogger>('IAuditLogger');

  const user = await userRepo.create(request.body);
  auditLogger.log('user_created', { userId: user.id });

  return reply.status(201).send(user);
});

return fastify;
}

// ============ Start Server ============
buildApp().then((app) => {
app.listen({ port: 3000 }, (err) => {
  if (err) {
    app.log.error(err);
    process.exit(1);
  }
});
});
// Complete Fastify example with ts-ioc-container

import 'reflect-metadata';
import Fastify, { FastifyRequest } from 'fastify';
import fp from 'fastify-plugin';
import {
Container,
MetadataInjector,
Registration,
singleton,
scope,
inject,
} from 'ts-ioc-container';
import type { IContainer } from 'ts-ioc-container';

// ============ Type Definitions ============
declare module 'fastify' {
interface FastifyInstance {
  container: IContainer;
}
interface FastifyRequest {
  container: IContainer;
}
}

// ============ Services ============
interface ILogger {
info(message: string, data?: Record<string, unknown>): void;
}

class Logger implements ILogger {
info(message: string, data?: Record<string, unknown>) {
  console.log(`[INFO] ${message}`, data ?? '');
}
}

interface IUserRepository {
findAll(): Promise<User[]>;
create(data: { name: string; email: string }): Promise<User>;
}

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

class UserRepository implements IUserRepository {
private users: User[] = [
  { id: '1', name: 'Alice', email: 'alice@example.com' },
  { id: '2', name: 'Bob', email: 'bob@example.com' },
];

async findAll() {
  return this.users;
}

async create(data: { name: string; email: string }) {
  const user = { id: String(this.users.length + 1), ...data };
  this.users.push(user);
  return user;
}
}

interface IAuditLogger {
log(action: string, data: Record<string, unknown>): void;
}

class AuditLogger implements IAuditLogger {
constructor(@inject('ILogger') private logger: ILogger) {}

log(action: string, data: Record<string, unknown>) {
  this.logger.info(`AUDIT: ${action}`, data);
}
}

// ============ Container Setup ============
function createAppContainer(): IContainer {
return new Container(new MetadataInjector(), { tags: ['application'] })
  .addRegistration(
    Registration.fromClass(Logger).pipe(singleton()).to('ILogger')
  )
  .addRegistration(
    Registration.fromClass(UserRepository)
      .pipe(scope((s) => s.hasTag('request')))
      .pipe(singleton())
      .to('IUserRepository'),
    Registration.fromClass(AuditLogger)
      .pipe(scope((s) => s.hasTag('request')))
      .pipe(singleton())
      .to('IAuditLogger')
  );
}

// ============ Container Plugin ============
const containerPlugin = fp(async (fastify, opts: { container: IContainer }) => {
fastify.decorate('container', opts.container);
fastify.decorateRequest('container', null);

fastify.addHook('onRequest', async (request) => {
  request.container = opts.container.createScope({ tags: ['request'] });
});

fastify.addHook('onResponse', async (request) => {
  request.container.dispose();
});

fastify.addHook('onClose', async () => {
  opts.container.dispose();
});
});

// ============ Application ============
async function buildApp() {
const fastify = Fastify({ logger: true });
const container = createAppContainer();

await fastify.register(containerPlugin, { container });

// Routes
fastify.get('/users', async (request) => {
  const userRepo = request.container.resolve<IUserRepository>('IUserRepository');
  const auditLogger = request.container.resolve<IAuditLogger>('IAuditLogger');

  const users = await userRepo.findAll();
  auditLogger.log('users_listed', { count: users.length });

  return users;
});

fastify.post<{ Body: { name: string; email: string } }>('/users', async (request, reply) => {
  const userRepo = request.container.resolve<IUserRepository>('IUserRepository');
  const auditLogger = request.container.resolve<IAuditLogger>('IAuditLogger');

  const user = await userRepo.create(request.body);
  auditLogger.log('user_created', { userId: user.id });

  return reply.status(201).send(user);
});

return fastify;
}

// ============ Start Server ============
buildApp().then((app) => {
app.listen({ port: 3000 }, (err) => {
  if (err) {
    app.log.error(err);
    process.exit(1);
  }
});
});

Scope Naming Convention

ScopeTagUse Case
ApplicationapplicationGlobal singletons (pools, config, loggers)
RequestrequestPer-request services (context, repos)
TransactiontransactionDatabase transaction boundaries

Memory Management

Fastify-Specific Patterns

  1. Use fastify-plugin to properly encapsulate plugins
  2. Decorate instance and request for type-safe access
  3. Register plugins in dependency order (container before transaction)
  4. Use Fastify’s injection system with app.inject() for testing

Container Plugin

interface ContainerPluginOptions {
  container: IContainer;
}

// Register with:
await fastify.register(containerPlugin, { container });

Request Decorations

// Access container in route handlers
request.container.resolve<T>(key)

// Transaction methods (with transaction plugin)
await request.startTransaction()
await request.commitTransaction()
await request.rollbackTransaction()

Instance Decorations

// Access app container
fastify.container.resolve<T>(key)