Two ways to register a global pipe / guard / interceptor / exception filter. They look interchangeable. They are not.

The two registrations

1. Bound on the application instance

// main.ts
const app = await NestFactory.create(AppModule)
app.useGlobalPipes(new ValidationPipe({ whitelist: true }))
app.useGlobalGuards(new AuthGuard())
app.useGlobalInterceptors(new LoggingInterceptor())
app.useGlobalFilters(new HttpExceptionFilter())
await app.listen(3000)

The component is new’d outside the DI container. Nest never sees its constructor. Whatever you pass is what you get.

2. Registered as a provider with an APP_* token

// app.module.ts
import { APP_PIPE, APP_GUARD, APP_INTERCEPTOR, APP_FILTER } from "@nestjs/core"
 
@Module({
  providers: [
    { provide: APP_PIPE, useClass: ValidationPipe },
    { provide: APP_GUARD, useClass: AuthGuard },
    { provide: APP_INTERCEPTOR, useClass: LoggingInterceptor },
    { provide: APP_FILTER, useClass: HttpExceptionFilter },
  ],
})
export class AppModule {}

The container instantiates the component, so it can inject other providers, run with request scope, and play nicely with hybrid apps. The provider can be declared in any module: Nest hoists APP_* registrations to the application root.

Side-by-side

useGlobal* is the shortcut: instantiate it yourself, no DI, no surprises. APP_* is the DI-aware version: the container builds it, so it can inject providers, take request scope, and apply to hybrid apps. Same effect on the wire; different powers under the hood.

Concernapp.useGlobalX(new T()){ provide: APP_X, useClass: T }
Can inject providers (ConfigService, repositories, loggers)
Supports request scope
Applies to gateways/microservices in hybrid apps❌ unless inheritAppConfig: true
Where it livesmain.ts, near NestFactory.createAny module’s providers array
Can pass options as a literal object✅ trivially⚠️ via useValue or useFactory

Rule of thumb: stateless component + static config → either works. Needs DI or request scope → APP_* provider.

Worked example: when useGlobalPipes() is enough

A stock ValidationPipe with literal options. No injection needed:

// main.ts
import { NestFactory } from "@nestjs/core"
import { ValidationPipe } from "@nestjs/common"
import { AppModule } from "./app.module"
 
async function bootstrap() {
  const app = await NestFactory.create(AppModule)
  app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }))
  await app.listen(3000)
}
bootstrap()

Worked example: when you must use APP_PIPE

Suppose whitelist should be on in production but off locally so QA can post extra debug fields. The flag lives in ConfigService, which is a provider:

// validation.config.ts
import { ValidationPipe } from "@nestjs/common"
import { ConfigService } from "@nestjs/config"
import { APP_PIPE } from "@nestjs/core"
 
export const validationPipeProvider = {
  provide: APP_PIPE,
  inject: [ConfigService],
  useFactory: (config: ConfigService) =>
    new ValidationPipe({
      whitelist: config.get<boolean>("STRICT_VALIDATION", true),
      transform: true,
    }),
}
// app.module.ts
import { Module } from "@nestjs/common"
import { ConfigModule } from "@nestjs/config"
import { validationPipeProvider } from "./validation.config"
 
@Module({
  imports: [ConfigModule.forRoot()],
  providers: [validationPipeProvider],
})
export class AppModule {}

With useGlobalPipes(new ValidationPipe({ whitelist: ??? })) in main.ts, ConfigService isn’t resolvable yet: the app instance exists but you’d be injecting by hand (app.get(ConfigService)). The APP_PIPE provider lets Nest wire it for you.

Worked example: a guard that reads the current request

Request-scoped components only work when the container constructs them. A TenantGuard that depends on the inbound Request:

// tenant.guard.ts
import { CanActivate, ExecutionContext, Inject, Injectable, Scope } from "@nestjs/common"
import { REQUEST } from "@nestjs/core"
import type { Request } from "express"
import { TenantService } from "./tenant.service"
 
@Injectable({ scope: Scope.REQUEST })
export class TenantGuard implements CanActivate {
  constructor(
    @Inject(REQUEST) private readonly req: Request,
    private readonly tenants: TenantService,
  ) {}
 
  async canActivate(_: ExecutionContext): Promise<boolean> {
    const tenantId = this.req.header("x-tenant-id")
    return tenantId ? this.tenants.exists(tenantId) : false
  }
}
// app.module.ts
import { Module } from "@nestjs/common"
import { APP_GUARD } from "@nestjs/core"
import { TenantGuard } from "./tenant.guard"
import { TenantService } from "./tenant.service"
 
@Module({
  providers: [
    TenantService,
    { provide: APP_GUARD, useClass: TenantGuard },
  ],
})
export class AppModule {}

app.useGlobalGuards(new TenantGuard(/* what do I pass here? */)) is unreachable: there is no request to inject, and TenantService lives in the container.

Worked example: an interceptor that reads config

A common case: an interceptor whose behavior depends on a runtime flag from ConfigService.

// audit.interceptor.ts
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from "@nestjs/common"
import { ConfigService } from "@nestjs/config"
 
@Injectable()
export class AuditInterceptor implements NestInterceptor {
  constructor(private readonly config: ConfigService) {}
 
  intercept(ctx: ExecutionContext, next: CallHandler) {
    if (!this.config.get<boolean>("AUDIT_ENABLED")) return next.handle()
    // …log to your audit sink
    return next.handle()
  }
}
// app.module.ts
import { Module } from "@nestjs/common"
import { APP_INTERCEPTOR } from "@nestjs/core"
import { AuditInterceptor } from "./audit.interceptor"
 
@Module({
  providers: [
    { provide: APP_INTERCEPTOR, useClass: AuditInterceptor },
  ],
})
export class AppModule {}

app.useGlobalInterceptors(new AuditInterceptor(/* ??? */)) is unreachable: you’d be calling new yourself with no ConfigService in scope. Rule of thumb: if the interceptor has any constructor dependency, use APP_INTERCEPTOR. Same logic applies to a guard with Reflector plus an injected UsersService, a filter that needs the request-scoped Logger, and so on.

Hybrid apps gotcha

app.useGlobalX() does not cover gateways or microservice transports by default

In a hybrid app, app.useGlobalGuards() / useGlobalPipes() / etc. apply only to the HTTP layer. Microservice listeners and WebSocket gateways stay uncovered. Two fixes:

  1. Pass { inheritAppConfig: true } when calling app.connectMicroservice(...).
  2. Register via APP_GUARD (or APP_PIPE / APP_INTERCEPTOR / APP_FILTER). Provider-bound globals cover every transport without extra flags.

Picking between useClass, useValue, and useFactory

FormWhen to reach for it
useClassYour component has a constructor and Nest can resolve every dep from the container.
useValueYou need a pre-built instance with literal options (e.g., new ValidationPipe({…})).
useFactoryConstruction depends on async work, env vars, or providers you must inject manually.
// useClass: most common
{ provide: APP_INTERCEPTOR, useClass: LoggingInterceptor }
 
// useValue: keeps pipe options inline
{ provide: APP_PIPE, useValue: new ValidationPipe({ whitelist: true }) }
 
// useFactory: pulls config from another provider
{
  provide: APP_FILTER,
  inject: [Logger],
  useFactory: (logger: Logger) => new SentryFilter(logger, { release: process.env.RELEASE }),
}

Edge cases worth knowing

  • Multiple registrations stack. You can register the same APP_* token more than once across modules; all of them run. Good for layering (e.g., a LoggingInterceptor plus a TimeoutInterceptor). Order follows module resolution order.
  • useGlobalPipes and an APP_PIPE provider together both apply. Provider-bound runs first because it’s resolved during module initialization; the useGlobalX instance runs on top. Avoid mixing unless you have a reason.
  • No need to export APP_* providers. They aren’t consumed by other modules; the container picks them up by token automatically.
  • Testing. Provider-bound globals are visible to Test.createTestingModule(...), so you can override them with .overrideProvider(APP_GUARD).useClass(MockGuard). The useGlobalX form bypasses the testing module entirely.

See also