A port accepts as many ships as it can unload. When more arrive, there are two options: unload faster or let fewer in. An application under load has exactly the same two - cache reduces the work each request costs, and rate limiting caps the number of requests you accept at all.
This lesson closes both sides: first the sentry at the gate, then the storehouse goods are issued from without descending to the depot.
NestJS has a built-in mechanism for limiting traffic, so do not write your own counter:
1@Module({
2 imports: [
3 ThrottlerModule.forRoot([
4 {
5 ttl: 60000,
6 limit: 100,
7 },
8 ]),
9 ],
10 providers: [
11 { provide: APP_GUARD, useClass: ThrottlerGuard },
12 ],
13})
14export class AppModule {}Two numbers describe the whole rule.
is the window's length in milliseconds - a minute here. ttl
is the number of requests allowed within it. Together: one hundred requests per minute from one address.limit
Registering through
APP_GUARD makes ThrottlerGuard a global guard - it applies to every route without adding @UseGuards to each controller. Once the limit is exceeded the client receives 429 Too Many Requests, and the controller method does not run at all.Individual routes can be loosened or tightened with a decorator:
1@Post('login')
2@Throttle({ default: { ttl: 60000, limit: 5 } })
3login(@Body() dto: LoginDto) { }Five login attempts a minute instead of a hundred. This is where rate limiting stops being a performance matter and becomes a safeguard: without it nothing stops password guessing by brute force.
The other side is handled by the cache. Wrapping it in your own service gives one place for keys and lifetimes:
1@Injectable()
2export class CacheService {
3 constructor(@Inject(CACHE_MANAGER) private cache: Cache) {}
4
5 async get<T>(key: string): Promise<T | undefined> {
6 return this.cache.get<T>(key);
7 }
8
9 async set<T>(key: string, value: T, ttl = 300): Promise<void> {
10 await this.cache.set(key, value, ttl);
11 }
12
13 async del(key: string): Promise<void> {
14 await this.cache.del(key);
15 }
16}Three methods cover everything.
reads, get
writes with a lifetime, set
removes. The default del
ttl = 300 can be overridden on any call - short for volatile data, long for lookup tables.Note
del - the method easiest to forget while writing and hardest to live without. Without a way to remove a key, the only means of getting rid of stale data is waiting for it to expire.For ordinary reads you need not write even that.
CacheInterceptor does it for you:1@Controller('provinces')
2export class ProvincesController {
3 @Get()
4 @UseInterceptors(CacheInterceptor)
5 @CacheTTL(600)
6 findAll() {
7 return this.provincesService.findAll();
8 }
9}On every request it goes through five steps, always in this order:
Step three is the heart of it: on a hit the controller method does not run at all, so there is no database query and no work. The interceptor builds the key from the URL by default, so
/provinces?page=2 and /provinces?page=3 are two separate entries.Hence its limitation too: the interceptor handles
GET requests only. Caching a POST makes no sense, since it is meant to change state rather than read it.Caching is easy until the data changes. When it does, the stale entries must go - and the process has four steps:
del).1async update(id: number, dto: UpdateProvinceDto) {
2 const province = await this.repo.save({ id, ...dto });
3
4 await this.cacheService.del(`province:${id}`);
5 await this.cacheService.del('provinces:all');
6
7 return province;
8}Step two causes the most trouble, and the code above shows why. Changing one province invalidates two keys: that province's entry and the list of all of them, because the list contains its data too. With a third page of results and a fifth filter those keys number a dozen - and that is where omissions begin.
So when designing keys, @name, ask yourself the reverse question straight away: not "how do I store this", but "what will I have to remove when it changes". A cache you cannot invalidate is worse than no cache - because it shows stale data and you do not know it.
The port controls its traffic and issues goods from the storehouse:
ThrottlerModule.forRoot is described by two numbers: ttl (the window in milliseconds) and limit (requests within it),APP_GUARD makes ThrottlerGuard global; exceeding the limit gives the client a 429,@Throttle tightens the limit for a single route - on login that is a safeguard, not an optimisation,get, set with a configurable TTL, and del,CacheInterceptor works in five steps: intercept the request → check the key → run the handler (on a MISS) → store the result → return the response,GET only,del) → fetch and store the fresh data,In the next lesson we will turn to compression and response optimisation - what reduces traffic on the way out. For now remember: cache speeds up serving, a limit protects against excess - and the hard part of caching is not the write but knowing when to remove it.