feat(api): Implement API response Cache-Control

- Introduce `withCacheControl` utility function for API responses.
- Apply `private, no-store` to authentication and data modification (POST, PATCH, DELETE) routes.
- Apply `private, no-cache` to sensitive data retrieval (GET) routes.
- Enhance security by preventing caching of private user data.
- Ensure fresh data is always fetched for authenticated API responses.
This commit is contained in:
Yusuf İpek
2026-02-07 16:51:46 +03:00
parent 669f6fa9d2
commit 6e8170d080
24 changed files with 179 additions and 70 deletions
+5
View File
@@ -126,6 +126,11 @@ export function successResponse<T>(
return NextResponse.json(body, { status });
}
export function withCacheControl<T>(response: NextResponse<T>, value: string): NextResponse<T> {
response.headers.set('Cache-Control', value);
return response;
}
/**
* Common error response helpers
*/
+38
View File
@@ -0,0 +1,38 @@
import type { VideoMetadata } from './types';
type CacheEntry = {
value: VideoMetadata;
expiresAt: number;
};
const MAX_ENTRIES = 500;
const DEFAULT_TTL_MS = 24 * 60 * 60 * 1000;
const cache = new Map<string, CacheEntry>();
function pruneIfNeeded(): void {
if (cache.size <= MAX_ENTRIES) return;
const overflow = cache.size - MAX_ENTRIES;
for (let i = 0; i < overflow; i += 1) {
const oldestKey = cache.keys().next().value as string | undefined;
if (!oldestKey) return;
cache.delete(oldestKey);
}
}
export function getCachedMetadata(key: string): VideoMetadata | null {
const entry = cache.get(key);
if (!entry) return null;
if (Date.now() > entry.expiresAt) {
cache.delete(key);
return null;
}
cache.delete(key);
cache.set(key, entry);
return entry.value;
}
export function setCachedMetadata(key: string, value: VideoMetadata, ttlMs: number = DEFAULT_TTL_MS): void {
cache.set(key, { value, expiresAt: Date.now() + ttlMs });
pruneIfNeeded();
}
+11 -2
View File
@@ -1,4 +1,5 @@
import type { VideoProvider, VideoMetadata, EmbedOptions, ThumbnailSize } from './types';
import { getCachedMetadata, setCachedMetadata } from './metadata-cache';
// Vimeo URL patterns
const VIMEO_PATTERNS = [
@@ -60,6 +61,9 @@ export const vimeoProvider: VideoProvider = {
},
async getMetadata(videoId: string): Promise<VideoMetadata> {
const cacheKey = `vimeo:${videoId}`;
const cached = getCachedMetadata(cacheKey);
if (cached) return cached;
try {
const response = await fetch(
`https://vimeo.com/api/oembed.json?url=https://vimeo.com/${videoId}`
@@ -71,7 +75,7 @@ export const vimeoProvider: VideoProvider = {
const data = await response.json();
return {
const metadata: VideoMetadata = {
title: data.title,
description: data.description,
thumbnailUrl: data.thumbnail_url,
@@ -80,11 +84,16 @@ export const vimeoProvider: VideoProvider = {
authorUrl: data.author_url,
uploadDate: data.upload_date ? new Date(data.upload_date) : undefined,
};
setCachedMetadata(cacheKey, metadata);
return metadata;
} catch (error) {
return {
const fallback: VideoMetadata = {
title: 'Vimeo Video',
thumbnailUrl: this.getThumbnailUrl(videoId, 'large'),
};
setCachedMetadata(cacheKey, fallback);
return fallback;
}
},
};
+11 -2
View File
@@ -1,4 +1,5 @@
import type { VideoProvider, VideoMetadata, EmbedOptions, ThumbnailSize } from './types';
import { getCachedMetadata, setCachedMetadata } from './metadata-cache';
// YouTube URL patterns
const YOUTUBE_PATTERNS = [
@@ -57,6 +58,9 @@ export const youtubeProvider: VideoProvider = {
},
async getMetadata(videoId: string): Promise<VideoMetadata> {
const cacheKey = `youtube:${videoId}`;
const cached = getCachedMetadata(cacheKey);
if (cached) return cached;
// Using oEmbed API - no API key required
// For production, you might want to use YouTube Data API for more data
try {
@@ -70,18 +74,23 @@ export const youtubeProvider: VideoProvider = {
const data = await response.json();
return {
const metadata: VideoMetadata = {
title: data.title,
thumbnailUrl: data.thumbnail_url,
author: data.author_name,
authorUrl: data.author_url,
};
setCachedMetadata(cacheKey, metadata);
return metadata;
} catch (error) {
// Fallback with minimal data
return {
const fallback: VideoMetadata = {
title: 'YouTube Video',
thumbnailUrl: this.getThumbnailUrl(videoId, 'large'),
};
setCachedMetadata(cacheKey, fallback);
return fallback;
}
},
};