Run Redis locally with Docker, connect from Next.js with a shared ioredis client, and use the core commands — SET, GET, DEL, and expiry with TTL. Everything else builds on these.
Redis is an in-memory key/value store — it keeps data in RAM, which is why reads come back in well under a millisecond. The fastest way to get one running is Docker: this starts Redis on its default port 6379 and gives you a redis-cli to poke at it. Keep this container running while you work through the course.
# Open an interactive prompt against it
docker exec -it redis redis-cli
# Try a few commands:
127.0.0.1:6379> SET greeting "hello"
OK
127.0.0.1:6379> GET greeting
"hello"ioredis is the most-used Redis client for Node. Because it talks to Redis over a raw network connection, it only runs in server code — a Route Handler, Server Component, or Server Action — never a Client Component. The Next.js catch: dev mode reloads your modules on every save, so a plain new Redis() at the top of a file would open a fresh connection each time and pile them up. The fix is a singleton — create the client once in a shared module, stash it on globalThis, and import that everywhere. The server-only package makes sure this file never gets bundled into the browser.
$ pnpm add ioredis// lib/redis.ts — one shared client for the whole app
import 'server-only'
import Redis from 'ioredis'
// Reuse a single connection. In dev, Next.js reloads modules on every
// save; without this, each reload would open another connection.
const globalForRedis = globalThis as unknown as { redis?: Redis }
export const redis =
globalForRedis.redis ??
new Redis(process.env.REDIS_URL ?? 'redis://127.0.0.1:6379')
if (process.env.NODE_ENV !== 'production') globalForRedis.redis = redis// app/api/greeting/route.ts — import the shared client in server code
import { redis } from '@/lib/redis'
export async function GET() {
await redis.set('greeting', 'hello')
const value = await redis.get('greeting') // 'hello'
await redis.del('greeting') // remove the key
return Response.json({ value })
}What turns a key/value store into a cache is expiry. A TTL (time to live) tells Redis to delete a key automatically after N seconds, so cached data cannot go stale forever. Set it inline with the EX option when you write, or add it later with expire. ttl() tells you the seconds remaining (-2 means the key is gone, -1 means it has no expiry).
// Write a value AND its TTL in one call: expires in 60 seconds.
await redis.set('session:abc', 'user-42', 'EX', 60)
await redis.ttl('session:abc') // ~60, counting down
// Add or change an expiry on an existing key:
await redis.expire('session:abc', 30)
// After it expires, the key is simply gone:
await redis.get('session:abc') // null once 30s pass