24
You don't know Redis (Part 2)
In the first part of You don't know Redis, I built an app using Redis as a primary database. For most people, it might sound unusual simply because the key-value data structure seems suboptimal for handling complex data models.
In practice, the choice of a database often depends on the application’s data-access patterns as well as the current and possible future requirements.
Redis was a perfect database for a Q&A board. I described how I took advantage of sorted sets and hashes data types to build features efficiently with less code.
Now I need to extend the Q&A board with registration/login functionality.
I will use Redis again. There are two reasons for that.
Firstly, I want to avoid the extra complexity that comes with adding yet another database.
Secondly, based on the requirements that I have, Redis is suitable for the task.
Important to note, that user registration and login is not always about only email and password handling. Users may have a lot of relations with other data which can grow complex over time.
Despite Redis being suitable for my task, it may not be a good choice for other projects.
Always define what data structure you need now and may need in the future to pick the right database.
I use serverless functions, the ioredis library and Upstash Serverless Redis.
I can’t help but talk about serverless all the time because it greatly simplifies development. I love when complexity is removed whenever possible and Upstash is doing just that for me.
I have zero work with setting up Redis. Moreover, I am using Upstash both in development and production.
During registration, we collect the user name
, email
and password
. Before registering a user, we need to make sure that the email has not been registered already (is unique in the system).
Redis does not support constraints. However, we can keep track of all registered emails using a sorted set named emails
.
On every new registration, we can use the ZSCORE command to check whether the provided email is already registered.
If the email is taken, we need to notify the user about it.
⚠️ Note, that this isn’t the best option because by telling that a given email is registered we provide a simple way for anyone to check whether someone is registered with a particular service, albeit it’s not a big security issue.
Before we can save a new user, we need to:
- Generate a unique user
ID
.
We can use the INCR command to always get a unique value by incrementing a number stored at a key by one. If the key does not exist, Redis will set it to 0
before performing the operation. This means that the initial value will be 1
.
const id = await redis.incr('user_ids') // -> 1
Whenever you need to create a counter, INCR
is a great choice. Or you can build a rate-limiter to protect your API from being overwhelmed by using INCR
together with EXPIRE.
- Hash the password with the bcrypt library.
const hash = await bcrypt.hash(password, 10)
Now that we have the unique user ID
(e.g. user ID is 7) and the hashed password, we can:
1. Store user details in a hash under the user:{ID}
key.
redis.hmset('user:7', { 7, name, email, hash })
Knowing the ID
, we can easily get all user details using the HGETALL command:
redis.hgetall('user:7');
2. Add the user’s email to the emails
sorted set.
redis.zadd('emails', -Math.abs(7), email)
This allows us to lookup emails to check if they are registered or get the user's ID
by email
which is exactly what we need for the login process.
redis.zscore('emails', email)
will return the score which is the ID
or nil
if the email is not found.
Notice how we use this sorted set for two important features, namely ensuring unique emails and looking up users by email.
But we are taking it one step further and set scores (which represent user ID
s) as negative numbers to mark emails as unverified: -Math.abs(7)
. Then, when the email is verified, we simply convert it to a positive number.
redis.zadd('emails', Math.abs(7), email)
If a specified email
is already a member of the emails
sorted set, Redis will update the score only.
During the login process, we can always check for negative numbers and request users to verify their email instead of logging them in.
Retrieving all unverified emails is a trivial operation done with the ZRANGEBYSCORE command.
redis.zrangebyscore('emails', '-inf', -1, 'WITHSCORES');
Registration function source code
Before logging in the user, we check if the provided email exists in our database. As mentioned before, the score
is the user ID
.
const userId = await redis.zscore('emails', email);
If so, we first check if the email is verified by making sure the ID
is a positive number. If not, we ask users to verify their email.
If the email is verified, we get the password hash that we stored for the user:
const hash = await redis.hget('user:7', 'hash');
and check whether the password is correct:
const match = await bcrypt.compare(password, hash);
If the password is correct, we generate a token and return it to the client.
And we are done.
Login function source code
As you can see, we needed four Redis commands for registration and only two for login.
Probably you noticed that while describing the registration and login process with Redis we also revealed two more use cases for Redis, namely counter and rate-limiting.
Redis has a lot more use cases beyond cache and learning about them will only make you even more efficient.
Follow me to read about how I am implementing a secure production-ready registration flow with email verification and password recovery backed by Redis.
Check out my article on how I implemented the LinkedIn-like reactions with Serverless Redis.
24