This implementation adds CDN-friendly cache-control headers to the GET endpoints of the gists API, enabling downstream CDN services (Fastly, Vercel, Cloudflare) to cache responses and purge them when content changes.
Created three interceptor classes:
- BaseCacheControlInterceptor: Abstract base class that sets Cache-Control and Cache-Tag headers
- NearbyCacheControlInterceptor: For GET /gists (nearby search queries)
- GistIdCacheControlInterceptor: For GET /gists/:id (single gist retrieval)
Headers set:
Cache-Control: s-maxage=60, stale-while-revalidate=120- CDNs can cache for 60 seconds
- Stale content can be served for an additional 120 seconds while revalidating
Cache-Tag: gist:nearby:{cell}(for nearby queries, where {cell} is the geohash)Cache-Tag: gist:one:{id}(for single gist queries)
Added interceptors to GET endpoints:
GET /gists→ UsesNearbyCacheControlInterceptorGET /gists/:id→ UsesGistIdCacheControlInterceptor
POST endpoints remain unchanged (no caching).
Added purgeCdnTags() method to CacheService that:
- Sends purge requests to CDN providers when content changes
- Supports Fastly (Fastly-Key header), Vercel, and Cloudflare (Bearer token)
- Gracefully degrades if CDN configuration is not present
Modified invalidateNearbyCache() to:
- Clear Redis cache (existing behavior)
- Purge CDN cache tags when a new gist is posted
Registered the interceptors as providers so they can be dependency-injected.
Added two new optional environment variables:
CDN_PURGE_ENDPOINT: CDN API endpoint for cache purgingCDN_PURGE_TOKEN: API token/key for authentication
-
First request:
GET /gists?lat=9.0579&lon=7.4951- Backend processes request
- Response includes:
Cache-Control: s-maxage=60, stale-while-revalidate=120 - Response includes:
Cache-Tag: gist:nearby:s0mc8dy(geohash for the location) - CDN caches response for 60 seconds
-
Subsequent requests (within 60s): Served directly from CDN
-
New gist posted at the same location:
- Backend creates gist
- Backend calls
invalidateNearbyCache(lat, lon) - Computes geohash:
s0mc8dy - Sends purge request to CDN:
{"tags": ["gist:nearby:s0mc8dy"]} - CDN invalidates cached responses with that tag
- Next request gets fresh data
Nearby queries use geohash-based tags:
- Query at (9.0579, 7.4951) → geohash
s0mc8dy→ taggist:nearby:s0mc8dy - All queries in the same geohash cell share the same cache
- Posting a gist purges all cached responses for that cell
Single gist queries use ID-based tags:
- Query for gist
abc123→ taggist:one:abc123 - Isolated caching per gist
No additional configuration needed. Cache-Control headers are still emitted but won't have any effect without a CDN in front.
CDN_PURGE_ENDPOINT=https://api.fastly.com/service/{service_id}/purge
CDN_PURGE_TOKEN=your_fastly_api_tokenCDN_PURGE_ENDPOINT=https://api.vercel.com/v1/purge
CDN_PURGE_TOKEN=your_vercel_api_tokenCDN_PURGE_ENDPOINT=https://api.cloudflare.com/client/v4/zones/{zone_id}/purge_cache
CDN_PURGE_TOKEN=your_cloudflare_api_tokencurl -I "http://localhost:3000/gists?lat=9.0579&lon=7.4951"Expected headers:
Cache-Control: s-maxage=60, stale-while-revalidate=120
Cache-Tag: gist:nearby:s0mc8dy
- Configure CDN environment variables
- Post a new gist:
curl -X POST http://localhost:3000/gists \ -H "Content-Type: application/json" \ -d '{"lat":9.0579,"lon":7.4951,"content":"Test gist","author":"user1"}'
- Check backend logs for:
Successfully purged CDN cache tags: gist:nearby:s0mc8dy - If no CDN configured, logs show:
CDN_PURGE_ENDPOINT or CDN_PURGE_TOKEN not configured, skipping CDN purge
- Response bears both
Cache-ControlandCache-Tagheaders - Post a new gist → purge the tag → CDNs revalidate
- Implemented in
Backend/src/gists/gists.controller.ts - Created
Backend/src/common/interceptors/cache-control.interceptor.ts
Before: Every GET /gists query hits the backend (and possibly Redis)
After:
- CDN serves cached responses for 60 seconds
- Stale responses can be served for up to 180 seconds total
- Backend only processes requests on cache miss or after purge
- Significant RPS reduction on backend during high traffic
- Geohash precision is 7 characters (~153m x 153m cells)
- Cache tags are tied to geohash cells, not exact lat/lon
- Batch gist creation purges all affected geohash cells
- CDN purge is fire-and-forget (errors are logged but don't block creation)
- Redis cache invalidation still happens regardless of CDN configuration