Skip to main content

Command Palette

Search for a command to run...

API Caching Headers: ETag and Cache-Control

Published
7 min readView as Markdown
T

Welcome to TopperBlog! 👋

I'm a tech content creator passionate about helping developers level up their careers and master cutting-edge technologies.

🎯 What I Write About: • AI/ML Engineering & LLMs • Web3 & Blockchain Development
• System Design & Architecture • Interview Preparation (FAANG) • Freelancing & Remote Work • Modern Tech Stacks (Next.js, React, Rust, TypeScript) • Performance Optimization & Best Practices

💼 Mission: Sharing practical, actionable insights that accelerate your tech career and maximize your earning potential.

📚 15+ In-Depth Guides covering everything from earning $10k/month as a freelancer to cracking FAANG interviews.

🌐 Let's connect and grow together in this amazing tech journey!

#TechBlogger #SoftwareEngineering #CareerGrowth #WebDevelopment #AIEngineering

API Caching Headers: ETag and Cache-Control for Modern Web Applications

Introduction

Every developer has experienced the frustration: your API returns stale data, users complain about slow load times, or worse—your server buckles under unnecessary traffic. The culprit? Poor caching strategy. While caching is fundamental to performant web applications, implementing it correctly remains surprisingly challenging in 2026.

API caching headers—specifically ETag and Cache-Control—are your first line of defense against these issues. Yet many developers either ignore them entirely or implement them incorrectly, leading to data inconsistency, wasted bandwidth, and degraded user experience. This article explores why traditional caching approaches fail, how to implement robust caching with modern TypeScript, and the pitfalls you must avoid.

The 2026 Problem: Why Caching Matters More Than Ever

The web landscape has evolved dramatically. Single-page applications (SPAs), mobile-first architectures, and edge computing have made caching not just an optimization—it's a necessity. Consider these realities:

Bandwidth costs remain significant. Despite improved infrastructure, transferring unnecessary data costs money and drains mobile batteries. A typical REST API response might be 50KB, but if nothing changed, that's 50KB of waste multiplied by thousands of requests.

User expectations have increased. Users expect instant responses. A 2026 study shows that 53% of mobile users abandon sites that take longer than 3 seconds to load. Proper caching can reduce response times from hundreds of milliseconds to near-zero.

API rate limits are stricter. Third-party APIs increasingly enforce aggressive rate limiting. Caching reduces your request count, keeping you within limits while maintaining functionality.

Edge computing demands smart caching. With CDNs and edge functions becoming standard, understanding cache headers is critical for leveraging these technologies effectively.

Why Traditional Caching Approaches Fail

Many developers implement caching as an afterthought, leading to common failures:

The "Cache Everything Forever" Anti-Pattern

// DON'T DO THIS
app.get('/api/users/:id', (req, res) => {
  res.setHeader('Cache-Control', 'public, max-age=31536000');
  res.json(getUserData(req.params.id));
});

This approach caches data for a year, ignoring that user data changes frequently. Users see outdated information, and you have no mechanism to invalidate the cache.

The "No Cache" Overreaction

// ALSO PROBLEMATIC
app.get('/api/products', (req, res) => {
  res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate');
  res.json(getProducts());
});

Disabling caching entirely wastes bandwidth and server resources. Even relatively static data like product catalogs benefit from intelligent caching.

Ignoring Conditional Requests

Many implementations send full responses every time, even when the client's cached version is still valid. This wastes bandwidth and processing time.

Modern TypeScript Solution: Implementing ETag and Cache-Control

Let's build a robust caching solution using TypeScript, Express, and modern best practices.

Understanding the Headers

Cache-Control tells clients and intermediaries how to cache responses:

  • max-age: How long (in seconds) the response is fresh
  • public: Can be cached by any cache (CDN, browser)
  • private: Only cacheable by the browser
  • no-cache: Must revalidate before using cached version
  • immutable: Content will never change

ETag (Entity Tag) is a unique identifier for a specific version of a resource. When content changes, the ETag changes.

Complete Implementation

import express, { Request, Response, NextFunction } from 'express';
import crypto from 'crypto';

interface CacheOptions {
  maxAge?: number;
  isPrivate?: boolean;
  mustRevalidate?: boolean;
  immutable?: boolean;
}

// Generate ETag from response data
function generateETag(data: any): string {
  const hash = crypto.createHash('md5');
  hash.update(JSON.stringify(data));
  return `"${hash.digest('hex')}"`;
}

// Middleware for handling ETags and conditional requests
function cacheMiddleware(options: CacheOptions = {}) {
  return (req: Request, res: Response, next: NextFunction) => {
    const originalJson = res.json.bind(res);

    res.json = function(data: any) {
      // Generate ETag for response
      const etag = generateETag(data);
      res.setHeader('ETag', etag);

      // Build Cache-Control header
      const cacheControl: string[] = [];

      if (options.isPrivate) {
        cacheControl.push('private');
      } else {
        cacheControl.push('public');
      }

      if (options.maxAge !== undefined) {
        cacheControl.push(`max-age=${options.maxAge}`);
      }

      if (options.mustRevalidate) {
        cacheControl.push('must-revalidate');
      }

      if (options.immutable) {
        cacheControl.push('immutable');
      }

      res.setHeader('Cache-Control', cacheControl.join(', '));

      // Check if client's cached version is still valid
      const clientETag = req.headers['if-none-match'];

      if (clientETag === etag) {
        // Client has current version, send 304 Not Modified
        return res.status(304).end();
      }

      // Send full response
      return originalJson(data);
    };

    next();
  };
}

// Example usage
const app = express();

// Static product catalog - cache for 1 hour
app.get('/api/products', 
  cacheMiddleware({ maxAge: 3600, isPrivate: false }),
  (req, res) => {
    const products = getProducts(); // Your data fetching logic
    res.json(products);
  }
);

// User-specific data - cache for 5 minutes, private
app.get('/api/users/:id',
  cacheMiddleware({ maxAge: 300, isPrivate: true, mustRevalidate: true }),
  (req, res) => {
    const user = getUserById(req.params.id);
    res.json(user);
  }
);

// Versioned static assets - cache forever
app.get('/api/assets/:version/:file',
  cacheMiddleware({ maxAge: 31536000, immutable: true }),
  (req, res) => {
    const asset = getAsset(req.params.file, req.params.version);
    res.json(asset);
  }
);

Advanced: Last-Modified Header

For resources with known modification times, combine ETag with Last-Modified:

function enhancedCacheMiddleware(options: CacheOptions & { getLastModified?: () => Date }) {
  return (req: Request, res: Response, next: NextFunction) => {
    if (options.getLastModified) {
      const lastModified = options.getLastModified();
      res.setHeader('Last-Modified', lastModified.toUTCString());

      const ifModifiedSince = req.headers['if-modified-since'];
      if (ifModifiedSince) {
        const clientDate = new Date(ifModifiedSince);
        if (clientDate >= lastModified) {
          return res.status(304).end();
        }
      }
    }

    // Continue with ETag logic...
    next();
  };
}

Common Pitfalls and How to Avoid Them

Pitfall 1: Caching Authenticated Requests Publicly

Never use public for user-specific data. Always use private to prevent CDNs from serving one user's data to another.

Pitfall 2: Weak ETags in Distributed Systems

In load-balanced environments, ensure ETag generation is consistent across servers. Use content-based hashing, not server-specific identifiers.

Pitfall 3: Ignoring Vary Header

If responses differ based on headers (like Accept-Language), include the Vary header:

res.setHeader('Vary', 'Accept-Language, Accept-Encoding');

Pitfall 4: Over-Caching Dynamic Data

Don't cache data that changes frequently. For real-time data, use short max-age values or no-cache with ETag validation.

Pitfall 5: Not Testing Cache Behavior

Always test with browser DevTools Network tab. Verify 304 responses and check cache headers.

Best Practices

  1. Use appropriate max-age values: 5 minutes for dynamic data, 1 hour for semi-static, 1 year for immutable assets.

  2. Combine strategies: Use both ETag and Cache-Control for maximum efficiency.

  3. Version your APIs: Include version numbers in URLs for breaking changes, allowing aggressive caching.

  4. Monitor cache hit rates: Track how often 304 responses are sent versus full responses.

  5. Document your caching strategy: Make it clear to API consumers what caching behavior to expect.

  6. Use stale-while-revalidate: For better UX, serve stale content while fetching fresh data in the background.

res.setHeader('Cache-Control', 'max-age=60, stale-while-revalidate=300');

Frequently Asked Questions

Q: Should I use ETag or Last-Modified? A: Use both when possible. ETag is more accurate for content changes, while Last-Modified is simpler and works well for file-based resources.

Q: How do I invalidate cached responses? A: Change the ETag by modifying the content, or use cache-busting query parameters. For CDNs, use their purge APIs.

Q: What's the difference between no-cache and no-store? A: no-cache allows caching but requires revalidation before use. no-store prevents caching entirely. Use no-store only for sensitive data.

Q: Can I cache POST requests? A: Generally no. POST requests are not cacheable by default. Use GET for cacheable operations.

Q: How does this work with GraphQL? A: GraphQL typically uses POST, making HTTP caching harder. Consider implementing application-level caching or using persisted queries with GET.

Q: What about service workers and caching? A: Service workers provide additional caching layers. HTTP cache headers still apply and work alongside service worker caching strategies.

Q: How do I handle cache in development vs production? A: Use environment variables to adjust max-age values. Disable caching in development or use very short durations.

Conclusion

Implementing proper API caching with ETag and Cache-Control headers is no longer optional—it's essential for building performant, scalable applications in 2026. By understanding how these headers work together and following modern best practices, you can dramatically reduce bandwidth usage, improve response times, and create better user experiences.

The TypeScript implementation provided gives you a solid foundation, but remember that caching strategy should be tailored to your specific use case. Start conservative with short max-age values, monitor your cache hit rates, and adjust based on real-world usage patterns.

Proper caching is an investment that pays dividends in reduced infrastructure costs, improved performance, and happier users. Take the time to implement it correctly, and your future self will thank you.


```json { "seo_title": "API Caching Headers: ETag and Cache-Control Guide 2026", "meta_description": "Master API caching with ETag and Cache-Control headers. Learn modern TypeScript implementation, avoid common pitfalls, and boost performance with best practices.", "primary_keyword": "API caching headers", "secondary_keywords": [ "ETag header", "Cache-Control header", "HTTP caching", "TypeScript API caching", "conditional requests", "304 Not Modified", "cache validation", "API performance optimization" ], "tags": [ "API Development", "TypeScript", "Web Performance", "HTTP Headers", "Caching Strategy", "Backend Development", "REST API" ] }