Back to Blog
Lesson 8 of the Redis: Redis Essentials & Data Types course
DatabasesJuly 26, 20263 min read

Building a Simple API Response Cache with Node.js and Redis

Learn to build a production-ready API response cache. Reduce database load and slash latency by implementing the cache-aside pattern using Redis and Node.js.

RedisCachingNode.jsAPIOptimizationDatabase Load
Elevated view of a unique building facade featuring red cube patterns.

Previously in this course, we covered implementing expiration and TTL to ensure our data doesn't persist forever. Now, we're putting those concepts to work by building a practical caching layer for our API.

When your application receives a request, querying the database is often the most expensive operation in the lifecycle. By storing the result of these queries in Redis, you can serve subsequent requests in milliseconds, effectively shielding your primary database from unnecessary load.

The Cache-Aside Pattern

In a typical API, we use the "cache-aside" pattern. The application logic is responsible for checking if the data exists in the cache:

  1. Check: The app checks Redis for the requested key.
  2. Hit: If the key exists, return the cached value immediately.
  3. Miss: If the key is missing, fetch the data from the database, store it in Redis for future use, and return the result.

This approach is highly effective for read-heavy workloads, much like the strategies discussed in LLM Caching Strategies to Slash Latency and API Costs.

Implementing an API Response Cache

To implement this, we'll use the ioredis client (which you configured in Setting Up the Backend Project Baseline). We will store our API responses as JSON strings.

JAVASCRIPT
const Redis = require(CE9178">'ioredis');
const redis = new Redis();

async function getProductData(productId) {
  const cacheKey = CE9178">`product:${productId}`;

  // 1. Check if the key exists
  const cachedData = await redis.get(cacheKey);

  if (cachedData) {
    console.log(CE9178">'Cache Hit!');
    return JSON.parse(cachedData);
  }

  // 2. Cache Miss: Fetch from DB (simulated)
  console.log(CE9178">'Cache Miss. Fetching from database...');
  const product = await db.query(CE9178">'SELECT * FROM products WHERE id = ?', [productId]);

  // 3. Store the result in Redis with a TTL (e.g., 60 seconds)
  await redis.set(cacheKey, JSON.stringify(product), CE9178">'EX', 60);

  return product;
}

Hands-on Exercise

Modify your existing API route handler to implement the cache-aside pattern for a "User Profile" endpoint:

  1. Define a unique key for the user, such as user:profile:{userId}.
  2. Use GET to check the cache.
  3. If null, mock a database call (using setTimeout to mimic latency), then SET the result into Redis with a 30-second expiration.
  4. Verify the "Cache Hit" vs "Cache Miss" logs in your console by hitting the endpoint twice in rapid succession.

Common Pitfalls

  • Serialization Errors: Always remember that Redis stores strings. If you try to save a JavaScript object directly, you'll end up with [object Object] in your cache. Always use JSON.stringify() on save and JSON.parse() on retrieval.
  • The "Thundering Herd": If your cache expires and you have thousands of concurrent requests for the same key, all of them might hit the database at once. In production, we often implement "locking" or "probabilistic early recomputation" to prevent this, but for now, focus on simple TTLs.
  • Ignoring Cache Invalidation: Caching is easy; invalidating it is hard. If your database record updates, your cache will serve stale data until the TTL expires. Plan your TTLs based on how often your data actually changes.

Frequently Asked Questions

Why not just cache everything? RAM is expensive and finite. Caching everything leads to "cache eviction," where Redis deletes old keys to make room for new ones. Focus on high-traffic, slow-to-calculate queries.

Is Redis always faster than a database? For simple key-value lookups, yes. Databases are optimized for complex relational integrity and persistence; Redis is optimized for raw speed and in-memory access.

What happens if Redis goes down? Your application should be resilient. If the cache layer fails, your code should treat it as a "cache miss" and fall back to the database, ensuring your API remains functional even if your cache is unavailable.

Recap

We've successfully moved from raw data storage to an optimization pattern. By checking for keys before hitting the database, we can drastically reduce our database load. Using the cache-aside pattern ensures our API remains performant and scalable.

Up next: We'll dive into Improving Cache Hit Ratios by refining our miss-handling logic and managing cache invalidation.

Similar Posts