NestJS webhook handlers fail in subtle ways. A global ValidationPipe strips unknown fields before your guard runs, and default JSON parsing destroys the raw bytes providers sign. Local testing should prove rawBody: true, signature guards, and route-scoped validation before you paste a tunnel URL into Stripe or GitHub.
Enable raw body at bootstrap
Pass rawBody: true when creating the Nest application so Express keeps an untouched buffer on the request:
async function bootstrap() {
const app = await NestFactory.create(AppModule, { rawBody: true });
app.useGlobalPipes(new ValidationPipe({ whitelist: true }));
await app.listen(3000);
}
Without this flag, req.rawBody is undefined and every HMAC check fails even when the network path is correct.
@Body() vs req.rawBody for signatures
Never verify signatures against @Body() — Nest has already parsed and potentially transformed the object. Read the buffer attached by raw-body middleware:
@Post('stripe')
@UseGuards(StripeSignatureGuard)
handleStripe(@Req() req: RawBodyRequest) {
const payload = req.rawBody;
const event = JSON.parse(payload.toString('utf8'));
this.events.process(event);
return { received: true };
}
Parse JSON only after the guard confirms the signature. The order is guard first, deserialize second.
Use a signature guard before ValidationPipe effects
Encapsulate provider verification in a guard that reads headers and compares digests in constant time:
@Injectable()
export class StripeSignatureGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const req = context.switchToHttp().getRequest>();
const sig = req.headers['stripe-signature'] as string;
return verify(req.rawBody, sig, process.env.STRIPE_WEBHOOK_SECRET);
}
}
Apply the guard with @UseGuards on the webhook route only. Other API routes can keep ValidationPipe and DTO validation normally.
Avoid the global ValidationPipe trap on webhook routes
A global ValidationPipe with transform: true runs before your controller and may coerce types on parsed bodies. Webhook routes should rely on rawBody + manual JSON.parse after verification. If you must share pipes, exclude webhook controllers from global validation or register guards that execute before pipes consume the body.
Local testing with nest start + PortPreview
- Start the API:
npm run start:devornest start --watchon port 3000. - Expose it:
npx portpreview 3000. - Register the tunnel URL + webhook path in the provider dashboard.
- Trigger test deliveries and confirm guard pass/fail in logs.
- Replay duplicate event IDs to validate idempotency — see retry patterns.
Common pitfalls
ValidationPipe before signature check
If validation runs first, Nest may read and transform the body, leaving rawBody stale or empty. Guards must run on the raw buffer before any DTO binding.
Global JSON middleware order
Express json() middleware registered before Nest raw-body handling can consume the stream. Keep rawBody: true at factory creation and avoid duplicate body parsers in main.ts.
Slow handlers and retries
Return 200 quickly after verification, queue heavy work, and deduplicate by event ID so provider retries do not double-charge or double-email.
Where to go deeper
For fundamentals, read what localhost tunneling is and how to debug webhooks locally. For signature mechanics, see the signature verification guide. For duplicate-safe handlers, read retry and idempotency patterns. start PortPreview free.
