Skip to content

Why Benni?

Redis is often used like this, here with raw node-redis:

await nodeRedis.hSet(`user:${id}`, {
name: user.name,
score: String(user.score)
});
const raw = await nodeRedis.hGetAll(`user:${id}`);
const loadedUser = {
name: raw.name,
score: Number(raw.score)
};

node-redis even types that hGetAll call, as Record<string, string>. The types are present, but they describe Redis’s wire shape, not your data: score comes back a string, and you coerce it by hand. This works, but over time it creates problems:

  • Key names are spread across the codebase.
  • Values are manually serialized and parsed.
  • Return types are not obvious.
  • Data structures are implicit.
  • Refactoring is risky.
  • Raw Redis commands are powerful but easy to misuse.

Benni keeps Redis explicit, but adds a typed schema layer:

export const users = hash("user", {
name: string(),
score: number()
});
await redis.hash(users).hset(id, {
name: "Ada",
score: 10
});
const user = await redis.hash(users).hget(id);

You still use Redis. You still understand what happens. You just stop scattering strings and parsers across your app.

Benni is not a replacement for Redis. It is a typed layer on top of a Redis client: the client types the commands, Benni types your data.

Featurenode-redis / ioredisBenni
Raw Redis commandsYesYes
Command-level typesYes (wire shape)Yes
Typed schemasManualYes
Typed hash / JSON valuesManualYes
Key prefixesManualSchema-based
Runtime reachNode onlyNode, Bun, Deno, edge/serverless
Escape hatchNativeredis.raw

Use a raw client when you want direct command access everywhere. Use Benni when your application has repeated Redis data patterns and you want your declared types to survive the round-trip, safer keys, and better refactoring, across every runtime.

Three apps built twice against Redis 8, once through Benni and once through raw node-redis, feature for feature. Lines of implementation code, blank lines excluded:

AppBenniRaw
URL shortener (hash, counter, sorted set, stream, cache, rate limit)97171
AI generation service (queue: resumable stream, cancel, retries)45437
Realtime presence and payouts (sessions, leaderboard, pub/sub, WATCH, lock)103197

Plain typed reads and writes come out about even. What Benni saves is the code around them: the raw column carries a sliding-window limiter, a read-through cache with single-flight, a token-fenced lock, and a queue with heartbeat leases, six hand-written Lua scripts in total. Reach for BullMQ and a limiter package instead and the counts converge again, at the price of several more dependencies that still hand your data back as string | null.

Nine ordinary Redis bugs planted in both versions (a typo’d hash field, a wrong value type, a missing required field, a read of an undeclared field, a nullable read treated as non-null, a counter reply used as a string, the wrong store kind, an undeclared event shape published to a typed channel, and a number member in a string-member sorted set) were nine compile errors through Benni and four through the raw version, which had a hand-written typed edge of its own. The five it missed were the quiet ones: the typo added a second field rather than replacing one, a date string in a numeric slot read back as NaN, a partial write left a partial record, and a field nobody writes read as undefined. Only the wrong store kind threw.

Nothing you can measure at runtime: 2,000 sequential ops against a local Redis, seven interleaved reps, medians of 166 ms for Benni’s hset against 162 ms for node-redis’s hSet, and 171 ms against 161 ms for the read. Three to six percent, inside the run-to-run spread, on a loopback with no real round trip to hide behind.

At compile time it is cheaper than not using it. The same three apps under tsc --extendedDiagnostics:

TypesInstantiationsCheck time
Benni versions8,76613,7740.15s
Raw node-redis versions33,695198,0610.54s

A schema layer sounds like something that slows an editor down. node-redis’s own command generics cost roughly 14 times the type instantiations that Benni’s typed surface does, so in practice the schema layer is the cheap part.