🔄 Understanding ETag and If-None-Match in Web Development
In the world of web development, optimizing resource delivery isn’t just about CDN and lazy loading. Headers like ETag
and If-None-Match
are low-key heroes 🦸♂️ when it comes to reducing bandwidth and improving load speed 🚀.
In this post, I’ll walk through:
- 🧠 What ETags and If-None-Match actually are
- 🤝 Why you should care
- 🔁 How the browser and server interact with these headers
- 🛠️ Real-world usage in a NestJS backend
Let’s dive in 🏊♂️
🧠 What is an ETag?
An ETag (short for Entity Tag) is like a fingerprint 🧬 for a resource. Every time your backend returns something (a JSON response, image, HTML, etc.), it can attach an ETag
header — a unique string that represents the current version of that resource.
Example server response:
200 OK
ETag: "abc123"
This value is cached by the client 📦. On future requests, the client can ask:
“Hey server, is this still version
abc123
?”
If yes, the server replies:
304 Not Modified
…with no response body — saving bandwidth 💡.
📨 What is If-None-Match?
If-None-Match
is the request header the client sends to say:
“Only send the data if it’s different from the version I already have.”
Example:
GET /api/user/123
If-None-Match: "abc123"
If the server's version is still the same, it responds with:
304 Not Modified
Otherwise, it sends the updated content with a new ETag 🆕.
🔁 How They Work Together
Here’s the flow:
- 🧑💻 Client requests a resource
- 🧾 Server responds with data +
ETag: "xyz789"
- 💽 Client saves the response + ETag
- 🔄 Next time, client sends
If-None-Match: "xyz789"
- ✅ If match → server replies with
304 Not Modified
- ❌ If not → updated data is sent with a new ETag
It’s like conditional rendering 🔃, but for APIs.
⚡ Why You Should Care (The Benefits)
- 🪄 Save Bandwidth – Don’t send the same thing twice
- 🚀 Faster UX – Cached responses mean quicker loads
- 🔐 Safe Concurrency – Prevent stale updates with version checks
🛠️ Implementing ETags in NestJS (with Code)
Let’s say you have a user API and want to skip re-sending the same user data when it hasn’t changed.
1. Generate an ETag from your response
// etag.util.ts
import * as crypto from 'crypto';
export function generateEtag(data: any): string {
const str = JSON.stringify(data);
return crypto.createHash('sha1').update(str).digest('hex');
}
2. Use ETag and If-None-Match in your controller
import { Controller, Get, Param, Req, Res, HttpStatus } from '@nestjs/common';
import { Request, Response } from 'express';
import { generateEtag } from './etag.util';
@Controller('user')
export class UserController {
@Get(':id')
async getUser(@Param('id') id: string, @Req() req: Request, @Res() res: Response) {
const user = { id, name: 'Alice' }; // Dummy user data
const etag = generateEtag(user);
const clientEtag = req.headers['if-none-match'];
if (clientEtag === etag) {
return res.status(HttpStatus.NOT_MODIFIED).setHeader('ETag', etag).end();
}
res.setHeader('ETag', etag);
return res.status(HttpStatus.OK).json(user);
}
}
Now you’re only sending data when necessary. Simple and effective 💯.
🧪 Practical Example
Let’s say your site has a giant CSS file 🎨:
- 👀 First visit → browser downloads it, gets
ETag: "v1"
- 👋 Second visit → browser sends
If-None-Match: "v1"
- 🔁 If unchanged → server responds with
304
, no download
Result? Less waiting. Better UX. Happy user 😊.
⚠️ Considerations
- ⚙️ ETag Generation: Needs to be deterministic. Use stable hashing.
- 🧵 Weak ETags:
W/"etag"
means “close enough” (used for minor changes). - 🧩 Middleware/CDNs: Some proxies or CDNs may strip or override ETags.
- ⏱️ Overhead: For dynamic content, ETag hashing might add slight load.
🧷 Bonus: Updating Resources Safely with If-Match
Imagine two users editing the same record 🧑🤝🧑. You can use If-Match
to prevent overwriting someone else’s changes.
PUT /user/123
If-Match: "v1"
NestJS:
@Put(':id')
async updateUser(@Param('id') id: string, @Req() req: Request, @Res() res: Response) {
const currentUser = { id, name: 'Alice' };
const currentEtag = generateEtag(currentUser);
const clientEtag = req.headers['if-match'];
if (clientEtag !== currentEtag) {
return res.status(HttpStatus.PRECONDITION_FAILED).send('ETag mismatch');
}
// Perform the update
return res.json({ message: 'Updated successfully' });
}
It’s like optimistic locking, but way easier 🔐.
📥 Recap of Request & Response Headers
📤 Request Headers:
If-None-Match: "etag-value" // For GET
If-Match: "etag-value" // For PUT/UPDATE
📥 Response Headers:
ETag: "etag-value"
✅ TL;DR
Header | Direction | Who Sends It | Used For |
---|---|---|---|
ETag |
Server ➡️ Client | Server | Identifying resource version |
If-None-Match |
Client ➡️ Server | Client | Conditional GET (cache check) |
If-Match |
Client ➡️ Server | Client | Safe updates |
304 Not Modified |
Server ➡️ Client | Server | Says: “You already have it” |
412 Precondition Failed |
Server ➡️ Client | Server | Update blocked due to version mismatch |
🧠 Final Thoughts
ETag and If-None-Match are underrated performance boosts 🚀. They save bandwidth, speed up your app, and help avoid update collisions — all with just a few headers.
And with NestJS, it’s super easy to implement 💪.