# Nfs Add Cache

> Retrofit Redis-backed cache handling into an existing project scaffolded with nextjs-fullstack-starter. Use when the user wants to add Redis, share Next.js cache across processes / deploys, speed up frequent reads, or any 'add caching to this project' request. Adds ioredis, a docker-compose redis service, wires Next.js's experimental cacheHandler to use Redis, adds a typed cache helper at src/server/lib/cache.ts, and REDIS_URL env. Refuses to run on projects that don't have the scaffolded structure.

- Skill: `juncoding/nfs-add-cache` (Agent Skill)
- Install (CLI): `npx skillmds@latest add juncoding/nfs-add-cache`
- Raw SKILL.md: https://api.skillmd.com/api/skills/juncoding/nfs-add-cache/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: juncoding (https://skillmd.com/u/juncoding)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/juncoding/nfs-add-cache

---


# Retrofit Redis-backed cache

Use when a project from `nextjs-fullstack-starter` (which uses Next.js's in-process cache by default) outgrows that. Typical triggers:

- The user is deploying multiple Next.js processes / containers and wants a shared cache.
- A blue-green deploy throws away the warm cache every release and that's becoming painful.
- The user wants to use `src/server/lib/cache.ts` as a general-purpose Redis helper for non-Next.js caches too (rate limits, queues, etc.).

If the project ISN'T at one of those tipping points, push back gently — Next.js's default cache is fine until measurably not.

## Pre-flight checks

Refuse if any of these is true:

1. `src/server/modules/` doesn't exist — the project wasn't scaffolded with this plugin.
2. `src/server/lib/cache.ts` already exists — Redis is already wired. Suggest `/nfs-review-project` for an audit instead.

## Plan

1. Add `ioredis` to dependencies. Add `@neshca/cache-handler` if the user wants the Next.js cache itself to go through Redis.
2. Add `redis` service to `docker-compose.yml` (under the `cache` profile so it doesn't always run).
3. Add `REDIS_URL` to `src/env.ts` and `.env.example`.
4. Write `src/server/lib/cache.ts` — a typed wrapper exposing `get`, `set`, `del`, and a `cached(fn, key, ttl)` helper.
5. (Optional) Configure `next.config.ts`'s `cacheHandler` to use `@neshca/cache-handler` against Redis.
6. Update `CLAUDE.md` with the new pattern.

## The cache helper

```ts
// src/server/lib/cache.ts
import "server-only";
import Redis from "ioredis";
import { env } from "@/env";
import { logger } from "@/server/lib/logger";

const log = logger.child({ module: "cache" });

let client: Redis | null = null;

function getClient(): Redis {
  if (!client && env.REDIS_URL) {
    client = new Redis(env.REDIS_URL, {
      lazyConnect: true,
      maxRetriesPerRequest: 2,
      retryStrategy: (times) => Math.min(times * 100, 2000),
    });
    client.on("error", (err) => log.warn({ err }, "redis error"));
  }
  if (!client) throw new Error("REDIS_URL not set");
  return client;
}

export const cache = {
  async get<T>(key: string): Promise<T | null> {
    try {
      const raw = await getClient().get(key);
      return raw ? (JSON.parse(raw) as T) : null;
    } catch (err) {
      log.warn({ err, key }, "cache.get failed");
      return null;
    }
  },

  async set<T>(key: string, value: T, ttlSeconds?: number): Promise<void> {
    try {
      const raw = JSON.stringify(value);
      if (ttlSeconds) await getClient().setex(key, ttlSeconds, raw);
      else await getClient().set(key, raw);
    } catch (err) {
      log.warn({ err, key }, "cache.set failed");
    }
  },

  async del(key: string): Promise<void> {
    try {
      await getClient().del(key);
    } catch (err) {
      log.warn({ err, key }, "cache.del failed");
    }
  },

  /**
   * Read-through cache wrapper. Returns the cached value if present, else
   * computes via `compute`, stores, and returns.
   */
  async cached<T>(key: string, ttlSeconds: number, compute: () => Promise<T>): Promise<T> {
    const hit = await this.get<T>(key);
    if (hit !== null) return hit;
    const fresh = await compute();
    await this.set(key, fresh, ttlSeconds);
    return fresh;
  },
};
```

## When to reach for `cache.cached` vs. `"use cache"` directive

| Need | Use |
|---|---|
| Cache a Server Component / service read for Next.js to invalidate by tag | `"use cache"` + `cacheTag` + `updateTag` (built-in) |
| Cache something that isn't a server read (rate-limit counter, queue, ad-hoc key) | `cache.get` / `cache.set` directly |
| Cache an expensive computation across processes | `cache.cached(key, ttl, fn)` |

The two systems coexist — Next.js's `"use cache"` handles the server-render cache; `cache.*` is your own general-purpose Redis client.

## Update `CLAUDE.md`

Add to the stack list:

```markdown
- **Redis** via `ioredis`. Helper at `src/server/lib/cache.ts`. Use for non-Next.js caches (rate limits, queues, ad-hoc keys). Next.js's `"use cache"` + `updateTag` continue to handle server-render caching independently.
```

Update the architecture section to mention which keys to cache and which not (per the `caching.md` reference).

## Verification

```bash
pnpm install
docker compose --profile cache up -d   # starts the redis container
REDIS_URL=redis://localhost:6379 pnpm verify
```

## Anti-patterns to refuse

- **Replacing the Next.js cache wholesale with the ioredis helper.** They serve different purposes. Server-render caching belongs to Next.js's primitives; ad-hoc caching is what the helper is for.
- **Caching per-user data without explicit user-scoped keys.** Easy way to leak data across users. If you cache anything user-scoped, the key MUST include the userId.
- **Using `cache.*` to "fix" slow queries.** Caching is a workaround. If a query is slow, fix the query or add an index first; only cache when there's no further optimization room.

