I'm a really big fan of using redis for caching. I'm also a big fan of Next.js. A React framework that incorporates frontend code with backend code.... what isn't there to like?
I set up a project with docker-compose using next.js and redis. For the app I used the ioredis
module. I've used the redis module frequently in the past and it works really well, but I had already been playing around with ioredis, next.js, and Apollo subscriptions.
I started up my project and lo and behold..... next.js crashed... and crashed.... and crashed. Next.js was complaining about missing modules..... weird..... Ok, so I started installing modules.... a lot of modules.
Turns out next.js was loading the ioredis module in the browser even though I was only importing it in my api routes or standalone files that weren't being used in the frontend.
No matter what I tried, the app would not run with the ioredis import statement.
Ouch.....
Determined to not be defeated, I came up with novel solution.
I created a redis-proxy service that sits in between the app and the redis service. It's a simple express server that takes commands from the next.js app and forwards them to redis. Since it's express, my frontend app can use http requests for the redis commands.
/**
* This service proxies redis requests from Frontend App
* to the Redis server.
*/
const Redis = require("ioredis");
const express = require("express");
const bodyParser = require("body-parser");
const app = new express();
app.use(bodyParser.json());
// connection is async but the other apps won't send a request
// by the time the connection is made
const redis = new Redis({ host: "redis" });
app.get("/ping", (req, res) => res.send("pong"));
/**
* Catch everything so we don't have to manually make a route for
* each Redis command.
*/
app.post("*", async function (req, res) {
/**
* body is the array of arguments that were passed to the
* proxy handler so we can pass them as is to Redis
* rename as args for readability -
* I don't use this syntax too often so have to remind myself how it works.
*/
const { body: args } = req;
/**
* req.url is something like
* "/set" or "/get"
*/
const method = req.url.substr(1, req.url.length - 1);
let message = "";
let error = false;
let result = null;
if (typeof redis[method] !== "function") {
error = true;
message = `${method} is not a valid method`;
} else {
result = await redis[method](...args).catch((e) => {
error = true;
message = e.message;
return null;
});
}
res.json({ result, error, message });
});
app.listen(80);
This left me with the annoying problem of recreating a redis library for all the redis commands I would be using. For each command I would be sending the request to the redis-proxy.
Needless to say, that was way to much work for me. And then the lightbulb came on. And so, finally, after so much time, I finally had a reason to use....
PROXIES!!!!!
Yes, that's right. I've read about them, tested them out, but had never come up with a real world reason to use them..... Until now.
/**
* Proxy handler for redis.
*
* ! After much trial and error I was unable to get a direct connection to
* ! redis server from next.js. The redis package was being loaded in the client and erroring.
*
* * To solve, there is a simple express server running that communicates with Redis. Nextjs
* * app sends the requests via POST to the express server, which sends requests to Redis.
*
* * The proxy object traps all method calls and sends them to proxy server.
* * The redis-proxy server handles invalid method calls or invalid arguments.
*
* * It works surprisingly well.
*/
// Since we are in dockerland we can use the name of the service
// as the hostname
const REDIS_HOST = "http://redis-proxy";
const params = {
method: "POST",
headers: {
"Content-Type": "application/json",
},
};
const handler = {
get: function (target, prop) {
// By returning a function we can trap method calls
// redis.set();
return async function () {
const args = [...arguments];
/**
* If we call await redis.set("someKey", "someValue", "EX", "60")
* prop = set
* args = ["someKey", "someValue", "EX", "60"]
*
* If we call await redis.get("someOtherKey")
* prop = get
* args = ["someOtherKey"]
*/
return fetch(`${REDIS_HOST}/${prop}`, {
...params,
body: JSON.stringify(args),
})
.then((response) => response.json())
.then(({ result, error, message }) => {
if (!error) {
return result;
}
throw new Error(message);
});
};
},
};
export const redis = new Proxy({}, handler);
The Proxy catches all get commands and returns an anonymous function which sends the request to the redis-proxy.
And now, I can use redis in my next.js app. Life is good again.
Happy coding.