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.
| Concern | app.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 lives | main.ts, near NestFactory.create | Any 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 defaultIn a hybrid app,
app.useGlobalGuards()/useGlobalPipes()/ etc. apply only to the HTTP layer. Microservice listeners and WebSocket gateways stay uncovered. Two fixes:
- Pass
{ inheritAppConfig: true }when callingapp.connectMicroservice(...).- Register via
APP_GUARD(orAPP_PIPE/APP_INTERCEPTOR/APP_FILTER). Provider-bound globals cover every transport without extra flags.
Picking between useClass, useValue, and useFactory
| Form | When to reach for it |
|---|---|
useClass | Your component has a constructor and Nest can resolve every dep from the container. |
useValue | You need a pre-built instance with literal options (e.g., new ValidationPipe({…})). |
useFactory | Construction 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., aLoggingInterceptorplus aTimeoutInterceptor). Order follows module resolution order. useGlobalPipesand anAPP_PIPEprovider together both apply. Provider-bound runs first because it’s resolved during module initialization; theuseGlobalXinstance 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). TheuseGlobalXform bypasses the testing module entirely.
See also
- Pipes: binding scopes table.
- Guards:
APP_GUARDis the default for auth. - Interceptors: same registration story.
- Exception filters:
APP_FILTERfor global error handling. - Validation recipe: the most common reason to reach for
APP_PIPE. - Official docs: Hybrid application.