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
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
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 dependenciesRun 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
- 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"
- 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;
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();
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();
- 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;
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;
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);
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 });
};
- Using Postman to test the endpoint, we'll get the below response after a successful registration.
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 };
- 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 });
};
- Using Postman to test the endpoint, we'll get the below response after a successful login.
😊 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 } });
};
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) });
};
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 } });
};
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" });
};
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 } });
};
- 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
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
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:
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
Netlify
Additional Resources / Info
Watch this video on the benefits of Redis Cloud over other Redis providers
Redis Developer Hub - tools, guides, and tutorials about Redis
Screenshots
- Check out Redis OM, client libraries for working with Redis as a multi-model database.
- Use RedisInsight to visualize your data in Redis.
- Sign up for a free Redis database.
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)
Nice 🔥
Great article,
but i have couple issues, how can you run the line
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?
It's a top level await, so shouldn't throw an error. Any snippet of the error encountered?
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?
Waoh, This is just so amazing🔥👍
This is Impressive👍
An Indepth article to really understand Redis if you are new to it.
Worth every line and time💫