Skip to content
TypeScript ts nestjs 4 min read

Generics in NestJS

Generics let you write a service or repository once and reuse it for any entity while keeping full type safety — no any, no casts. In a NestJS codebase the most common payoff is a base CRUD service or a generic repository that every feature module extends, plus strongly typed wrappers around responses and pagination. This page shows how to apply TypeScript generics inside Nest’s class-based, decorator-driven world, where they coexist with runtime concerns like dependency injection and parameter pipes such as ParseIntPipe.

A generic base service

A base service captures the operations shared by every entity and leaves the concrete type as a parameter. Subclasses fill in the type, and TypeScript propagates it through every method.

export abstract class BaseService<T extends { id: number }> {
  protected items: T[] = [];

  findAll(): T[] {
    return this.items;
  }

  findOne(id: number): T | undefined {
    return this.items.find((item) => item.id === id);
  }

  create(entity: T): T {
    this.items.push(entity);
    return entity;
  }
}

The constraint T extends { id: number } guarantees every entity has a numeric id, so findOne can compare against it safely. Without the constraint, item.id would be a type error because a bare T has no known properties.

Extending the base in a feature service

A feature service extends the base with its concrete entity type. All inherited methods are now specialised — findAll() returns User[], not unknown[].

import { Injectable } from "@nestjs/common";

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

@Injectable()
export class UsersService extends BaseService<User> {
  findByEmail(email: string): User | undefined {
    return this.items.find((u) => u.email === email);
  }
}

Because BaseService is abstract, it cannot be instantiated directly and only serves as a reusable parent. The @Injectable() decorator on the subclass is what registers UsersService with the DI container.

A generic repository contract

Generics shine in repository interfaces, where one signature describes the data access for any model. The interface is a compile-time contract; concrete repositories implement it per entity.

export interface Repository<T, ID = number> {
  find(id: ID): Promise<T | null>;
  findAll(): Promise<T[]>;
  save(entity: T): Promise<T>;
  delete(id: ID): Promise<void>;
}

The second parameter ID = number is a default type parameter: callers usually omit it and get a numeric key, but a model using UUID strings can write Repository<Order, string>. Defaults keep the common case terse without losing flexibility.

Generic response wrappers

API responses often share an envelope — data plus metadata. A generic type models the envelope once and reuses it for every payload shape.

export interface ApiResponse<T> {
  data: T;
  meta: { page: number; total: number };
}

@Injectable()
export class UsersService {
  list(): ApiResponse<User[]> {
    return { data: [], meta: { page: 1, total: 0 } };
  }
}

ApiResponse<User[]> and ApiResponse<Order> are distinct, fully typed shapes from a single declaration. Consumers get autocomplete on .data matching the exact inner type.

Generics meet runtime pipes: ParseIntPipe

Generics handle compile-time types, but route parameters arrive as strings at runtime. Nest’s built-in ParseIntPipe bridges the gap: it converts and validates the value, and its return type narrows the parameter to number.

import { Controller, Get, Param, ParseIntPipe } from "@nestjs/common";

@Controller("users")
export class UsersController {
  constructor(private readonly users: UsersService) {}

  @Get(":id")
  findOne(@Param("id", ParseIntPipe) id: number) {
    // id is a real number here; "abc" → 400 before this runs
    return this.users.findOne(id);
  }
}
PipeInputOutput typeOn failure
ParseIntPipe"42"number400 Bad Request
ParseBoolPipe"true"boolean400 Bad Request
ParseUUIDPipe"a1b2..."string (validated)400 Bad Request
ParseArrayPipe"a,b,c"typed T[]400 Bad Request

Type annotations alone never coerce values — @Param("id") id: number would still be a string at runtime despite the type. Always add a parse pipe to make the runtime value match the static type.

Best Practices

  • Constrain generic entity parameters (T extends { id: number }) so shared methods can use known properties.
  • Mark reusable base services abstract; let feature services supply the concrete type via extends.
  • Use default type parameters (ID = number) to keep the common case clean while allowing overrides.
  • Model API envelopes and pagination with a single generic interface instead of repeating shapes.
  • Pair generic types with parse pipes (ParseIntPipe) so runtime values actually match the static types.
  • Avoid any in generic code; a constrained type parameter preserves safety end to end.
  • Keep DI decorators (@Injectable()) on the concrete subclass, not the abstract base.
Last updated June 29, 2026
Was this helpful?