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);
}
}
| Pipe | Input | Output type | On failure |
|---|---|---|---|
ParseIntPipe | "42" | number | 400 Bad Request |
ParseBoolPipe | "true" | boolean | 400 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: numberwould 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 viaextends. - 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
anyin generic code; a constrained type parameter preserves safety end to end. - Keep DI decorators (
@Injectable()) on the concrete subclass, not the abstract base.