DEV Community

Cover image for Building a Fullstack App with NodeJS, ExpressJs and Redis-OM
Wakeel Kehinde Ige
Wakeel Kehinde Ige

Posted on • Updated on

Building a Fullstack App with NodeJS, ExpressJs and Redis-OM

Overview of My Submission

According to Meik Wiking (author of The Art of Making Memories), happy memories are essential to our mental health. They strengthen our sense of identity and purpose and bond our relationships. Happy memories are an important ingredient in present happiness. As such, this gave birth to my project, Memories App which allows everyone to document their memories at any point in time.

Without Further ado, let's navigate into the project details.

Preamble
Redis is a NoSQL database that's loved for its simplicity and speed. In this blogpost, We will be building a Fullstack app with Redis Database for storing the data. We'll be building our model with Redis-OM capabilities which allow create, retrieve, update, and carry out a full-text search.
It’s totally fine if you are not familiar with the Redis database.

To start with:

What is Redis?
Well, Redis is an in-memory key-value store that is often used as a cache to make traditional databases faster. However, it has evolved into a multimodel database capable of full-text search, graph relationships, AI workloads, and more.

What is RedisJSON?
RedisJSON is a Redis module that provides JSON support in Redis. RedisJSON lets your store, update, and retrieve JSON values in Redis just as you would with any other Redis data type. RedisJSON also works seamlessly with RediSearch to let you index and query your JSON documents. It is the best way to work with the Redis database.

What is Redis-OM
Redis OM (pronounced REDiss OHM) is a library that provides object mapping for Redis—that's what the OM stands for... object mapping. It maps Redis data types — specifically Hashes and JSON documents — to JavaScript objects. And it allows you to search over these Hashes and JSON documents. It uses RedisJSON and RediSearch to do this.

while RedisJSON adds a JSON document data type and the commands to manipulate it, RediSearch adds various search commands to index the contents of JSON documents and Hashes.

Let's build with Redis and take the advantages of these Redis-OM capabilities!

Setting up

Start up Redis
First, we'll set up our environment. Then, for development purposes, we'll use Docker to run Redis Stack:

docker run -p 10001:6379 -p 13333:8001 redis/redis-stack:latest
Enter fullscreen mode Exit fullscreen mode

This starts your Redis server, once this is started, proceed to build the backend server

Intitialize your application

  • In your project directory, navigate to the terminal and run the following commands:
mkdir server
cd server
npm run init -y
Enter fullscreen mode Exit fullscreen mode
  • This creates a package.json and package-lock.json files in your server folder, now you can start installing your dependencies. We will install the following packages:
    redis-om, cors, express, nodemon along with other dependencies

  • Run the following command to install the dependencies

npm i cors express nodemon redis-om bcrypt body-parser colors dotenv express express-validator jsonwebtoken nodemon redis redis-om
Enter fullscreen mode Exit fullscreen mode
  • Let's create our env file before we proceed to the last part of the app create .env in the server folder and add
PORT=3005
REDIS_HOST="redis://localhost:6379"

JWT_TOKEN_SECRET= "add your preferred jwt secret"
JWT_EXPIRE="add the expiry"
Enter fullscreen mode Exit fullscreen mode
  • With dotenv installed as part of our dependencies, we can start using our environmental variable data in our application.

Create and connect a Redis Client

  • To create and connect a Redis client, Create config folder in the server folder and create a file named connectToRedis.js
config/connectToRedis.js

import { Client } from "redis-om";
import dotenv from "dotenv";
const url = process.env.REDIS_HOST;
let client;
try {
  client = await new Client().open(url);
console.log("##########################################################");
  console.log("#####            REDIS STORE CONNECTED               #####");
  console.log("##########################################################\n");
} catch (err) {
  console.log(`Redis error: ${err}`.red.bold);
}
export default client;
Enter fullscreen mode Exit fullscreen mode

Create the models/repositories

  • Create a folder named model in the server folder and add the two files named post.js and user.js respectively.
model/post.js 

import { Entity, Schema } from "redis-om";
import client from "../config/connectToRedis.js";
class Post extends Entity {}

const postSchema = new Schema(
  Post,
  {
    title: { type: "text" },
    message: { type: "text" },
    name: { type: "string" },
    creator: { type: "string" },
    tags: { type: "text" },
    selectedFile: { type: "string" },
    likes: { type: "string[]" },
    comments: { type: "string[]" },
    createdAt: { type: "date", sortable: true },
  },
  {
    dataStructure: "JSON",
  }
);

export const postRepository = client.fetchRepository(postSchema);
await postRepository.createIndex();
Enter fullscreen mode Exit fullscreen mode
model/user.js

import { Entity, Schema } from "redis-om";
import client from "../config/connectToRedis.js";

class User extends Entity {}

const userSchema = new Schema(
  User,
  {
    name: {
      type: "string",
    },
    email: {
      type: "string",
    },
    password: {
      type: "string",
    },
  },
  {
    dataStructure: "JSON",
  }
);

export const userRepository = client.fetchRepository(userSchema);
await userRepository.createIndex();
Enter fullscreen mode Exit fullscreen mode
  • For both schemas, we defined an entity. An Entity is the class that holds your data when you work with it — the thing being mapped to. It is what you create, read, update, and delete. Any class that extends Entity is an entity.
  • We also define our schemas for both; A schema defines the fields on your entity, their types, and how they are mapped internally to Redis. By default, entities map to JSON documents.

Note: in the field type, A text field is a lot like a string. The difference is that string fields can only be matched on their whole value — no partial matches — and are best for keys while text fields have full-text search enabled on them and are optimized for human-readable text.

  • We also created a repository for each. A Repository is the main interface into Redis OM. It gives us the methods to read, write, and remove a specific Entity

and lastly...

  • We created an index so we would be able to search for the data in each repositories. We do that by calling .createIndex(). If an index already exists and it's identical, this function won't do anything. If it's different, it'll drop it and create a new one

Set up the Routes

  • Create a route folder an add both posts.js and users.js file which contain the routes for our application. It should like what we have below:
routes/posts.js 

import express from "express";
const router = express.Router();
import { getPosts, getPost, getPostsBySearch, createPost, updatePost, deletePost, likePost, commentPost, getMyPosts, getSuggestedPosts, } from "../controller/posts.js";
import validator from "../middleware/validator.js";
import auth from "../middleware/auth.js";
import schema from "../validation/post.js";
const { postSchema } = schema;

router.route("/").get(auth, getMyPosts).post(auth, validator(postSchema), createPost);
router.route("/all").get(getPosts);
router.get("/search", getPostsBySearch);
router.get("/suggestion", getSuggestedPosts);
router.route("/:id").patch(auth, updatePost).delete(auth, deletePost);
router.get("/:id", getPost);
router.patch("/:id/comment", commentPost);
router.patch("/:id/like", auth, likePost);

export default router;

Enter fullscreen mode Exit fullscreen mode
routes/users.js 

import express from "express";

const router = express.Router();

import { signin, signup } from "../controller/user.js";
import validator from "../middleware/validator.js";
import schema from "../validation/user.js";
const { userSchema } = schema;

router.route("/signin").post(signin);
router.route("/signup").post(validator(userSchema), signup);

export default router;

Enter fullscreen mode Exit fullscreen mode

Note, validators are done with express validator, so we imported the post and user validation schema and ensure our data are first validated when a request is sent to the endpoint. You can have access to necessary validation files here Validation Files and validator middleware

Set up the Index.js File

Create Index.js file in the server folder

index.js

import express from "express";
import bodyParser from "body-parser";
import cors from "cors";
import dotenv from "dotenv";
import colors from "colors";
import client from "./config/connectToRedis.js";
import postRoutes from "./routes/posts.js";
import { errorHandler, notFound } from "./middleware/error.js";
import userRoutes from "./routes/users.js";

const app = express();

dotenv.config();

//Body Parser
app.use(bodyParser.json({ limit: "30mb", extended: true }));
app.use(bodyParser.urlencoded({ limit: "30mb", extended: true }));

app.use(cors());

app.get("/", (req, res) => {
  res.json({ message: "Hello to Memories API" });
});
app.use("/posts", postRoutes);
app.use("/user", userRoutes);

const PORT = process.env.PORT || 5000;

const server = app.listen(PORT, () => {
  console.log("##########################################################");
  console.log("#####               STARTING SERVER                  #####");
  console.log("##########################################################\n");
  console.log(`server running on → PORT ${server.address().port}`.yellow.bold);
});

process.on("uncaughtException", (error) => {
  console.log(`uncaught exception: ${error.message}`.red.bold);
  process.exit(1);
});

process.on("unhandledRejection", (err, promise) => {
  console.log(`Error ${err.message}`.red.bold);
  server.close(() => process.exit(1));
});

app.use(notFound);

app.use(errorHandler);
Enter fullscreen mode Exit fullscreen mode

Here, we consumed different middlewares and import our connected Redis client in the file and also the created routes.

ooops, we are getting there comrade!, onto the last part of our application!,

Set up the Controllers

Here is where the logic of our application resides. For the user registration, We will be using JWT to sign the credentials and bycrypt to encrypt the password before storing them in our database.

  • From the /register route, we will:
    • Get user input.
    • Validate if the user already exists.
    • Encrypt the user password.
    • Create a user in our database.
    • And finally, create a signed JWT token.
controller/user.js


export const signup = async (req, res) => {
  const { firstName, lastName, email, confirmPassword } = req.body;
  const existingUser = await userRepository.search().where("email").is.equalTo(email).return.first();
  //check if user already registered with the email
  if (existingUser) {
    return res.status(400).json({ message: "A user already registered with the email." });
  }
  if (req.body.password !== confirmPassword) {
    return res.status(400).json({ message: "Passwords don't match." });
  }

  //hash password
  const hashedPassword = await bcrypt.hash(req.body.password, 12);
  const user = await userRepository.createAndSave({ name: `${firstName} ${lastName}`, email, password: hashedPassword });

  const token = jwt.sign({ email: user.email, id: user.entityId }, process.env.JWT_TOKEN_SECRET, {
    expiresIn: process.env.JWT_EXPIRE,
  });
  const { entityId, password, ...rest } = user.toJSON();
  const data = { id: user.entityId, ...rest };
  res.status(200).json({ result: data, token });
};
Enter fullscreen mode Exit fullscreen mode
  • Using Postman to test the endpoint, we'll get the below response after a successful registration.

Signup response

Throughout this application, note how id replaces entityId provided for us by Redis-OM to match a traditional response.

 const { entityId, password, ...rest } = user.toJSON();
 const data = { id: user.entityId, ...rest };
Enter fullscreen mode Exit fullscreen mode
  • For the Login Route
    • Get user input.
    • Authenticate the user.
    • And finally, create and send a signed JWT token.
controller/user.js

export const signin = async (req, res) => {
  const { email } = req.body;
  const existingUser = await userRepository.search().where("email").is.equalTo(email).return.first();
  //check if user exists
  if (!existingUser) {
    return res.status(404).json({ message: "User not found." });
  }
  //check for correct password
  const isPasswordCorrect = await bcrypt.compare(req.body.password, existingUser.password);
  if (!isPasswordCorrect) {
    return res.status(404).json({ message: "invalid Credentials" });
  }
  //create auth token
  const token = jwt.sign({ email: existingUser.email, id: existingUser.entityId }, process.env.JWT_TOKEN_SECRET, {
    expiresIn: process.env.JWT_EXPIRE,
  });
  const { entityId, password, ...rest } = existingUser.toJSON();
  const data = { id: existingUser.entityId, ...rest };
  res.status(200).json({ result: data, token });
};
Enter fullscreen mode Exit fullscreen mode
  • Using Postman to test the endpoint, we'll get the below response after a successful login.

Login Response

😊 And the user can start creating his or her memories

okay, let's dive into the post controller...

Creating a Post

controller/post.js

export const createPost = async (req, res) => {
  const post = req.body;
  const newPost = await postRepository.createAndSave({
    ...post,
    creator: req.userId,
    createdAt: new Date().toISOString(),
  });

  const { entityId, ...rest } = newPost.toJSON();
  res.status(201).json({ data: { id: newPost.entityId, ...rest } });
};
Enter fullscreen mode Exit fullscreen mode

Create post response

Get Paginated Posts

controller/post.js

export const getPosts = async (req, res) => {
  const { page } = req.query;
  const limit = 8;
  const offset = (page - 1) * limit;
  if (!page) {
    res.status(400).json({ message: "Enter count and offset" });
  }
  const posts = await postRepository.search().sortDescending("createdAt").return.page(offset, limit);
  const postsCount = await postRepository.search().sortDescending("createdAt").return.count();
  const newPosts = posts.map((item) => {
    const { entityId, ...rest } = item.toJSON();
    return { id: item.entityId, ...rest };
  });

  res.status(200).json({ data: newPosts, currentPage: Number(page), numberOfPages: Math.ceil(postsCount / limit) });
};
Enter fullscreen mode Exit fullscreen mode

Get Paginated posts

Get a post

controller/post.js

export const getPost = async (req, res) => {
  const data = await postRepository.fetch(req.params.id);
  if (!data.title) {
    return res.status(404).json({ message: "No post with that id" });
  }
  const { entityId, ...rest } = data.toJSON();
  res.status(200).json({ data: { id: data.entityId, ...rest } });
};
Enter fullscreen mode Exit fullscreen mode

get a post

Delete a post

controller/post.js

export const deletePost = async (req, res) => {
  const post = await postRepository.fetch(req.params.id);
  if (!post.title) {
    return res.status(404).json({ message: "No post with that id" });
  }
  if (req.userId !== post.creator) {
    return res.status(400).json({ message: "Unauthorized, only creator of post can delete" });
  }
  await postRepository.remove(req.params.id);
  res.status(200).json({ message: "post deleted successfully" });
};


Enter fullscreen mode Exit fullscreen mode

Delete Post response

Like a post

controller/post.js

export const likePost = async (req, res) => {
  let post = await postRepository.fetch(req.params.id);
  if (!post.title) {
    return res.status(400).json({ message: "No post with that id" });
  }
  if (!post.likes) {
    post.likes = [];
  }
  const index = post.likes.findIndex((id) => id === String(req.userId));
  if (index === -1) {
    post.likes = [...post.likes, req.userId];
  } else {
    post.likes = post?.likes?.filter((id) => id !== String(req.userId));
  }
  await postRepository.save(post);
  const { entityId, ...rest } = post.toJSON();
  res.status(200).json({ data: { id: post.entityId, ...rest } });
};
Enter fullscreen mode Exit fullscreen mode

Like Post Response

  • And we would put an halt to request to our post endpoints here.

Redis Cloud Platform

  • On a final note, let's navigate through the Redis Cloud Platform
  • Create an account on Redis and log in.
  • Navigate to the dashboard to create a new subscription. You can start with the free tier and upgrade later depending on your app usage.
  • Create a new Database called Memories App Create database

Dashboard

Note the endpoints are consumed in a frontend app prepared along with this project

Submission Category:

  • MEAN/MERN Maverick: Redis as a primary database instead of MongoDB (i.e. replace “M” in MEAN/MERN with “R” for Redis).

Short Video Explanation of the Project:

Technologies Used

  • JS/Node.js
  • Express Js
  • JSON Web Token
  • ReactJs, Redux
  • Material UI

Link to Code

GitHub logo phenom-world / Memories-RERN-App

A full stack mern application called "memories" where users can post interesting events occurring in their life

Memories App

💫💫💫   Live Demo 💫💫💫

Full Stack "R"ERN Application - from start to finish.

The App is called "Memories" and it is a simple social media app that allows users to post interesting events that happened in their lives. with real app features like user authentication and social login using Google accounts and REDIS database.

  • Redis is an open-source (BSD-licensed), high-performance key value database and in-memory data structure store used as a database, cache, and message broker. Redis has excellent read-write performance, fast IO read-write speed from memory, and supports read-write frequencies of more than 100k + per second.

Overview video (Optional)

Here's a short video that explains the project and how it uses Redis:

Embed your YouTube video

How it works

  • Redis OM (pronounced REDiss OHM) makes it easy to add Redis to your Node.js application by mapping the Redis data structures you know and love to classes that you define…

Deployment

To make deploys work, you need to create free account on Redis Cloud

Heroku

Deployed Backend Link

Netlify

Frontend Memories App

Additional Resources / Info

Screenshots

Image1

Image2
Image3
Image4
Image5

Conclusion

And here we come to the end of the app where we have learned how to use NodeJS and Redis OM to create APIs that perform crud operations and take advantage of RedisJson capabilities by storing JSON documents and retrieving them and RedisSearch capabilities to perform queries and full_text_search.

Top comments (7)

Collapse
 
ubaydah profile image
Ubaydah

Nice 🔥

Collapse
 
ilyasselmoutaoukkil profile image
IlyassElmoutaoukkil

Great article,

but i have couple issues, how can you run the line

await new Client().open(url)
Enter fullscreen mode Exit fullscreen mode

in the connectToRedis.js file, without getting an error, as you used the await keyword without being in an async function?? any one have any idea?

Collapse
 
tijjken profile image
Wakeel Kehinde Ige

It's a top level await, so shouldn't throw an error. Any snippet of the error encountered?

Collapse
 
ilyasselmoutaoukkil profile image
IlyassElmoutaoukkil

yea, i know you didn't mention that this application should be in CommonJS not ES6.

but how i can integrate this on an application that runs on ES6?

Collapse
 
adetayo_bishop profile image
Israel.

Waoh, This is just so amazing🔥👍

Collapse
 
solace4016 profile image
B€@st

This is Impressive👍

Collapse
 
valentine_ifedayo_3f9e055 profile image
Valentine Ifedayo

An Indepth article to really understand Redis if you are new to it.

Worth every line and time💫