Copyright (c) 2025 Michael Welter me@mikinho.com
A better Redis Fastify plugin that uses the official Redis library
A lightweight Fastify plugin that exposes a single node‑redis client (redis package) on your Fastify instance and handles connection lifecycle (connect → ready → reconnect → close) for you.
- ✅ Uses the official
redisclient (not ioredis) - ✅ Clean Fastify integration with proper startup/shutdown hooks
- ✅ Simple API:
fastify.rediseverywhere in your app
If you are looking for the ioredis‑based plugin, see
@fastify/redis.
Requires Node.js 20 or newer, Fastify 5, node-redis 6, and Redis server 7.2 or newer. Install the package and its Redis peer dependency:
npm install @ynode/redis redisRegister the plugin with your Fastify instance. The plugin consumes namespace and startupTimeout; all other options are passed to the underlying node-redis createClient method.
import Fastify from "fastify";
import fastifyRedis from "@ynode/redis";
const fastify = Fastify({
logger: true,
});
// Register the plugin with options
await fastify.register(fastifyRedis, {
url: "redis://localhost:6379",
});
// Access the redis client from the fastify instance
fastify.get("/", async (request, reply) => {
const value = await fastify.redis.get("mykey");
return { key: "mykey", value: value };
});
const start = async () => {
try {
await fastify.listen({ port: 3000 });
} catch (err) {
fastify.log.error(err);
process.exit(1);
}
};
start();This plugin manages Redis connection lifecycle using Fastify hooks:
- Connects during Fastify startup (
onReady) - Closes the Redis client during Fastify shutdown (
onClose)
Startup is fail-fast. If Redis cannot be reached (or startup metadata commands fail), fastify.listen() rejects and the server will not start.
Startup is bounded to 10 seconds by default. Set startupTimeout: 0 to use node-redis connection behavior without a plugin-level deadline.
Use withNamespace(namespace) as the default, concurrency-safe way to scope keys. It returns a scoped client view without mutating global fastify.redis.namespace.
await fastify.register(fastifyRedis, {
url: "redis://localhost:6379",
});
const tenantRedis = fastify.redis.withNamespace("codex");
await tenantRedis.set("status", "online"); // writes "codex:status"
await tenantRedis.get("status"); // reads "codex:status"Scoped clients can safely coexist:
const tenantA = fastify.redis.withNamespace("alpha");
const tenantB = fastify.redis.withNamespace("beta");
await tenantA.set("counter", "1"); // alpha:counter
await tenantB.set("counter", "1"); // beta:counterThe mutable fastify.redis.namespace property is still supported for backward compatibility:
fastify.redis.namespace = "klingon";
await fastify.redis.set("status", "battle-ready"); // writes "klingon:status"If future node-redis internals change in a way that prevents safe namespace interception, this plugin now fails fast at startup with REDIS_NAMESPACE_INCOMPATIBLE_CLIENT instead of silently writing unprefixed keys.
To bypass namespacing for specific operations, use raw (works for base and scoped clients):
await fastify.redis.raw.get("status"); // reads the literal key "status" (no prefix)
await fastify.redis.raw.set("status", "manual"); // writes key "status" (no prefix)
const tenantRedis = fastify.redis.withNamespace("codex");
await tenantRedis.raw.get("status"); // still unprefixedAdvanced flows inherit the scope of the client that creates them:
const tenantRedis = fastify.redis.withNamespace("codex");
const transaction = tenantRedis.multi();
transaction.set("status", "ready").get("status");
await transaction.exec(); // operates on codex:status
const pipeline = tenantRedis.multi();
pipeline.set("counter", "1").get("counter");
await pipeline.execAsPipeline(); // operates on codex:counter
const rawTransaction = tenantRedis.raw.multi();
rawTransaction.set("status", "literal");
await rawTransaction.exec(); // operates on literal "status"Hash, set, and sorted-set scan iterators retain the scope of the client that creates them. Logical keys are always prefixed, even when they already begin with the namespace text; use raw when you intentionally have a physical Redis key.
Namespacing is a key-rewriting convenience, not an authorization boundary. Database-wide commands such as SCAN, KEYS, and RANDOMKEY operate on the physical database and are not tenant-filtered. A client returned by duplicate() is also independent and is not namespaced or closed by this plugin. Use Redis ACLs or separate databases/instances when hostile tenants must be isolated.
Commands whose key positions Redis reports as movable are rewritten when the plugin has an exact resolver. If an active namespace cannot be applied safely, the command fails closed instead of running against unprefixed keys; use raw only when that database-wide access is intentional.
For Redis modules or custom commands that are not available through COMMAND introspection, provide explicit key metadata with namespaceCommands:
await fastify.register(fastifyRedis, {
namespace: "codex",
namespaceCommands: {
MODULEKEY: { firstKey: 1, lastKey: 1 },
MODULEMSET: { firstKey: 1, lastKey: -1, step: 2 },
"MODULE.INFO": { keyless: true },
},
});
fastify.redis.registerNamespaceCommand("MODULEGET", { firstKey: 1, lastKey: 1 });Command indexes are one-based and refer to the arguments after the command name. Use { keyless: true } only for commands whose arguments are never Redis keys.
Namespacing rewrites command inputs, not Redis replies. Commands that return key names, such as the pop families, can therefore return their physical prefixed names.
This plugin exposes simple probe helpers:
fastify.redis.readiness(): lightweight state snapshotfastify.redis.healthcheck(): ping-based health check that never throws
const readiness = fastify.redis.readiness();
// { isOpen: true, isReady: true, namespace: "codex" }
const health = await fastify.redis.healthcheck();
// { ok: true, ping: "PONG", latencyMs: 1, isOpen: true, isReady: true, namespace: "codex" }name(string, optional): connection name used with RedisCLIENT SETNAME. Default:@ynode/redisnamespace(string, optional): key prefix for Redis commands that operate on keys.:is reserved as the separator; a trailing colon is normalized away, while embedded colons are rejected. Example:namespace: "codex"prefixes keys ascodex:<key>.namespaceCommands(objectorMap, optional): custom command key metadata for Redis modules or deployments whereCOMMANDintrospection is unavailable. Each command can define{ firstKey, lastKey, step }key positions or{ keyless: true }.startupTimeout(number, default:10000): maximum startup time in milliseconds. Set to0to disable the plugin deadline.
All other options are passed directly to the createClient function from the official redis library.
For a full list of available options, please see the official node-redis documentation.
This package ships TypeScript declarations, including Fastify instance augmentation for fastify.redis.
import Fastify from "fastify";
import fastifyRedis from "@ynode/redis";
const app = Fastify();
await app.register(fastifyRedis, { url: "redis://localhost:6379" });
await app.redis.set("health", "ok");npm testruns unit and integration tests. Usenpm run lintandnpm run format:checkfor static checks.- Integration tests use
REDIS_URLwhen provided. - If
REDIS_URLis not set, tests try to start a localredis-serverautomatically. - CI runs on push and pull request, starts a Redis service, and executes formatting, lint, and test gates.
This project is licensed under the MIT License.