A Step-by-Step Tutorial: Building Your Cafe CRM with the MERN Stack

Introduction

Welcome to our concise tutorial on building a Cafe Customer Relationship Management (CRM) system using the MERN stack. This step-by-step guide will take you through the entire process, from database design to frontend development, enabling you to create a customized CRM for your Cafe. Whether you’re a developer seeking to expand your skills or a Cafe owner wanting to optimize customer experiences, this tutorial provides a fast track to success.

Join us on this journey, and in no time, you’ll be equipped to elevate your Cafe management with a tailored MERN-based CRM. Let’s begin!

Design and Requirements

Planning before executing your goal is not just a wise choice; it’s the foundation of success. When you plan, you chart a clear path forward, set achievable milestones, and anticipate potential roadblocks. Before mindlessly executing our CRM let’s first discuss the plan of execution.

✅ ERD: It defines the architecture of our database
MongoDB Atlas: Database host
Node.js: Backend server
Express.js: Backend Server
✅ Frontend: Next.js
✅ Design: Tailwind

Backend Server

Requirements

➡️ Body parser: Parse JSON body from the client
➡️ Cors: Handle cors
➡️ Express: Node.js
➡️ Mongoose: ORM for MongoDB

"dependencies": {
"body-parser": "^1.20.2",
"cors": "^2.8.5",
"express": "^4.18.2",
"mongoose": "^7.4.5"
}

Step 1

Create index.js file.
Import express and initialize the application.

const express = require('express')
const app = express()

Step 2

Install dependencies and import the dependencies.

const bodyParser = require('body-parser')
const cors = require('cors')
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({ extended: true }))
app.use(cors()) 
const { config } = require('./config')
const { PORT } = config

app.listen(PORT, () => {
console.log(`App listening on port ${PORT}`)
})

Step 3

To create a database connection, we are using MongoDB Atlas as our database provider.

// models/index.js
const mongoose = require('mongoose');
const fs = require('fs');
const path = require('path');
const { config } = require('../config')
const { MONGO_URI } = config
const mongoOptions = {
useNewUrlParser: true,
useUnifiedTopology: true
}

const connect = () => mongoose.connect(MONGO_URI, mongoOptions);

connect().then(()=>{
//You may use loggers to handle log
console.log('Connection success')
})
.catch((e)=>{
console.log('error----------',e)
})

Step 4

After creating a connection let’s start creating models with which we are going to query the database.

const mongoose = require('mongoose');
const CategorySchema = new mongoose.Schema({
name: String,
status:{
type: String,
enum: ['active','inactive'],
default: 'active'
}
});
const Category = mongoose.model('Category', CategorySchema);
module.exports = Category;

Step 5

Now we have our models configured let’s create routes to handle.

const express = require('express')
const app = express()
const bodyParser = require('body-parser')
const cors = require('cors')
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({ extended: true }))
app.use(cors())

require('./models')
const { config } = require('./config')
const { PORT } = config
const product = require('./routes/product')
const customer = require('./routes/customer')
const category = require('./routes/category')

app.use('/product', product)
app.use('/customer', customer)
app.use('/category', category)

app.listen(PORT, () => {
console.log(`App listening on port ${PORT}`)
})

Step 6

Now let’s create a routes file and write code to handle the request.

let express = require('express')
const router = express.Router()
const { getCategories } = require('../controller/Category')

router.get('/', async (req, res, next) => {
try {
let response = await getCategories()
res.json({
success: true,
data: response,
message: 'Categories Fetched successfully'
})
} catch (error) {
console.log("🚀 ~ file: category.js:10 ~ router.get ~ error:", error)
}
})

Now let’s encapsulate our business logic into a controller file:

const Category = require('../models/Category')

/**
* API to fetch categories
* @returns
*/
const getCategories = () => {
return Category.find({})
.then((data) => {
return data
})
.catch((e) => {
throw new Error(e)
})
}

Likewise, we can create functions for other operations as well like Add, Edit, Get Category by ID

/**
* API to fetch category by Id
* @returns
*/
const getCategoryById = () => {
Category.findById(req.params.id)
.then((data) => {
return data
})
.catch((e) => {
throw new Error(e)
})
}
/**
* API to add categories
* @returns
*/
const addCategory = (data) => {
Category.create(data)
.then((data) => {
return data
})
.catch((e) => {
throw new Error(e)
})
}
/**
* API to update categories
* @returns
*/
const updateCategory = (id,data) => {
Category.findByIdAndUpdate(id, data)
.then(()=>{
return true
})
.catch((e) => {
throw new Error(e)
})
}

module.exports = {
getCategories,
getCategoryById,
addCategory,
updateCategory
}

If you notice we have not used the Delete operation instead we will use the status key to manage the data.

Finally, the routes file will look like this:

let express = require('express')
const router = express.Router()
const { getCategories,
getCategoryById,
addCategory,
updateCategory } = require('../controller/Category')

router.get('/', async (req, res, next) => {
try {
let response = await getCategories()
res.json({
success: true,
data: response,
message: 'Categories Fetched successfully'
})
} catch (error) {
console.log("🚀 ~ file: category.js:10 ~ router.get ~ error:", error)
}
})

router.get('/:id', async (req, res) => {
try {
let response = await getCategoryById(req.params.id)
res.json({
success: true,
data: response,
message: 'Category Fetched successfully'
})
} catch (error) {
console.log("🚀 ~ file: category.js:10 ~ router.get ~ error:", error)
}

})

router.post('/', async (req, res) => {
try {
let data = req.body
let response = await addCategory(data)
res.json({
success: true,
data: response,
message: 'Category Added successfully'
})
} catch (error) {
console.log("🚀 ~ file: category.js:10 ~ router.get ~ error:", error)
}
})

router.put('/:id', async (req, res) => {
try {
let data = req.body
let id = req.params.id
let response = await updateCategory(id, data)
res.json({
success: true,
message: 'Category updated successfully',
data: {}
})
} catch (error) {
console.log("🚀 ~ file: category.js:58 ~ router.get ~ error:", error)
}
})

module.exports = router

Now following the same framework we will create the route handler and controller for Product and Customer.

Reference: GitHub-Cafe-Backend

Empower Your Team: Hire Our Elite MERN Developers Now!

Frontend

After creating the backend server we will now proceed with our frontend practice to make the user interact with the API.

Requirements

➡️ Framework: Next.js
➡️ UI: Tailwind CSS
➡️ Components: Flowbite
➡️ Form handling : Formik
➡️ Validation: Yup

Install Next.js

npx create-next-app@latest

During installation, you’ll be asked for a certain configuration, for initial setup you can go with default settings.

Initializing routing

In the app root folder, under layout, we are using the following configuration. I’ve used custom styling and components. You can use any configuration you feel fit and it’s absolutely okay if the file looks different. We are focusing on the concepts. Make sure you focus on the concept and approach.

import './globals.css'
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import { Sidebar } from './components/sidebar'

const inter = Inter({ subsets: ['latin'] })

export const metadata: Metadata = {
title: 'Cafe CRM Frontend',
description: 'CRM to handle Cafe orders',
}

export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body className={inter.className}>
<main className="flex min-h-screen">
<Sidebar />
<div className="w-full">
{children}
</div>
</main>
</body>
</html>
)
}

As per the documentation layout.tsx folder is the bootstrap file for our application. If you notice it has children’s prop. All the child components will be rendered here.
I’ve used some basic HTML and Tailwind configuration to create a simple admin dashboard layout. For code readability and scalability, I’ve created reusable components with dedicated names within the app root folder.

Related read: What is Code Restructuring?

Folder Hierarchy

  • Components: All the reusable table listing components for dedicated element
  • Forms: This folder encapsulates all the logic for form handling

Rest all the folders within the app root folder are default routing.

  • /category this route will by default read the category folder
  • /product will read the product folder
  • /customer will read the customer folder

Creating API Interceptor

Now to interact with the APIs we will be creating a file that handles our API interactions.
app/api.ts. I’ve utilized fetch APIs for API calls you can use Axios or any library you are comfortable with.

Reference: GitHub-Cafe-Frontend

After completing the installation and initial setup, let’s focus on the performing operations for the category element.

Reading Data

Route: /category
File: app/category/page.tsx

Imports-

Category: Render list component (app/components/category.tsx)
getCategory: Function to get the list of categories

To maintain the readability and reusability we have encapsulated the table to app/components/category.tsx file and using import we call this component to render the list.

Fetching the data-

We will be importing our getCategory() function from app/api.tsx and calling the function.
After getting the data from the API call we will provide the data to the list render component.

The final code will look like this:

import React from "react"
import { Category } from "../components/category" //importing render list
import { getCategory } from "../apis" // Calling the function from api

async function fetchData() {
const categories = await getCategory() // making the api call
return { categories }
}
const categoryPage = async () => {

let { categories } = await fetchData() 
return (
<>
<div className="body h-full">
<Category data={categories} />// passing props to the list component
</div>
</>
)
}

export default categoryPage

Final Output

final-category-listing

Adding Data

Route: /category/add
File: app/category/add/[..id]/page.tsx

Imports-

CategoryForm: Component containing Form handling logic (app/components/category.tsx)
useParams: To read url params
getCategoryById: Function to get the category details (app/apis.tsx)

To handle the route to add the data to a dedicated element, I’ve added the add folder category/add/[..id]/page.tsx. If you notice that we have added a slug, I’ll tell you more about the slug in the update part.

"use client"
import Link from "next/link"
import React, { useState, useEffect } from "react"
import { CategoryForm } from "@/app/forms/category"
import { useParams } from 'next/navigation'
import { getCategoryById } from "@/app/apis"

const CategoryPage = () => {
const params = useParams()
const { id } = params
const [category, setCategory] = useState({});
const [isEdit, setIsEdit] = useState('')
useEffect(() => {
isEditFunction()
}, [])
const isEditFunction = async () => {
try {
if (id) {
const categoryDetail = await getCategoryById(id)
setIsEdit(id[0])
setCategory(categoryDetail.data)
}
}
catch (error) {
console.log("🚀 ~ file: page.tsx:21 ~ isEditFunction ~ error:", error)
}
}
return (
<div className="w-full h-full">
<div className="bg-white shadow-md rounded-lg px-3 py-2 mb-4">
<div className="flex justify-between align-center text-gray-700 text-lg font-semibold py-2 px-2">
<p>{isEdit ? "Edit Category" : "Add Category"}</p>
<Link href='/category' className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
Back
</Link>
</div>
<div className="w-full">
<CategoryForm
isEdit={isEdit}
category={category}
/>
</div>
</div>
</div >
)
}
export default CategoryPage

Now when we click on the add category we will be redirected to the form page to add the category.

Now in the app/forms/category.tsx. For form handling, we are using Formik and Yup to validate forms.

After binding the Formik and Yup with our form we will have a code that looks like the below:

"use client"
import { Field, Form, Formik } from 'formik';
import * as Yup from 'yup';
import { addCategory, editCategory } from '../apis'
import { useRouter } from 'next/navigation'
import { useEffect, useState, useRef } from 'react';


export const CategoryForm = ({ isEdit, category }) => {
const router = useRouter()
const formRef = useRef()
const [form, setForm] = useState({ name: '' })
useEffect(() => {
checkIsEdit()
}, [isEdit])
const checkIsEdit = () => {
if (isEdit) {
formRef.current.setFieldValue('name', category.name)
}
}
const CategorySchema = Yup.object().shape({
name: Yup.string()
.min(2, 'Too Short!')
.max(50, 'Too Long!')
.required('Required')
});
const handleFormSubmit = async (formData: Object) => {
try {
let response = await !isEdit ? addCategory(formData) : editCategory(formData, isEdit)
router.push('/category')
} catch (error) {
console.log("🚀 ~ file: category.tsx:15 ~ handleFormSubmit ~ error:", error)
}
}
return (
<div className="mb-6">
<Formik
innerRef={formRef}
initialValues={form}
validationSchema={CategorySchema}
onSubmit={handleFormSubmit}
>
{({
values,
errors,
touched,
handleSubmit,
isSubmitting,
}) => (
<Form onSubmit={handleSubmit}>
{errors.name && touched.name && errors.name}
<Field
className="bg-gray-50 m-2 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="Enter Category"
type="text"
name="name"
/>
<button
className="bg-blue-500 m-2 hover:bg-blue-700 text-white font-bold py-2 px-4 mt-3 rounded"
type="submit"
disabled={isSubmitting}>
{isEdit ? 'Update' : 'Add'}
</button>
</Form>
)}
</Formik>
</div>
)
}

Update

Route: /category/add/:categoryId
File: app/category/add/[..id]/page.tsx

Imports-

CategoryForm: Component containing Form handling logic (app/components/category.tsx)
useParams: To read url params
getCategoryById: Function to get the category details (app/apis.tsx)

If you notice in our add form we are passing a slug, with the help of isEditFunction() we can check if the request is to add the category or to edit. So when we pass the optional slug parameter to the category/add route the system will automatically perform the update operation.

Field Binding

While updating a form it’s crucial to re-populate the fields.isEditFunction() provides us with the data. Now after having the data, we will utilize Formik API to bind the value to the field.

formRef.current.setFieldValue('name', category.name)

This will help us bind the value to the “name” field, and this is what the final render will look like when we click to update the category.

We have integrated the APIs to interact with category elements. Using the same logic we can create API integrations for products and customers as well.

coma

Conclusion

In this comprehensive tutorial, we crafted a Cafe CRM with the dynamic MERN stack, whether you’re a developer or Cafe owner. We tackled database design and frontend development, delving into backend setup, model creation, and route building.

With a user-friendly interface using Next.js, Tailwind CSS, Formik, and Yup, we explored routing and dynamic category editing. This empowers you to manage Cafe data efficiently and expand your CRM for a tailored solution. Cheers to your Cafe CRM journey, whether brewing coffee or code!

Keep Reading

Keep Reading

Struggling with EHR integration? Learn about next-gen solutions in our upcoming webinar on Mar 6, at 11 AM EST.

Register Now

Let's create something together!