Build an API Cache Layer with Redis
Cache external API responses in Redis and measure the speed difference.
Introduction
30 Second Summary
Ever notice how some websites load data instantly while others make you wait? Behind the scenes, the fast ones store frequently requested data in memory so it can be served in milliseconds instead of waiting for a slow external service.
In this project, you will build a Node.js API that caches weather data using Redis. You will see the speed difference firsthand as requests drop from hundreds of milliseconds to under 5ms.
What You'll Build
A Node.js Express API server that fetches weather data from an external API and caches responses in Redis for instant retrieval on repeat requests.
By the end of this project, you'll have:
- An Express server with a /weather/:city endpoint that fetches live weather data from a free external API.
- A Redis caching layer using the cache-aside pattern that stores responses and serves them in under 5ms on repeat requests.
- A /stats endpoint showing real-time cache hit and miss counts so you can see exactly how effective your cache is.
- Secret Mission: Add cache invalidation and cache warming to handle production-grade cache management.
Are there any prerequisites?
None. This project includes full setup instructions for every tool you need, including Node.js and Redis. No prior experience with caching or server development is required.
Install Node.js and Memurai
Your caching API needs two core pieces of infrastructure to run. Node.js is the JavaScript runtime that powers your server. Memurai is a Redis-compatible server that runs natively on Windows, giving you an in-memory data store for your cache.
In this step, you will install both tools and verify they are working. You will also set up VS Code as your code editor with a built-in terminal.
In this step, get ready to:
- Install Node.js and verify it in the terminal.
- Install Memurai Developer Edition and confirm it is running.
- Set up VS Code with a working integrated terminal.
Install Node.js
Node.js lets you run JavaScript outside of a browser. Your Express API server will run on Node.js, and npm (Node's package manager) will install your project dependencies later.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type PowerShell and press Enter to open it.
- Check if Node.js is already installed by running this command:
node --version
✔️ I see a version number
You are all set. Node.js is already installed on your machine.
- Confirm npm is also available by running:
npm --version
You should see a version number printed in the terminal. Both tools are ready to go.
ⓧ Command not found
- Open your browser and go to nodejs.org.
- Click the LTS download button (this is the stable, recommended version).
- Run the downloaded installer and accept all the default settings.
- Close your PowerShell window and open a new one (the installer updates your system PATH, so a fresh window is needed).
- Verify the install by running:
node --version
- Also confirm npm is available by running:
npm --version
Both commands should print a version number.
Still seeing command not found?
Make sure you closed and reopened PowerShell after the install. The installer registers node and npm on your system PATH, but existing terminal windows do not pick up the change automatically.
If it still fails, try restarting your computer so Windows refreshes the PATH completely. Help me troubleshoot my Node.js installation
Install Memurai Developer Edition
Memurai is a Redis-compatible in-memory data store that runs as a native Windows Service. It listens on port 6379 (the standard Redis port) and starts automatically when your computer boots.
Why Memurai instead of Redis directly?
Redis does not run natively on Windows. The official Redis documentation recommends Memurai as the Windows-compatible alternative. It supports the full Redis 7.2 API, so every Redis command you learn here works the same way on a Linux Redis server in production.
The Developer Edition is free for development and testing. It has a 10-day maximum uptime before requiring a service restart, which is more than enough for this project.
- Open your browser and go to memurai.com/get-memurai.
- Download the Memurai Developer Edition MSI installer.
- Run the installer and accept the default settings.
What does the installer do?
The MSI installer registers Memurai as a Windows Service. This means it runs in the background automatically. You do not need to start it manually each time you work on this project.
- Back in PowerShell, verify Memurai is running by running this command:
memurai-cli ping
You should see PONG printed in the terminal. This confirms Memurai is running and accepting connections.
Not seeing PONG?
If you see a "not recognized" error, close PowerShell and open a new window. The installer needs a fresh session to register the memurai-cli command on your PATH.
If PowerShell recognizes the command but you get a connection error, open the Windows Services app and check that the Memurai service is listed and its status is Running. If it is stopped, right-click it and select Start.
Help me troubleshoot my Memurai installation
Set up VS Code
VS Code gives you a code editor and an integrated terminal in one window. You will write all your code and run your server from inside VS Code for the rest of this project.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Visual Studio Code and press Enter to open it.
✔️ VS Code opens
Great, VS Code is already installed. Move on to opening the integrated terminal below.
ⓧ Nothing opens or app not found
- Open your browser and go to code.visualstudio.com.
- Click the download button and run the installer.
- Accept all default settings during installation.
- Once installed, open your search bar again, type Visual Studio Code, and press Enter.
- Open the integrated terminal by pressing Ctrl+` (Windows) or Cmd+` (macOS).
- Confirm Node.js is accessible from inside VS Code by running:
node --version
You should see the same version number you saw earlier in PowerShell. This confirms VS Code's terminal can reach all the tools you just installed.
VS Code terminal not recognizing node?
If VS Code was already open when you installed Node.js, close VS Code completely and reopen it. VS Code inherits the system PATH at launch, so it needs a fresh start to pick up new installations.
Help me get VS Code to recognize my Node.js installation
Your development environment is ready. Next up, you will create the project folder, install your dependencies, and connect to Redis from code.
Scaffold the Project and Connect to Redis
Your tools are installed and Memurai is running. Now it is time to build the actual API server that talks to Redis.
In this step, you will create a Node.js project that connects to Redis, fetches live weather data from an external API, and caches the responses so that repeated requests return almost instantly.
In this step, get ready to:
- Create a Node.js project with Express and the Redis client library.
- Build a weather endpoint that fetches data from the Open-Meteo API.
- Add a Redis caching layer that stores responses and serves them on repeat requests.
Create the project and install dependencies
Every Node.js project starts with a folder and a package.json file that tracks your project's metadata and dependencies.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir redis-caching-api
cd redis-caching-api
- Confirm the folder exists by running:
ls
You should see the redis-caching-api folder listed in the output.
- Initialize your Node.js project by running:
npm init -y
What does this command do?
The -y flag accepts all defaults automatically. This creates a package.json file with your project's name, version, and entry point pre-filled.
- Install the two libraries your project needs by running:
npm install express redis
What are these packages?
Express is a web framework that makes it simple to define API routes and handle HTTP requests.
The redis package is the official Node.js client for Redis. It lets your code talk to the Memurai server running on your machine.
You should see output ending with "added 2 packages" (or similar) confirming the install succeeded.
Seeing permission errors or EACCES?
Make sure you are running PowerShell as a regular user (not Administrator). If the error persists, close VS Code and reopen it.
help me fix npm install permission errors on Windows
Now you need to tell Node.js that this project uses modern ES module syntax (the import keyword) instead of the older require style.
- Open the redis-caching-api folder in VS Code by clicking File in the top menu bar.
- Click Open Folder.
- Select the redis-caching-api folder on your Desktop, then click Select Folder.
- In the Explorer sidebar, click package.json to open it.
- Replace the entire contents of package.json with this:
{
"name": "redis-caching-api",
"version": "1.0.0",
"type": "module",
"description": "API cache layer with Redis",
"main": "index.js",
"scripts": {
"start": "node index.js"
},
"dependencies": {
"express": "^4.21.0",
"redis": "^5.12.1"
}
}
What changed here?
- "type": "module" tells Node.js to use ES module syntax (import/export) instead of the older require() style.
- "start": "node index.js" gives you a shortcut to run your server with npm start.
- Save package.json by pressing Ctrl+S.
Now you will create the main application file that sets up your Express server and connects to Redis.
- Create a new file called index.js by clicking the new file icon in the Explorer sidebar.
- Type index.js as the file name and press Enter.
- Add the following code to index.js to import your libraries and connect to Redis:
import { createClient } from "redis";
import express from "express";
const app = express();
const PORT = 3000;
const CACHE_TTL = 60;
// Connect to the local Redis (Memurai) server
const redisClient = await createClient()
.on("error", (err) => console.log("Redis Client Error", err))
.connect();
console.log("Connected to Redis");
What does this code do?
- createClient() creates a Redis client that talks to Memurai on port 6379 (the default).
- The .on("error", ...) handler logs connection problems instead of crashing the app.
- await .connect() establishes the connection before the server starts accepting requests.
- CACHE_TTL = 60 means cached data expires after 60 seconds.
- Below the Redis connection code, add the stats tracker and the server startup:
// Track cache performance
const stats = {
totalRequests: 0,
cacheHits: 0,
cacheMisses: 0,
};
app.listen(PORT, () => {
console.log(`Server running at http://localhost:${PORT}`);
console.log(`Try: http://localhost:${PORT}/weather/london`);
console.log(`Stats: http://localhost:${PORT}/stats`);
});
What does this code do?
- The stats object keeps a running count of total requests, cache hits, and cache misses. You will use this to measure caching performance.
- app.listen(PORT) starts the Express server on port 3000 and prints helpful URLs to the console.
- Save index.js by pressing Ctrl+S.
- Open a terminal in VS Code by pressing Ctrl+` (backtick key).
- Start the server by running:
node index.js
You should see "Connected to Redis" followed by "Server running at http://localhost:3000" in the terminal.
Seeing "Redis Client Error" or connection refused?
This means Memurai is not running. Open a new PowerShell window and run memurai-cli ping. If it does not return PONG, restart the Memurai service from the Windows Services panel.
help me fix my Redis connection error
- Stop the server by pressing Ctrl+C in the terminal. You will restart it after adding the weather route.
Create the weather route
Your server can connect to Redis, but it does not serve any data yet. You will add a route that fetches live weather data from the Open-Meteo API.
Why Open-Meteo?
Open-Meteo is a free weather API that requires no API key and no signup. This lets you focus on learning Redis caching without dealing with account registration or secret management.
- In index.js, find the line that says app.listen(PORT, () => {.
- Add the following route ABOVE the app.listen block:
app.get("/weather/:city", async (req, res) => {
const city = req.params.city.toLowerCase();
stats.totalRequests++;
try {
// Fetch city coordinates from Open-Meteo geocoding API
const geoResponse = await fetch(
`https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(city)}&count=1`
);
const geoData = await geoResponse.json();
if (!geoData.results || geoData.results.length === 0) {
return res.status(404).json({ error: "City not found" });
}
const { latitude, longitude, name, country } = geoData.results[0];
// Fetch current weather using the coordinates
const weatherResponse = await fetch(
`https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}¤t=temperature_2m,wind_speed_10m,weather_code`
);
const weatherData = await weatherResponse.json();
// Build a clean result object
const result = {
city: name,
country: country,
temperature: weatherData.current.temperature_2m,
windSpeed: weatherData.current.wind_speed_10m,
weatherCode: weatherData.current.weather_code,
cachedAt: new Date().toISOString(),
};
res.json({ source: "api", data: result });
} catch (error) {
res.status(500).json({ error: "Failed to fetch weather data" });
}
});
What does this route do?
- The :city in the URL path is a route parameter. Visiting /weather/london sets req.params.city to "london".
- The route first calls the Open-Meteo geocoding API to convert a city name into latitude/longitude coordinates.
- It then calls the weather forecast API with those coordinates to get current temperature and wind speed.
- The response includes "source": "api" so you can tell whether data came from the live API or from cache.
- Save index.js and restart the server by running:
node index.js
- Open your browser and visit http://localhost:3000/weather/london.
You should see a JSON response with temperature, wind speed, and "source": "api". This confirms the route is fetching live data from Open-Meteo.
Seeing "City not found" or a 500 error?
If you see "City not found", try a well-known city name like london or tokyo. If you see a 500 error, check that your computer has an active internet connection (the route needs to reach the Open-Meteo servers).
help me debug my weather route
- Stop the server with Ctrl+C before moving to the next substep.
Add the Redis caching layer
Right now, every request hits the external API. That takes hundreds of milliseconds each time. The cache-aside pattern solves this by checking Redis for a stored response before making the network call.
What is the cache-aside pattern?
The cache-aside pattern follows three steps: check the cache first, fetch from the source only on a miss, then store the fresh result in the cache for future requests.
A TTL (time-to-live) automatically expires cached data after a set number of seconds. This keeps the cache fresh without requiring manual cleanup.
- In index.js, replace the entire app.get("/weather/:city", ...) route with this updated version that includes caching:
app.get("/weather/:city", async (req, res) => {
const city = req.params.city.toLowerCase();
const cacheKey = `weather:${city}`;
stats.totalRequests++;
try {
// Check Redis cache first
const cached = await redisClient.get(cacheKey);
if (cached) {
// Cache HIT - return stored data immediately
stats.cacheHits++;
console.log(`Cache HIT for ${city}`);
return res.json({ source: "cache", data: JSON.parse(cached) });
}
// Cache MISS - fetch from external API
stats.cacheMisses++;
console.log(`Cache MISS for ${city}`);
const geoResponse = await fetch(
`https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(city)}&count=1`
);
const geoData = await geoResponse.json();
if (!geoData.results || geoData.results.length === 0) {
return res.status(404).json({ error: "City not found" });
}
const { latitude, longitude, name, country } = geoData.results[0];
const weatherResponse = await fetch(
`https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}¤t=temperature_2m,wind_speed_10m,weather_code`
);
const weatherData = await weatherResponse.json();
const result = {
city: name,
country: country,
temperature: weatherData.current.temperature_2m,
windSpeed: weatherData.current.wind_speed_10m,
weatherCode: weatherData.current.weather_code,
cachedAt: new Date().toISOString(),
};
// Store in Redis with a 60-second TTL
await redisClient.set(cacheKey, JSON.stringify(result), { EX: CACHE_TTL });
res.json({ source: "api", data: result });
} catch (error) {
res.status(500).json({ error: "Failed to fetch weather data" });
}
});
What changed in this route?
- cacheKey creates a unique Redis key for each city (e.g. weather:london).
- redisClient.get(cacheKey) checks if Redis already has a stored response for this city.
- If cached data exists, the route returns it immediately without calling the external API. This is the speed boost.
- redisClient.set(cacheKey, ..., { EX: CACHE_TTL }) stores the result in Redis with a 60-second expiration.
Now add a /stats endpoint so you can see cache performance at any time.
- Add this route between the /weather/:city route and the app.listen block:
app.get("/stats", (req, res) => {
res.json({
...stats,
hitRate:
stats.totalRequests > 0
? ((stats.cacheHits / stats.totalRequests) * 100).toFixed(1) + "%"
: "0%",
});
});
What does the stats endpoint show?
This returns a JSON object with the total number of requests, how many were cache hits, how many were misses, and the overall hit rate as a percentage. It gives you a quick way to see how effective your cache is.
- Save index.js and start the server again:
node index.js
- Open your browser and visit http://localhost:3000/weather/london.
The first request shows "source": "api" and your terminal prints "Cache MISS for london". This request hit the external API.
- Refresh the same page in your browser.
The second request shows "source": "cache" and your terminal prints "Cache HIT for london". This response came straight from Redis without touching the external API.
- Visit http://localhost:3000/stats to see your cache performance counters.
You should see totalRequests at 2, cacheHits at 1, cacheMisses at 1, and a hitRate of "50.0%".
Still seeing "source": "api" on the second request?
Make sure Memurai is still running. Run memurai-cli ping in a separate terminal to confirm you get PONG. Also check that your redisClient.set() call includes { EX: CACHE_TTL } as the third argument.
help me debug why my Redis cache is not storing data
✔️ Awesome, I've got everything!
Great. Double check you have saved both package.json and index.js.
ⓧ I'd like to double check the full code
Here is the complete index.js file as it should look at this point:
import { createClient } from "redis";
import express from "express";
const app = express();
const PORT = 3000;
const CACHE_TTL = 60;
const redisClient = await createClient()
.on("error", (err) => console.log("Redis Client Error", err))
.connect();
console.log("Connected to Redis");
const stats = {
totalRequests: 0,
cacheHits: 0,
cacheMisses: 0,
};
app.get("/weather/:city", async (req, res) => {
const city = req.params.city.toLowerCase();
const cacheKey = `weather:${city}`;
stats.totalRequests++;
try {
const cached = await redisClient.get(cacheKey);
if (cached) {
stats.cacheHits++;
console.log(`Cache HIT for ${city}`);
return res.json({ source: "cache", data: JSON.parse(cached) });
}
stats.cacheMisses++;
console.log(`Cache MISS for ${city}`);
const geoResponse = await fetch(
`https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(city)}&count=1`
);
const geoData = await geoResponse.json();
if (!geoData.results || geoData.results.length === 0) {
return res.status(404).json({ error: "City not found" });
}
const { latitude, longitude, name, country } = geoData.results[0];
const weatherResponse = await fetch(
`https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}¤t=temperature_2m,wind_speed_10m,weather_code`
);
const weatherData = await weatherResponse.json();
const result = {
city: name,
country: country,
temperature: weatherData.current.temperature_2m,
windSpeed: weatherData.current.wind_speed_10m,
weatherCode: weatherData.current.weather_code,
cachedAt: new Date().toISOString(),
};
await redisClient.set(cacheKey, JSON.stringify(result), { EX: CACHE_TTL });
res.json({ source: "api", data: result });
} catch (error) {
res.status(500).json({ error: "Failed to fetch weather data" });
}
});
app.get("/stats", (req, res) => {
res.json({
...stats,
hitRate:
stats.totalRequests > 0
? ((stats.cacheHits / stats.totalRequests) * 100).toFixed(1) + "%"
: "0%",
});
});
app.listen(PORT, () => {
console.log(`Server running at http://localhost:${PORT}`);
console.log(`Try: http://localhost:${PORT}/weather/london`);
console.log(`Stats: http://localhost:${PORT}/stats`);
});
And here is package.json:
{
"name": "redis-caching-api",
"version": "1.0.0",
"type": "module",
"description": "API cache layer with Redis",
"main": "index.js",
"scripts": {
"start": "node index.js"
},
"dependencies": {
"express": "^4.21.0",
"redis": "^5.12.1"
}
}
Your caching layer is working. Next up, you will add performance timing so you can see exactly how many milliseconds each request saves.
Measure Performance and Add Stats
Your caching layer is working. Requests that hit the cache return instantly, while cache misses go out to the Open-Meteo API. But right now, you have no way to see the speed difference.
In this step, you will add timing instrumentation to your route handler. Every request will log its response time to the console, letting you compare cache hits (under 5ms) against cache misses (hundreds of milliseconds) in real time.
In this step, get ready to:
- Add response time logging with console.time and console.timeEnd.
- Review your cache stats tracking and the /stats endpoint.
- Test the full flow and observe the performance difference between cached and uncached requests.
Add performance timing to your route handler
Node.js has a built-in timing API. console.time() starts a timer with a label. console.timeEnd() stops that timer and prints the elapsed milliseconds to the console.
For accurate measurements, you need to start the timer once at the beginning of the handler and stop it at every possible exit point (cache hit, city not found, success, and error).
- In index.js, replace your entire /weather/:city route handler with this updated version that includes timing:
app.get("/weather/:city", async (req, res) => {
const city = req.params.city.toLowerCase();
const cacheKey = `weather:${city}`;
stats.totalRequests++;
// Start timing this request
console.time(`Request: ${city}`);
try {
const cached = await redisClient.get(cacheKey);
if (cached) {
stats.cacheHits++;
// Stop timer before returning cached response
console.timeEnd(`Request: ${city}`);
console.log(`Cache HIT for ${city}`);
return res.json({ source: "cache", data: JSON.parse(cached) });
}
stats.cacheMisses++;
console.log(`Cache MISS for ${city}`);
const geoResponse = await fetch(
`https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(city)}&count=1`
);
const geoData = await geoResponse.json();
if (!geoData.results || geoData.results.length === 0) {
// Stop timer before returning 404
console.timeEnd(`Request: ${city}`);
return res.status(404).json({ error: "City not found" });
}
const { latitude, longitude, name, country } = geoData.results[0];
const weatherResponse = await fetch(
`https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}¤t=temperature_2m,wind_speed_10m,weather_code`
);
const weatherData = await weatherResponse.json();
const result = {
city: name,
country: country,
temperature: weatherData.current.temperature_2m,
windSpeed: weatherData.current.wind_speed_10m,
weatherCode: weatherData.current.weather_code,
cachedAt: new Date().toISOString(),
};
await redisClient.set(cacheKey, JSON.stringify(result), { EX: CACHE_TTL });
// Stop timer after storing in cache
console.timeEnd(`Request: ${city}`);
res.json({ source: "api", data: result });
} catch (error) {
// Stop timer even on errors
console.timeEnd(`Request: ${city}`);
res.status(500).json({ error: "Failed to fetch weather data" });
}
});
What does this code do?
console.time(`Request: ${city}`) starts a named timer as soon as the handler begins processing the request.
console.timeEnd(`Request: ${city}`) appears at four exit points: the cache hit return, the 404 return, the successful API response, and the error catch. Wherever the handler finishes, the timer stops and prints the elapsed time.
- Save index.js.
- Stop your server if it is still running (press Ctrl+C in the terminal), then restart it by running:
node index.js
- You should see Connected to Redis and the server URL logged in the terminal.
Server not starting?
- Check that you have console.timeEnd (with a capital E) not console.timeend. JavaScript is case-sensitive.
- Make sure every backtick string uses the same label: Request: ${city}. A mismatched label will not throw an error but will print a warning instead of a time.
- Confirm Memurai is still running with memurai-cli ping.
Help me debug this
✔️ Awesome, I've got everything!
Great. Make sure you have saved index.js before moving on.
ⓧ I'd like to double check the full code
import { createClient } from "redis";
import express from "express";
const app = express();
const PORT = 3000;
const CACHE_TTL = 60;
const redisClient = await createClient()
.on("error", (err) => console.log("Redis Client Error", err))
.connect();
console.log("Connected to Redis");
const stats = {
totalRequests: 0,
cacheHits: 0,
cacheMisses: 0,
};
app.get("/weather/:city", async (req, res) => {
const city = req.params.city.toLowerCase();
const cacheKey = `weather:${city}`;
stats.totalRequests++;
console.time(`Request: ${city}`);
try {
const cached = await redisClient.get(cacheKey);
if (cached) {
stats.cacheHits++;
console.timeEnd(`Request: ${city}`);
console.log(`Cache HIT for ${city}`);
return res.json({ source: "cache", data: JSON.parse(cached) });
}
stats.cacheMisses++;
console.log(`Cache MISS for ${city}`);
const geoResponse = await fetch(
`https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(city)}&count=1`
);
const geoData = await geoResponse.json();
if (!geoData.results || geoData.results.length === 0) {
console.timeEnd(`Request: ${city}`);
return res.status(404).json({ error: "City not found" });
}
const { latitude, longitude, name, country } = geoData.results[0];
const weatherResponse = await fetch(
`https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}¤t=temperature_2m,wind_speed_10m,weather_code`
);
const weatherData = await weatherResponse.json();
const result = {
city: name,
country: country,
temperature: weatherData.current.temperature_2m,
windSpeed: weatherData.current.wind_speed_10m,
weatherCode: weatherData.current.weather_code,
cachedAt: new Date().toISOString(),
};
await redisClient.set(cacheKey, JSON.stringify(result), { EX: CACHE_TTL });
console.timeEnd(`Request: ${city}`);
res.json({ source: "api", data: result });
} catch (error) {
console.timeEnd(`Request: ${city}`);
res.status(500).json({ error: "Failed to fetch weather data" });
}
});
app.get("/stats", (req, res) => {
res.json({
...stats,
hitRate:
stats.totalRequests > 0
? ((stats.cacheHits / stats.totalRequests) * 100).toFixed(1) + "%"
: "0%",
});
});
app.listen(PORT, () => {
console.log(`Server running at http://localhost:${PORT}`);
console.log(`Try: http://localhost:${PORT}/weather/london`);
console.log(`Stats: http://localhost:${PORT}/stats`);
});
Review your cache stats tracking
In the previous step, you added a stats object and a /stats endpoint to your server. These track how effectively your cache is performing over time.
How does the stats tracking work?
- stats.totalRequests increments on every incoming request to /weather/:city.
- stats.cacheHits increments when redisClient.get() returns data (the response came from Redis without hitting the external API).
- stats.cacheMisses increments when the cache has no data for that city, forcing a fetch to Open-Meteo.
- The /stats endpoint returns these counts plus a calculated hitRate percentage showing what fraction of requests were served from cache.
Test the full flow
Now you can see the caching layer in action. You will make two requests for the same city and compare the response times logged in your terminal.
- Open your browser and navigate to:
http://localhost:3000/weather/london
- Look at your terminal. You should see a line like Request: london: 200ms (the exact number varies) followed by Cache MISS for london. This confirms the request hit the external API.
- Refresh the same page in your browser (or navigate to the same URL again).
- Check your terminal again. This time you should see something like Request: london: 1ms followed by Cache HIT for london. The response came directly from Redis, skipping the network call entirely.
Why is the difference so dramatic?
The first request travels over the internet to Open-Meteo's servers and back. The second request reads a string from Redis running on your local machine. Local memory access is measured in microseconds. Network round-trips are measured in hundreds of milliseconds.
- Now check your stats by navigating to:
http://localhost:3000/stats
- You should see a JSON response showing totalRequests as 2, cacheHits as 1, cacheMisses as 1, and hitRate as 50.0%.
Not seeing timing in the console?
- Make sure you restarted the server after saving your changes. Code changes do not take effect until you stop the server with Ctrl+C and run node index.js again.
- If you see Cache HIT on your very first request, a previous test cached that city. Try a different city like /weather/tokyo to get a fresh cache miss.
Help me debug my timing output
You have proven the power of caching with hard numbers. Next up, you will extend this project with cache invalidation and cache warming in the Secret Mission.
Secret mission
Cache Invalidation and Cache Warming
Your caching API works great, but production systems need more control. What happens when cached data becomes stale and you need to force a refresh? What if you want popular cities pre-loaded so the first user never waits? Build two production-grade features that give you full control over your cache.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely on your local machine, so there are no ongoing cloud costs. Decide whether to keep your resources as they are, pause them to come back later, or delete them entirely.
Resources you used:
- Project folder containing index.js, package.json, and node_modules.
- Memurai Developer Edition (running as a Windows Service on port 6379).
- Node.js (installed from nodejs.org).
Keep everything running
No action needed. Choose this if you are still actively building or want to experiment with more caching patterns.
- Your Express server is only running while you have the node index.js process active in your terminal. Close the terminal window to stop it.
- Memurai continues running as a Windows Service in the background. It uses minimal memory when idle.
- Node.js remains installed and available for other projects.
Pause - I'll come back to this later
Stop running processes to free up memory, but keep your files so you can resume later.
- Close the terminal running your Express server (or press Ctrl+C to stop it).
- Stop the Memurai Windows Service by opening PowerShell as Administrator and running:
memurai.exe --service-stop
- Your project folder and all code remain on disk. To resume later, start the Memurai service again and run node index.js from your project folder.
Delete - I don't want to use this again
Remove all project resources and uninstall tools you no longer need.
- Delete your project folder by opening PowerShell and running:
Remove-Item -Recurse -Force ~\Desktop\redis-caching-api
- Uninstall the Memurai Windows Service by opening PowerShell as Administrator and running:
memurai.exe --service-uninstall
- Uninstall the Memurai application by opening Settings, selecting Apps, then Apps & features.
- Search for Memurai, click it, and select Uninstall.
- To uninstall Node.js, search for Node.js in the same Apps & features list, click it, and select Uninstall. Only do this if you have no other projects using Node.js.
That's a Wrap!
That's a Wrap!
Nice work! You just built a production-style caching layer from scratch using Redis and Node.js. Your API now serves cached weather data in a fraction of the time it takes to hit the external source.
You've learned how to:
- Built an Express API server with a /weather/:city endpoint that fetches live data from the Open-Meteo API.
- Implemented the cache-aside pattern with Redis, storing responses with a 60-second TTL to keep data fresh without manual intervention.
- Measured response time differences between cache hits and misses, proving that cached requests return in under 5ms compared to hundreds of milliseconds.
- Secret Mission: Added cache invalidation, cache warming, and TTL monitoring to make your caching layer production-ready.
Ready to quiz yourself?