All articles
A NestJS server with rawBody enabled verifying webhook signatures from req.rawBody via a guard, with nest start exposed through a local tunnel.
NestJSNode.jswebhook debugginglocal testing

NestJS Webhooks: rawBody, Signature Guards, ValidationPipe Traps

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

  1. Start the API: npm run start:dev or nest start --watch on port 3000.
  2. Expose it: npx portpreview 3000.
  3. Register the tunnel URL + webhook path in the provider dashboard.
  4. Trigger test deliveries and confirm guard pass/fail in logs.
  5. 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.

Frequently asked questions

Why is req.rawBody undefined in my NestJS webhook?
You must pass rawBody: true to NestFactory.create. Without it, Express does not attach the raw buffer and signature verification cannot access signed bytes.
Can I verify signatures using @Body() in NestJS?
No. @Body() returns a parsed object that may differ from the bytes the provider signed. Use req.rawBody after enabling rawBody: true.
How do I test NestJS webhooks locally?
Run nest start on port 3000, expose with npx portpreview 3000, register the tunnel URL in the provider dashboard, and trigger test events.