A simple node cache api

If one wants cross request cache queries in a node server, one can use a simple cache api inspired by react-query with only a redis library implementation.

Caching by query key


import Redis from 'ioredis'
// or setup a redis connection in a separate file with creds etc
// import { redis } from '@/server/redis' // ioredis instance redis = new Redis()
const redis = new Redis()
type CacheOptions = {
ttlInSeconds: number // seconds
skipCache?: boolean
}
// An api similar to react query's useQuery
export async function cacheQuery<T>({
queryKey,
queryFn,
options = {
ttlInSeconds: 60 * 5,
skipCache: false,
},
}: {
queryKey: QueryKey
queryFn: () => Promise<T>
options?: CacheOptions
}): Promise<T> {
const key = hashKey(queryKey)
if (!options?.skipCache) {
const cached = await redis.get(key)
if (typeof cached === 'string') {
try {
if (process.env.LOG_LEVEL === 'debug') {
console.log('Cache hit', key)
}
const parsed = JSON.parse(cached)
return parsed as T
} catch (e) {
console.error('Error parsing cached data', e)
}
}
}
return queryFn?.().then(async (data) => {
if (process.env.LOG_LEVEL === 'debug') {
console.log('Cache miss', key)
}
try {
await redis?.set?.(key, JSON.stringify(data), 'EX', options?.ttlInSeconds)
} catch (e) {
console.error('Error caching data', e)
}
return data
})
}
/**
* Default query & mutation keys hash function.
* Hashes the value into a stable hash.
* https://github.com/TanStack/query/blob/69d37f33bdee50d73d0f05256f243113a857a1ee/packages/query-core/src/utils.ts#L177
*/
export type QueryKey = ReadonlyArray<unknown>
export function hashKey(queryKey: QueryKey): string {
return JSON.stringify(queryKey, (_, val) =>
isPlainObject(val)
? Object.keys(val)
.sort()
.reduce((result, key) => {
result[key] = val[key]
return result
}, {} as any)
: val
)
}
function hasObjectPrototype(o: any): boolean {
return Object.prototype.toString.call(o) === '[object Object]'
}
// Copied from: https://github.com/jonschlinkert/is-plain-object
export function isPlainObject(o: any): o is Object {
if (!hasObjectPrototype(o)) {
return false
}
// If has no constructor
const ctor = o.constructor
if (ctor === undefined) {
return true
}
// If has modified prototype
const prot = ctor.prototype
if (!hasObjectPrototype(prot)) {
return false
}
// If constructor does not have an Object-specific method
if (!prot.hasOwnProperty('isPrototypeOf')) {
return false
}
// Most likely a plain Object
return true
}
export function isPlainArray(value: unknown) {
return Array.isArray(value) && value.length === Object.keys(value).length
}

Security notes

Remember that this is a simple cache and either needs user id validation prior and caching with the user id or a more complex cache key generation.