Interfaces vs Classes in NestJS
NestJS leans heavily on TypeScript’s type system, but it also leans on runtime reflection — and that is where the distinction between an interface and a class becomes a hard rule rather than a matter of taste. Interfaces are a pure compile-time construct: they vanish during transpilation and leave no trace in the emitted JavaScript. Classes, by contrast, compile to real constructor functions that can carry decorator metadata, participate in dependency injection, and be inspected by class-validator and Swagger. This page explains exactly where each belongs so you stop fighting the framework.
The erasure rule
Every interface and type alias is deleted by the compiler. Try to use one as a runtime value and the build fails, because there is nothing to reference once the types are stripped.
interface UserShape {
id: number;
email: string;
}
// ❌ Error: 'UserShape' only refers to a type, but is being used as a value here
const fields = Object.keys(UserShape);
Because Nest’s ValidationPipe, ParsePipe, and @nestjs/swagger all read metadata off a constructor at runtime, an interface simply has nothing for them to read. The framework cannot validate, transform, or document a type that no longer exists.
Where classes are required
Anything Nest needs to construct, inject, or introspect must be a class. The table below maps the common building blocks to the construct they demand.
| Construct | Use | Why |
|---|---|---|
DTOs (@Body(), @Query()) | class | validated & transformed at runtime |
| Entities (TypeORM/Prisma models) | class | decorators map columns at runtime |
| Providers / services | class | instantiated by the DI container |
| Custom pipes, guards, interceptors | class | implement an interface, instantiated by Nest |
| Plain data contracts (no runtime use) | interface | erased, zero cost |
| Function/option shapes | interface or type | compile-time only |
The pattern is consistent: if Nest’s runtime touches it, it is a class. If only the compiler touches it, an interface is lighter and clearer.
DTOs and entities must be classes
A DTO that is declared as an interface compiles fine but silently disables validation — the ValidationPipe receives no metadata and lets every payload through.
import { IsEmail, IsInt, Min } from "class-validator";
// ✅ Real constructor — decorators have somewhere to live
export class CreateUserDto {
@IsEmail()
email!: string;
@IsInt()
@Min(18)
age!: number;
}
The decorators above attach metadata to CreateUserDto.prototype. If you swapped class for interface, the decorators would be a syntax error, which is TypeScript saving you from a subtle runtime bug.
Where interfaces shine
Interfaces are perfect for contracts that live only in your type-checking — service abstractions, configuration shapes, and the signatures Nest itself asks you to implement, such as OnModuleInit or NestInterceptor.
// A pure abstraction, no runtime footprint
export interface PaymentGateway {
charge(amountCents: number, token: string): Promise<{ id: string }>;
}
import { Injectable, OnModuleInit } from "@nestjs/common";
@Injectable()
export class StripeGateway implements PaymentGateway, OnModuleInit {
onModuleInit() {
/* connect SDK */
}
async charge(amountCents: number, token: string) {
return { id: "ch_123" };
}
}
Here the interface defines the contract and the class implements it. The interface can later be used as an injection token via a string/symbol token (see Custom Types & Providers), letting you swap StripeGateway for a fake in tests without changing consumers.
Interfaces cannot be used directly as DI tokens because they are erased. To inject “any
PaymentGateway”, pair the interface with a string orSymboltoken and provide the concrete class against it.
implements keeps classes honest
When a class implements an interface, TypeScript verifies the class actually satisfies the contract, but it does not inherit anything — implements is purely a compile-time check, unlike extends.
import { CanActivate, ExecutionContext } from "@nestjs/common";
export class RolesGuard implements CanActivate {
// ❌ Error: Property 'canActivate' is missing in type 'RolesGuard'
}
Removing the method gives an immediate, precise error. This is why Nest ships interfaces like CanActivate, PipeTransform, and ExceptionFilter: you get a runtime class plus compile-time guarantees that it fulfils the framework’s expectations.
Best Practices
- Make every DTO and entity a class so validation, transformation, and ORM mapping work at runtime.
- Use interfaces for service abstractions, config shapes, and Nest lifecycle/contract interfaces.
- Implement Nest interfaces (
CanActivate,PipeTransform, etc.) withimplementsfor compile-time safety. - Remember
implementsonly checks shape; useextendswhen you actually need inheritance. - Never try to use an interface as a value, a DI token, or a
@Body()type — it is erased. - Pair an interface with a string/
Symboltoken when you want to inject against an abstraction. - Keep classes free of unnecessary runtime weight: if nothing inspects it, an interface is cheaper.