Injection Tokens and @Inject

Tokens that are Not Classes

When the provider key is a string, a symbol or an interface, NestJS cannot guess it from the type. Use @Inject(TOKEN) on the constructor parameter.

src/products/products.service.ts

import { Inject, Injectable } from '@nestjs/common';
import { GST_RATE } from '../payments/payments.module';

export interface ProductRepository {
  findById(id: number): Promise<{ id: number; price: number } | null>;
}
export const PRODUCT_REPOSITORY = Symbol('PRODUCT_REPOSITORY');

@Injectable()
export class ProductsService {
  constructor(
    @Inject(PRODUCT_REPOSITORY) private readonly repo: ProductRepository,   // interface + token
    @Inject(GST_RATE) private readonly gstRate: number,
  ) {}

  async priceWithGst(id: number) {
    const product = await this.repo.findById(id);
    return product ? Math.round(product.price * (1 + this.gstRate)) : null;
  }
}

Tip: Prefer Symbol('NAME') or exported constants over raw strings. A typo in a string token is only found when the app starts.

Programming to an Interface

Example

Tokens let you depend on an interface and choose the implementation in the module. Here the service works with either repository:

interface ProductRepository {
  findById(id: number): { id: number; price: number } | null;
}

class InMemoryProductRepository implements ProductRepository {
  private rows = [{ id: 7, price: 1299 }];
  findById(id: number) { return this.rows.find((r) => r.id === id) ?? null; }
}

class CachedProductRepository implements ProductRepository {
  private cache = new Map<number, { id: number; price: number } | null>();
  constructor(private inner: ProductRepository) {}
  findById(id: number) {
    if (!this.cache.has(id)) {
      console.log('  cache miss for', id);
      this.cache.set(id, this.inner.findById(id));
    }
    return this.cache.get(id) ?? null;
  }
}

class ProductsService {
  constructor(private repo: ProductRepository, private gstRate: number) {}
  priceWithGst(id: number) {
    const p = this.repo.findById(id);
    return p ? Math.round(p.price * (1 + this.gstRate)) : null;
  }
}

const plain = new ProductsService(new InMemoryProductRepository(), 0.18);
console.log('plain:', plain.priceWithGst(7), plain.priceWithGst(99));

const cached = new ProductsService(new CachedProductRepository(new InMemoryProductRepository()), 0.18);
console.log('cached:', cached.priceWithGst(7));
console.log('cached:', cached.priceWithGst(7));

Output

plain: 1533 null
  cache miss for 7
cached: 1533
cached: 1533