How to Block AI Bots on NestJS: Complete 2026 Guide
NestJS is an opinionated, decorator-based TypeScript framework with a distinct execution pipeline: Middleware → Guards → Interceptors → Pipes → Handler. The correct layer for AI bot blocking is Middleware — it fires before Guards and before NestJS resolves DI providers, making it the most efficient interception point. This guide covers every approach from a single app.use() call in main.ts through NestMiddleware, Guards, and Interceptors.
NestJS 10 / 11
This guide targets NestJS 10+ (Express adapter, the default). All examples work on NestJS 11. Fastify adapter notes are included where the API differs. TypeScript 5.x with strict mode is assumed throughout.
Methods at a glance
| Method | What it does | Blocks JS-less bots? |
|---|---|---|
| @nestjs/serve-static public/ | Signals crawlers to stay out | Signal only |
| GET /robots.txt controller | Dynamic robots.txt with env rules | Signal only |
| noai meta tag in template | Opt out of AI training per page | ✓ (server-rendered) |
| X-Robots-Tag via Interceptor | noai on all HTTP responses | ✓ (header) |
| app.use() in main.ts | Hard 403 globally — before NestJS routing | ✓ |
| NestMiddleware + configure() | Hard 403 via NestJS DI-aware middleware | ✓ |
| CanActivate Guard (global) | Hard 403 via Guard — after route matching | ✓ |
| nginx map block | Hard 403 at reverse proxy layer | ✓ |
NestJS request pipeline
Understanding where each layer fires determines the right tool for bot blocking.
1. robots.txt — @nestjs/serve-static
Register ServeStaticModule in AppModule. It serves all files in your public/ directory at the root path. public/robots.txt becomes accessible at /robots.txt with no controller needed.
public/robots.txt
User-agent: GPTBot
Disallow: /
User-agent: ChatGPT-User
Disallow: /
User-agent: OAI-SearchBot
Disallow: /
User-agent: ClaudeBot
Disallow: /
User-agent: Claude-Web
Disallow: /
User-agent: anthropic-ai
Disallow: /
User-agent: Google-Extended
Disallow: /
User-agent: Bytespider
Disallow: /
User-agent: CCBot
Disallow: /
User-agent: PerplexityBot
Disallow: /
User-agent: Applebot-Extended
Disallow: /
User-agent: *
Allow: /app.module.ts — ServeStaticModule
import { Module } from '@nestjs/common';
import { ServeStaticModule } from '@nestjs/serve-static';
import { join } from 'node:path';
@Module({
imports: [
ServeStaticModule.forRoot({
rootPath: join(__dirname, '..', 'public'),
// Serve at root path — /robots.txt, /favicon.ico, etc.
serveRoot: '/',
// Don't interfere with API routes
exclude: ['/api/(.*)'],
}),
// ... other modules
],
})
export class AppModule {}
// Install: npm install @nestjs/serve-staticAlternative: dedicated controller
import { Controller, Get, Res } from '@nestjs/common';
import { Response } from 'express';
import { ConfigService } from '@nestjs/config';
const ROBOTS_PROD = `User-agent: GPTBot
Disallow: /
User-agent: ChatGPT-User
Disallow: /
User-agent: OAI-SearchBot
Disallow: /
User-agent: ClaudeBot
Disallow: /
User-agent: anthropic-ai
Disallow: /
User-agent: Google-Extended
Disallow: /
User-agent: Bytespider
Disallow: /
User-agent: CCBot
Disallow: /
User-agent: PerplexityBot
Disallow: /
User-agent: *
Allow: /`;
const ROBOTS_DEV = 'User-agent: *\nDisallow: /';
@Controller('robots.txt')
export class RobotsController {
constructor(private readonly config: ConfigService) {}
@Get()
robots(@Res() res: Response): void {
const isProd = this.config.get<string>('NODE_ENV') === 'production';
res.type('text/plain').send(isProd ? ROBOTS_PROD : ROBOTS_DEV);
}
}2. app.use() in main.ts — simplest global block
NestJS runs on Express by default. app.use() in main.ts registers an Express middleware that fires before any NestJS routing or DI resolution — the earliest possible interception point.
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import type { Request, Response, NextFunction } from 'express';
const BLOCKED_UAS =
/GPTBot|ChatGPT-User|OAI-SearchBot|ClaudeBot|Claude-Web|anthropic-ai|Google-Extended|Bytespider|CCBot|PerplexityBot|Applebot-Extended/i;
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Register BEFORE app.listen() — fires before all NestJS logic
app.use((req: Request, res: Response, next: NextFunction) => {
// Always allow robots.txt
if (req.path === '/robots.txt') return next();
const ua = req.headers['user-agent'] ?? '';
if (BLOCKED_UAS.test(ua)) {
return res.status(403).type('text/plain').send('Forbidden');
}
next();
});
await app.listen(3000);
}
bootstrap();When to use app.use() vs NestMiddleware
app.use() in main.ts is simpler and fires slightly earlier. Use it when you don't need NestJS DI (injecting services, config) inside the middleware. Use NestMiddleware (section 3) when you need access to ConfigService, LoggerService, or other injected providers.
3. NestMiddleware — DI-aware middleware
NestMiddleware is the NestJS way to write middleware — it participates in the dependency injection system, so you can inject ConfigService or any provider. Register it in AppModule.configure() using the MiddlewareConsumer.
middleware/block-ai-bots.middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common';
import type { Request, Response, NextFunction } from 'express';
const BLOCKED_UAS =
/GPTBot|ChatGPT-User|OAI-SearchBot|ClaudeBot|Claude-Web|anthropic-ai|Google-Extended|Bytespider|CCBot|PerplexityBot|Applebot-Extended/i;
@Injectable()
export class BlockAiBotsMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction): void {
// Exempt robots.txt before UA check
if (req.path === '/robots.txt') {
return next();
}
const ua = req.headers['user-agent'] ?? '';
if (BLOCKED_UAS.test(ua)) {
res.status(403).type('text/plain').send('Forbidden');
return;
}
next();
}
}app.module.ts — configure() registration
import { MiddlewareConsumer, Module, NestModule, RequestMethod } from '@nestjs/common';
import { BlockAiBotsMiddleware } from './middleware/block-ai-bots.middleware';
@Module({
// ... imports, controllers, providers
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer): void {
consumer
.apply(BlockAiBotsMiddleware)
// Apply to all routes except /robots.txt
.exclude({ path: 'robots.txt', method: RequestMethod.GET })
// Apply to all other paths
.forRoutes('*');
}
}.exclude() is an alternative to the path check inside the middleware. Both work — the inline check is slightly more explicit about intent.
4. Guard (CanActivate) — per-controller blocking
Guards fire after route matching — later than Middleware but with full access to the ExecutionContext (route metadata, decorators). Use Guards when you need to block AI bots only on specific controllers and want to use the decorator pattern.
guards/block-ai-bots.guard.ts
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { HttpException, HttpStatus } from '@nestjs/common';
import type { Request } from 'express';
const BLOCKED_UAS =
/GPTBot|ChatGPT-User|OAI-SearchBot|ClaudeBot|Claude-Web|anthropic-ai|Google-Extended|Bytespider|CCBot|PerplexityBot|Applebot-Extended/i;
@Injectable()
export class BlockAiBotsGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest<Request>();
const ua = request.headers['user-agent'] ?? '';
if (BLOCKED_UAS.test(ua)) {
throw new HttpException('Forbidden', HttpStatus.FORBIDDEN);
}
return true;
}
}Apply globally in main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { BlockAiBotsGuard } from './guards/block-ai-bots.guard';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Global guard — applies to all controllers
// Note: fires after route matching, unlike Middleware
app.useGlobalGuards(new BlockAiBotsGuard());
await app.listen(3000);
}
bootstrap();Apply per-controller with decorator
import { Controller, Get, UseGuards } from '@nestjs/common';
import { BlockAiBotsGuard } from '../guards/block-ai-bots.guard';
@Controller('api')
@UseGuards(BlockAiBotsGuard) // Only this controller is protected
export class ApiController {
@Get('content')
getContent() {
return { data: 'protected' };
}
}
// No guard on this controller — public
@Controller()
export class AppController {
@Get()
home() {
return 'Hello';
}
}5. Interceptor — X-Robots-Tag response header
Interceptors wrap the handler execution and can modify responses. Use an Interceptor to add X-Robots-Tag to all responses — it fires after the handler, so it sees the full response before it's sent.
interceptors/x-robots-tag.interceptor.ts
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { Observable, tap } from 'rxjs';
import type { Response } from 'express';
@Injectable()
export class XRobotsTagInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
return next.handle().pipe(
tap(() => {
const response = context.switchToHttp().getResponse<Response>();
const request = context.switchToHttp().getRequest();
// Don't set X-Robots-Tag on robots.txt itself
if (request.path !== '/robots.txt') {
response.setHeader('X-Robots-Tag', 'noai, noimageai');
}
})
);
}
}Register globally in main.ts
app.useGlobalInterceptors(new XRobotsTagInterceptor());6. noai meta tag with @nestjs/view
NestJS supports server-side rendering via @nestjs/view with Handlebars, Pug, or EJS. Pass a blockAiTraining flag from your controller to the template.
views/layout.hbs — Handlebars
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
{{#if blockAiTraining}}
<meta name="robots" content="noai, noimageai">
{{/if}}
<title>{{title}}</title>
</head>
<body>
{{{body}}}
</body>
</html>Controller with @Render()
import { Controller, Get, Render } from '@nestjs/common';
@Controller()
export class AppController {
@Get()
@Render('layout')
home() {
return {
title: 'Home',
body: '<h1>Welcome</h1>',
blockAiTraining: false, // Allow AI training on public pages
};
}
@Get('articles')
@Render('layout')
articles() {
return {
title: 'Articles',
body: '<h1>Our Articles</h1>',
blockAiTraining: true, // Block AI training on content pages
};
}
}7. nginx — reverse proxy hard block
map $http_user_agent $block_ai_bot {
default 0;
"~*GPTBot" 1;
"~*ChatGPT-User" 1;
"~*OAI-SearchBot" 1;
"~*ClaudeBot" 1;
"~*Claude-Web" 1;
"~*anthropic-ai" 1;
"~*Google-Extended" 1;
"~*Bytespider" 1;
"~*CCBot" 1;
"~*PerplexityBot" 1;
"~*Applebot-Extended" 1;
}
server {
listen 80;
server_name yourdomain.com;
location = /robots.txt {
alias /var/www/public/robots.txt;
add_header Content-Type "text/plain";
}
location / {
if ($block_ai_bot) {
return 403;
}
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}8. Docker multi-stage build
FROM node:22-alpine AS base
WORKDIR /app
COPY package.json package-lock.json ./
FROM base AS deps
RUN npm ci
FROM base AS builder
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/dist ./dist
COPY --from=deps /app/node_modules ./node_modules
COPY public ./public
EXPOSE 3000
CMD ["node", "dist/main.js"]NestJS layers compared
| Layer | Fires when | DI support | Best for |
|---|---|---|---|
| app.use() main.ts | First — before NestJS routing | No | Simplest global block |
| NestMiddleware | First — before NestJS routing | Yes | Global block + need ConfigService |
| Guard (CanActivate) | After route matching | Yes | Per-controller / decorator-based |
| Interceptor | Wraps handler (pre + post) | Yes | X-Robots-Tag response header |
FAQ
Why use Middleware over a Guard for bot blocking?
Middleware fires before NestJS resolves routes and before Guards execute. A Guard fires after route matching — meaning NestJS has already done work to determine which controller and handler handles the request. For bot blocking, you want to reject as early as possible. Middleware also runs before Pipes and Interceptors, minimising compute for rejected bots.
How do I use ConfigService inside NestMiddleware?
Inject it through the constructor. NestMiddleware is a standard injectable class. Add ConfigService to its constructor: constructor(private readonly config: ConfigService) {}. Register ConfigModule globally (isGlobal: true) in AppModule and it will be available in all middleware and providers.
Does useGlobalGuards() support dependency injection?
Not when instantiated with new in main.ts — the guard is created outside the NestJS DI container. To use DI inside a global guard, register it as a provider: { provide: APP_GUARD, useClass: BlockAiBotsGuard } in AppModule.providers. This lets the guard inject ConfigService and other providers.
Does this work with the Fastify adapter?
Yes with minor changes. Replace @types/express with @fastify/type-provider-typebox and use FastifyRequest / FastifyReply types. app.use() in main.ts works the same way. NestMiddleware use() receives Fastify req/res objects — access User-Agent via req.headers["user-agent"].
How do I test the middleware without running the full NestJS app?
Use supertest with a NestJS test module: const module = await Test.createTestingModule({ ... }).compile(). Create the Nest app from the module and run supertest against it with a GPTBot User-Agent — expect a 403. This tests the full middleware stack without starting a real HTTP server.
Is your site protected from AI bots?
Run a free scan to check your robots.txt, meta tags, and overall AI readiness score.