Skip to content
TypeScript ts nestjs 4 min read

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.

ConstructUseWhy
DTOs (@Body(), @Query())classvalidated & transformed at runtime
Entities (TypeORM/Prisma models)classdecorators map columns at runtime
Providers / servicesclassinstantiated by the DI container
Custom pipes, guards, interceptorsclassimplement an interface, instantiated by Nest
Plain data contracts (no runtime use)interfaceerased, zero cost
Function/option shapesinterface or typecompile-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 or Symbol token 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.) with implements for compile-time safety.
  • Remember implements only checks shape; use extends when 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/Symbol token when you want to inject against an abstraction.
  • Keep classes free of unnecessary runtime weight: if nothing inspects it, an interface is cheaper.
Last updated June 29, 2026
Was this helpful?