typescript
42 lines · 8 steps
Rate-limiting an auth flow in NestJS
A NestJS auth controller applies per-endpoint throttling to defend login, 2FA, and token refresh against abuse.
Explained by
highlit
1import { Body, Controller, Ip, Post, UnauthorizedException } from '@nestjs/common';
2import { Throttle, ThrottlerGuard } from '@nestjs/throttler';
3import { UseGuards } from '@nestjs/common';
4import { AuthService } from './auth.service';
5import { LoginDto } from './dto/login.dto';
6
7@Controller('auth')
8@UseGuards(ThrottlerGuard)
9export class AuthController {
10 constructor(private readonly authService: AuthService) {}
11
12 @Post('login')
13 @Throttle({ default: { limit: 5, ttl: 60_000 } })
14 async login(@Body() dto: LoginDto, @Ip() ip: string) {
15 const user = await this.authService.validateCredentials(dto.email, dto.password);
16
17 if (!user) {
18 await this.authService.recordFailedAttempt(dto.email, ip);
19 throw new UnauthorizedException('Invalid email or password');
20 }
21
22 return this.authService.issueTokens(user, ip);
23 }
24
25 @Post('login/verify-2fa')
26 @Throttle({ default: { limit: 3, ttl: 300_000 } })
27 async verifyTwoFactor(@Body() dto: { challengeId: string; code: string }, @Ip() ip: string) {
28 const session = await this.authService.verifyOtp(dto.challengeId, dto.code, ip);
29
30 if (!session) {
31 throw new UnauthorizedException('Invalid or expired verification code');
32 }
33
34 return session;
35 }
36
37 @Post('token/refresh')
38 @Throttle({ default: { limit: 30, ttl: 60_000 } })
39 async refresh(@Body('refreshToken') refreshToken: string, @Ip() ip: string) {
40 return this.authService.rotateRefreshToken(refreshToken, ip);
41 }
42}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Applying a guard at the class level covers every route while per-route decorators fine-tune the policy for each one.
- 2Different auth actions deserve different rate limits based on their risk and expected call frequency.
- 3Uniform error messages and recorded failed attempts limit what an attacker learns from probing credentials.
Related explainers
typescript
import { registerLocaleData } from '@angular/common'; import localeFr from '@angular/common/locales/fr'; import localeFrExtra from '@angular/common/locales/extra/fr'; import localeDe from '@angular/common/locales/de';
Locale-aware bootstrapping in Angular
i18n
localization
dependency-injection
Intermediate
8 steps
typescript
import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import * as Joi from 'joi';
Validating env config at boot in NestJS
configuration
schema-validation
environment-variables
Intermediate
8 steps
python
import time import uuid from django.utils.deprecation import MiddlewareMixin
Attaching per-request context in Django
middleware
request lifecycle
multi-tenancy
Intermediate
7 steps
typescript
import { Inject, Injectable, Logger } from '@nestjs/common'; import { CACHE_MANAGER } from '@nestjs/cache-manager'; import { Cache } from 'cache-manager'; import { InjectRepository } from '@nestjs/typeorm';
A cache-aside country lookup in NestJS
cache-aside
dependency-injection
batch-lookup
Intermediate
8 steps
typescript
import { Injectable, effect, signal, computed } from '@angular/core'; interface Preferences { theme: 'light' | 'dark';
A signal-based preferences store in Angular
signals
state-management
persistence
Intermediate
7 steps
typescript
import { useEffect, useState } from "react"; interface Section { id: string;
Building a scroll-spy hook in React
custom-hooks
intersectionobserver
dom-observation
Intermediate
8 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/rate-limiting-an-auth-flow-in-nestjs-explained-typescript-d4f3/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.