feat: add Redis caching for recipe queries with automatic invalidation
All checks were successful
CI / update (push) Successful in 13s

Implements Redis caching layer for recipe endpoints to reduce MongoDB load and improve response times:

- Install ioredis for Redis client with TypeScript support
- Create cache.ts with namespaced keys (homepage: prefix) to avoid conflicts with other Redis applications
- Add caching to recipe query endpoints (all_brief, by tag, in_season) with 1-hour TTL
- Implement automatic cache invalidation on recipe create/edit/delete operations
- Cache recipes before randomization to maximize cache reuse while maintaining random order per request
- Add graceful fallback to MongoDB if Redis is unavailable
- Update .env.example with Redis configuration (REDIS_HOST, REDIS_PORT)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-07 20:25:07 +01:00
parent bfba0870e3
commit 1628f8ba23
10 changed files with 457 additions and 11 deletions

View File

@@ -2,6 +2,7 @@ import type { RequestHandler } from '@sveltejs/kit';
import { Recipe } from '../../../../models/Recipe';
import { dbConnect } from '../../../../utils/db';
import { error } from '@sveltejs/kit';
import { invalidateRecipeCaches } from '$lib/server/cache';
// header: use for bearer token for now
// recipe json in body
export const POST: RequestHandler = async ({request, cookies, locals}) => {
@@ -19,6 +20,8 @@ export const POST: RequestHandler = async ({request, cookies, locals}) => {
await dbConnect();
try{
await Recipe.create(recipe_json);
// Invalidate recipe caches after successful creation
await invalidateRecipeCaches();
} catch(e){
throw error(400, e)
}

View File

@@ -4,6 +4,7 @@ import { UserFavorites } from '../../../../models/UserFavorites';
import { dbConnect } from '../../../../utils/db';
import type {RecipeModelType} from '../../../../types/types';
import { error } from '@sveltejs/kit';
import { invalidateRecipeCaches } from '$lib/server/cache';
// header: use for bearer token for now
// recipe json in body
export const POST: RequestHandler = async ({request, locals}) => {
@@ -69,6 +70,9 @@ export const POST: RequestHandler = async ({request, locals}) => {
// Delete the recipe
await Recipe.findOneAndDelete({short_name: short_name});
// Invalidate recipe caches after successful deletion
await invalidateRecipeCaches();
return new Response(JSON.stringify({msg: "Deleted recipe successfully"}),{
status: 200,
});

View File

@@ -6,6 +6,7 @@ import { error } from '@sveltejs/kit';
import { rename } from 'fs/promises';
import { join } from 'path';
import { existsSync } from 'fs';
import { invalidateRecipeCaches } from '$lib/server/cache';
// header: use for bearer token for now
// recipe json in body
@@ -46,6 +47,10 @@ export const POST: RequestHandler = async ({request, locals}) => {
}
await Recipe.findOneAndUpdate({short_name: message.old_short_name }, recipe_json);
// Invalidate recipe caches after successful update
await invalidateRecipeCaches();
return new Response(JSON.stringify({msg: "Edited recipe successfully"}),{
status: 200,
});

View File

@@ -3,9 +3,27 @@ import type { BriefRecipeType } from '../../../../../types/types';
import { Recipe } from '../../../../../models/Recipe'
import { dbConnect } from '../../../../../utils/db';
import { rand_array } from '$lib/js/randomize';
import cache from '$lib/server/cache';
export const GET: RequestHandler = async ({params}) => {
await dbConnect();
let found_brief = rand_array(await Recipe.find({}, 'name short_name tags category icon description season dateModified').lean()) as BriefRecipeType[];
return json(JSON.parse(JSON.stringify(found_brief)));
const cacheKey = 'recipes:all_brief';
// Try cache first
let recipes: BriefRecipeType[] | null = null;
const cached = await cache.get(cacheKey);
if (cached) {
recipes = JSON.parse(cached);
} else {
// Cache miss - fetch from DB
await dbConnect();
recipes = await Recipe.find({}, 'name short_name tags category icon description season dateModified').lean() as BriefRecipeType[];
// Store in cache (1 hour TTL)
await cache.set(cacheKey, JSON.stringify(recipes), 3600);
}
// Apply randomization after fetching (so each request gets different order)
const randomized = rand_array(recipes);
return json(JSON.parse(JSON.stringify(randomized)));
};

View File

@@ -3,10 +3,27 @@ import { json, type RequestHandler } from '@sveltejs/kit';
import { Recipe } from '../../../../../../models/Recipe'
import { dbConnect } from '../../../../../../utils/db';
import { rand_array } from '$lib/js/randomize';
import cache from '$lib/server/cache';
export const GET: RequestHandler = async ({params}) => {
await dbConnect();
let found_in_season = rand_array(await Recipe.find({season: params.month, icon: {$ne: "🍽️"}}, 'name short_name images tags category icon description season dateModified').lean());
found_in_season = JSON.parse(JSON.stringify(found_in_season));
return json(found_in_season);
const cacheKey = `recipes:in_season:${params.month}`;
// Try cache first
let recipes = null;
const cached = await cache.get(cacheKey);
if (cached) {
recipes = JSON.parse(cached);
} else {
// Cache miss - fetch from DB
await dbConnect();
recipes = await Recipe.find({season: params.month, icon: {$ne: "🍽️"}}, 'name short_name images tags category icon description season dateModified').lean();
// Store in cache (1 hour TTL)
await cache.set(cacheKey, JSON.stringify(recipes), 3600);
}
// Apply randomization after fetching (so each request gets different order)
const randomized = rand_array(recipes);
return json(JSON.parse(JSON.stringify(randomized)));
};

View File

@@ -3,11 +3,27 @@ import { Recipe } from '../../../../../../models/Recipe';
import { dbConnect } from '../../../../../../utils/db';
import type {BriefRecipeType} from '../../../../../../types/types';
import { rand_array } from '$lib/js/randomize';
import cache from '$lib/server/cache';
export const GET: RequestHandler = async ({params}) => {
await dbConnect();
let recipes = rand_array(await Recipe.find({tags: params.tag}, 'name short_name images tags category icon description season dateModified').lean()) as BriefRecipeType[];
const cacheKey = `recipes:tag:${params.tag}`;
recipes = JSON.parse(JSON.stringify(recipes));
return json(recipes);
// Try cache first
let recipes: BriefRecipeType[] | null = null;
const cached = await cache.get(cacheKey);
if (cached) {
recipes = JSON.parse(cached);
} else {
// Cache miss - fetch from DB
await dbConnect();
recipes = await Recipe.find({tags: params.tag}, 'name short_name images tags category icon description season dateModified').lean() as BriefRecipeType[];
// Store in cache (1 hour TTL)
await cache.set(cacheKey, JSON.stringify(recipes), 3600);
}
// Apply randomization after fetching (so each request gets different order)
const randomized = rand_array(recipes);
return json(JSON.parse(JSON.stringify(randomized)));
};