Exploding Kittens Card Game - React, Nodejs and Redis (Part 2)
NabajitS
Posted on May 9, 2023
This is the second part of the series. In this part I'll be going over the backend implementation, the frontend code along with its implementation has been given in the first part, so you can check it out.
Prerequisites π₯οΈ
- React (Basics)
- Nodejs (Basics)
- Typescript (Basics)
Also, having Redis-insight installed is recommended. You can get it here
For this project we are going to be using Redis Cloud, just go here and signup to create an account (you can also install and setup a Redis database locally).
I choose the free plan, where we get 30mb free storage, although it might feel less, but it's enough to get familiar with redis.
After you signup inside subscriptions click on your database name. It's configuration page will open up, you will get the database public endpoint, username and password from here. We will be using these values to connect our application as well as redis-insight to our cloud database.
Let's get started
First let us install the required dependencies for this project -
npm install express jsonwebtoken nodemon dotenv cors redis-om
You might be wondering, what is this
redis-om
?
What redis-om basically does is provide a level of abstraction so that we can work with data without worrying about how it is actually stored inside of Redis.
For example redis-om provides us the functionality of storing and retrieving data in json format, however under the hood redis actually stores data in the form of key-value pairs.
Here's the projects folder structure
-
βββ src
β βββ controllers
β βββ middleware
β βββ routes
β βββ schema
β βββ redisClient.ts
βββ server.ts
βββ tsconfig.json
βββ package.json
βββ package-lock.json
We will initiate the server inside server.ts
//server.ts
import express from 'express';
import cors from 'cors';
import { authRouter } from './src/routes/userRoutes';
const app = express();
app.use(cors());
app.use(express.json());
const port = process.env.PORT ?? 5000;
app.listen(port, () => {
console.log(`Server running on: http://localhost:${port}`);
});
app.use('/users', authRouter);
Then inside the userRoutes.ts
file
import express from 'express';
import { createUser, updateScores, getHighScores, signInUser } from '../controllers/userController';
import { checkAuth } from '../middleware/checkAuth';
const authRouter = express.Router();
authRouter.post('/signup', createUser);
authRouter.post('/login', signInUser);
authRouter.use(checkAuth)
authRouter.get('/updatescore', updateScores);
authRouter.get('/highest', getHighScores);
export { authRouter }
We set up the various routes and their respective controllers. On /signup
and /login
we will call the createUser
and signInUser
controllers, which we will create later. We we will add checkAuth
middleware which checks the user authentication and saves current user's validated token to browser's local storage so that it can be sent to the server on subsequent requests.
Now let's get to the Redis bit
First we will connect to our Redis cloud database from our app.
The connection string will have the following format
redis://<username>:<password>@<host:port or Public endpoint>
So inside redisClient.ts
add the following code.
//redisClient.ts
import { Client } from 'redis-om';
import { config } from 'dotenv';
config()
const redisClient = new Client();
(async () => {
await redisClient.open(`redis://${process.env.USER}:${process.env.PASS}@${process.env.URL}`)
})();
;
export { redisClient }
We import the Client class from redis-om and connect to the database by calling its open method, and then export the client.
Also, if the code block looks unfamiliar to you, then it's called IIFE and what it basically does is call the function as soon as it is defined. And as to why we need to use it, we use it to make typescript happyπ
Inside userSchema.ts
add the following code.
//userSchema.ts
import { Entity, Schema } from "redis-om"
interface User {
password: string,
email: string,
score: number
}
class User extends Entity { }
export const userSchema = new Schema(User, {
password: { type: 'string' },
email: { type: 'string' },
score: {
type: 'number', sortable: true
}
}, {
dataStructure: "JSON"
})
Our userSchema
consists of an id
, email
, password
and a score
field. Ideally we would have a seperate schema(i.e objects) such as scoreSchema for score and a seperate schema for the user's email, password and id. Then add a createdBy field to scoreSchema to denote to which user that score belongs to.Then filter the information based on userID.
But here we going to everything in one single object.
We also add an extra property sortable
to score, this basically helps us in getting the sorted results faster.
By default entities map to JSON documents using RedisJSON but we can still define their type by.
{
dataStructure: "JSON"
}
There are other options too, such as HASH
Then let us create a .env file for storing the environment variables
PORT=
#If your using redis cloud, user should be default
USER=default
#URL is the public endpoint, which you can find on your database configuration page, along with the password
URL=
PASS=
# jwt secret, you can write any string here
SECRET=
Now, we will create the middleware checkAuth
, which checks and verifies user authentication using jwt.
//checkAuth.ts
import jwt, { JwtPayload } from "jsonwebtoken"
import { Request, Response } from "express";
interface CustomRequest extends Request {
currUserId?: any;
}
const checkAuth = async (req: CustomRequest, res: Response, next: () => void) => {
const { authorization } = req.headers;
if (!authorization) {
return res.status(401).json({ error: "Authorization token not present" })
}
const token = authorization.split(' ')[1]
try {
const { id } = jwt.verify(token, process.env.SECRET as string) as JwtPayload
req.currUserId = id // add a new property to the req object to store current authenticated user's id.
next();
}
catch (err) {
res.status(401).json({ error: "Token not verified" })
}
}
export { checkAuth }
We create CustomRequest interface
because we are going to store the authenticated user's id(or entity Id) by adding a new property called currUserId
on the req
object so that the server can keep track of which user is making the requests.
This id is returned by the jwt.verify
method after it verifies the token
Now lets create the controllers
//userControllers.ts
import { Request, Response } from 'express';
import { redisClient } from '../redisClient';
import { userSchema } from '../schema/userschema';
import jwt from "jsonwebtoken"
const userRepository = redisClient.fetchRepository(userSchema);
(async () => {
await userRepository.createIndex();
})();
const createToken = (id: string) => {
const token = jwt.sign({ id: id }, "amanandacateatfoodtogether", { expiresIn: '3d' });
return token;
}
interface CustomRequest extends Request {
currUserId?: any;
}
const createUser = async (req: CustomRequest, res: Response) => {
try {
const { email, password } = req.body
let user = userRepository.createEntity({
password: password,
email: email,
score: 0,
})
const id = await userRepository.save(user)
const token = createToken(id)
res.status(200).json({
email: email,
token: token
})
}
catch (err: any) {
res.status(401).json({ err: err.message })
}
}
const signInUser = async (req: CustomRequest, res: Response) => {
try {
const { email, password } = req.body
const userSearch: any = await userRepository.search()
.where('email').eq(email).where('password')
.eq(password).return.first()
if (!userSearch) {
return res.status(401).json({
msg: "invalid email or password"
})
}
const token = createToken(userSearch?.entityId)
res.status(200).json({
email: userSearch.email,
token: token
})
}
catch (err: any) {
res.status(401).json({ err: err.message })
}
}
const updateScores= async (req: CustomRequest, res: Response) => {
const user = await userRepository.fetch(req.currUserId);
user.score = user.score + 1;
await userRepository.save(user);
res.send(user)
}
const getHighScores = async (req: CustomRequest, res: Response) => {
const users = await userRepository.search()
.sortDescending('score')
.return.page(0, 10); //page(offset, count) i.e start from 0 and go till 2 i.e returns 2 user highscores
res.send(users);
}
export { createUser, updateScores, signInUser, getHighScores, userRepository }
First we create a repository called userRepository because inorder to work with entitites i.e to create, read entities we need access to that repository. Then inside the IIFE we create an Index on the repository, to get search functionality on a repository we first need to create an index on it.
Inside creatUser
the createEntity
method creates an entity but remember it doesn't save it to Redis, we do that by calling save
. This will return the entityId
of the recently created entity(or object), which we then send to the createToken
function to create a jwt token.
The updateScores
function is called when a user wins a game and all it does is update the user's score in the database with 1.
The other controllers are quite simple.
With this we are done.
You can the source code for the whole project here
Posted on May 9, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.