Key–value, the way you already know it.
GAThe KV surface is ioredis-compatible: the method names, arguments, and return shapes match what you use with Redis. Strings with TTLs, atomic counters, hashes, mget/mset, pipelines — and a command() escape hatch for anything not typed.
get / set / del / exists
import { cache } from './cache'
// Set with a 60-second TTL, only if the key is new:
await cache.set('session:abc', token, { ex: 60, nx: true })
await cache.get('session:abc') // token | null
await cache.exists('session:abc') // 1 | 0
await cache.del('session:abc') // number removedget() returns the value or null. set() takes an options object as its third argument (below). del() and exists() return counts, like Redis.
TTLs and conditions
await cache.set('k', 'v', { ex: 60 }) // expire in 60 seconds
await cache.set('k', 'v', { px: 1500 }) // expire in 1500 ms
await cache.set('k', 'v', { nx: true }) // set only if absent
await cache.set('k', 'v', { xx: true }) // set only if present
await cache.set('k', 'v', { keepTtl: true }) // overwrite value, keep existing TTL| Option | Meaning |
|---|---|
ex | Expire the key in this many seconds. |
px | Expire the key in this many milliseconds. |
nx | Set only if the key does not already exist. |
xx | Set only if the key already exists. |
keepTtl | Overwrite the value but keep the key’s current TTL instead of clearing it. |
set(key, id, { nx: true, ex: 30 }) is the classic single-round-trip lock: it succeeds for exactly one caller and self-expires, so a crashed holder can’t deadlock the key.Atomic counters
incr/incrby/decr/decrby are atomic on the server — no read-modify-write race:
await cache.incr('views') // 1
await cache.incrby('views', 10) // 11
await cache.decr('views') // 10
await cache.decrby('views', 5) // 5Expiry: expire / ttl / persist
await cache.set('otp', code)
await cache.expire('otp', 300) // 300s to live
await cache.ttl('otp') // seconds remaining (-1 no TTL, -2 missing)
await cache.persist('otp') // drop the TTL, keep the valuettl() returns the seconds remaining, or -1 when the key exists with no TTL and -2 when it doesn’t exist — the standard Redis convention.
mget / mset
await cache.mset({ 'a': '1', 'b': '2', 'c': '3' })
await cache.mget('a', 'b', 'c') // ['1', '2', '3'] (null for missing)One round trip for many keys. mget() preserves order and returns null for keys that don’t exist.
Hashes
Store a small record under one key with hget/hset/hgetall/hdel/hincrby:
await cache.hset('user:42', { name: 'Ada', plan: 'pro' })
await cache.hget('user:42', 'name') // 'Ada'
await cache.hgetall('user:42') // { name: 'Ada', plan: 'pro' }
await cache.hincrby('user:42', 'logins', 1)
await cache.hdel('user:42', 'plan')Pipelines
Batch several commands into a single round trip with pipeline(). Each entry is an [command, ...args] array; results come back in order:
const [views, exists] = await cache.pipeline([
['incr', 'views'],
['exists', 'session:abc'],
])Raw command()
Anything the typed methods don’t cover, reach for command() — an array of the command and its arguments:
// Raw command for anything the typed methods don't cover.
// Still subject to the server-side allow-list — see Security.
await cache.command(['SET', 'k', 'v', 'EX', '60'])
await cache.command(['GETRANGE', 'k', '0', '3'])command() is still bound by the server-side allow-list. Destructive or introspection commands (FLUSHALL, KEYS, CONFIG, EVAL) are rejected — see the security model.Next: Queues for durable work, Pub/Sub for fan-out, or Backends & vendors to choose where a namespace’s keys actually live.