typescript
59 lines · 7 steps
HTTP conditional caching in a NestJS controller
A controller endpoint returns 304 Not Modified when a client's cached copy is still fresh, using ETag and Last-Modified validators.
Explained by
highlit
1import {
2 Controller,
3 Get,
4 Param,
5 Header,
6 Req,
7 Res,
8 NotFoundException,
9} from '@nestjs/common';
10import { Request, Response } from 'express';
11import { createHash } from 'crypto';
12import { ArticlesService } from './articles.service';
13
14@Controller('articles')
15export class ArticlesController {
16 constructor(private readonly articles: ArticlesService) {}
17
18 @Get(':slug')
19 @Header('Cache-Control', 'public, max-age=60, stale-while-revalidate=300')
20 async findOne(
21 @Param('slug') slug: string,
22 @Req() req: Request,
23 @Res() res: Response,
24 ): Promise<void> {
25 const article = await this.articles.findBySlug(slug);
26 if (!article) {
27 throw new NotFoundException(`Article "${slug}" not found`);
28 }
29
30 const etag = this.weakEtag(article.id, article.updatedAt);
31 const lastModified = article.updatedAt.toUTCString();
32
33 res.setHeader('ETag', etag);
34 res.setHeader('Last-Modified', lastModified);
35 res.setHeader('Vary', 'Accept-Encoding');
36
37 const ifNoneMatch = req.headers['if-none-match'];
38 const ifModifiedSince = req.headers['if-modified-since'];
39
40 const etagMatches = ifNoneMatch === etag;
41 const notModifiedSince =
42 ifModifiedSince !== undefined &&
43 new Date(ifModifiedSince).getTime() >= article.updatedAt.getTime();
44
45 if (etagMatches || notModifiedSince) {
46 res.status(304).end();
47 return;
48 }
49
50 res.status(200).json(article);
51 }
52
53 private weakEtag(id: string, updatedAt: Date): string {
54 const digest = createHash('sha1')
55 .update(`${id}:${updatedAt.getTime()}`)
56 .digest('base64');
57 return `W/"${digest}"`;
58 }
59}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Conditional requests let a server skip resending a body when the client already holds a current copy.
- 2A weak ETag derived from stable identity fields lets you compare versions without hashing the full payload.
- 3Escaping to Express's raw response with @Res means you own status codes and body serialization for that handler.
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
go
package api import ( "crypto/sha256"
ETag conditional requests in Gin
http-caching
etag
conditional-requests
Intermediate
6 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/http-conditional-caching-in-a-nestjs-controller-explained-typescript-4b21/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.