MERN Stack

Building a Full-Stack CRUD Application with Node.js, Express, MongoDB, and Redux Toolkit

Creating a CRUD (Create, Read, Update, Delete) application is a great way to understand how the backend and frontend interact in modern web development. In this blog, we’ll build a complete CRUD application using Node.js, Express, MongoDB, and Redux Toolkit. The app will manage user data, including fields for firstName, lastName, email, and password.

What You’ll Learn

  • Set up a RESTful API with Express and MongoDB.
  • Connect a React frontend to the backend using Redux Toolkit for state management.
  • Implement Create, Read, Update, and Delete operations on user data.

Why These Technologies?

  • Node.js: Handles server-side JavaScript for building scalable web applications.
  • Express: Simplifies creating APIs and routing.
  • MongoDB: A NoSQL database for flexible data storage.
  • Redux Toolkit: Eases complex state management in React applications.

This project covers the entire development process, from backend setup to integrating frontend actions for creating, updating, and deleting users. Whether you’re a beginner or looking to enhance your skills, this guide will help you connect all the pieces into a cohesive CRUD app.

Backend: Building the REST API

Start by setting up the server to manage user data using Node.js, Express, and MongoDB.

Install Dependencies

Run the following command in your backend project folder:

npm install express mongoose body-parser cors

Create server.js File

Here’s how the backend is structured:

const express = require('express');
const mongoose = require('mongoose');
const bodyParser = require('body-parser');
const cors = require('cors');

const app = express();
const PORT = 5000;

app.use(cors());
app.use(bodyParser.json());

// MongoDB connection
mongoose.connect('mongodb://127.0.0.1:27017/usercrud', { useNewUrlParser: true, useUnifiedTopology: true })
  .then(() => console.log('MongoDB connected'))
  .catch(err => console.error(err));

// Define User Schema and Model
const userSchema = new mongoose.Schema({
  firstName: String,
  lastName: String,
  email: String,
  password: String,
});

const User = mongoose.model('User', userSchema);

// Routes
app.get('/users', async (req, res) => {
  try {
    const users = await User.find();
    res.json(users);
  } catch (error) {
    res.status(500).json({ message: error.message });
  }
});

app.post('/users', async (req, res) => {
  const { firstName, lastName, email, password } = req.body;
  try {
    const user = new User({ firstName, lastName, email, password });
    await user.save();
    res.status(201).json(user);
  } catch (error) {
    res.status(400).json({ message: error.message });
  }
});

app.put('/users/:id', async (req, res) => {
  const { id } = req.params;
  const { firstName, lastName, email, password } = req.body;
  try {
    const updatedUser = await User.findByIdAndUpdate(id, { firstName, lastName, email, password }, { new: true });
    res.json(updatedUser);
  } catch (error) {
    res.status(400).json({ message: error.message });
  }
});

app.delete('/users/:id', async (req, res) => {
  const { id } = req.params;
  try {
    await User.findByIdAndDelete(id);
    res.json({ message: 'User deleted successfully' });
  } catch (error) {
    res.status(500).json({ message: error.message });
  }
});

// Start Server
app.listen(PORT, () => console.log(`Server running on http://localhost:${PORT}`));

Frontend: React with Redux Toolkit

Install Frontend Dependencies

Run the following command in your React project:

npm install @reduxjs/toolkit react-redux axios

Configure Redux Store

Create store.js to manage your application’s state:

import { configureStore } from '@reduxjs/toolkit';
import userReducer from './userSlice';

export const store = configureStore({
  reducer: {
    users: userReducer,
  },
});

Create User Slice

Handle CRUD operations and state management in userSlice.js:

import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import axios from 'axios';

const API_URL = 'http://localhost:5000/users';

export const fetchUsers = createAsyncThunk('users/fetchUsers', async () => {
  const response = await axios.get(API_URL);
  return response.data;
});

export const addUser = createAsyncThunk('users/addUser', async (user) => {
  const response = await axios.post(API_URL, user);
  return response.data;
});

export const updateUser = createAsyncThunk('users/updateUser', async ({ id, user }) => {
  const response = await axios.put(`${API_URL}/${id}`, user);
  return response.data;
});

export const deleteUser = createAsyncThunk('users/deleteUser', async (id) => {
  await axios.delete(`${API_URL}/${id}`);
  return id;
});

const userSlice = createSlice({
  name: 'users',
  initialState: {
    users: [],
    status: 'idle',
    error: null,
  },
  reducers: {},
  extraReducers: (builder) => {
    builder
      .addCase(fetchUsers.fulfilled, (state, action) => {
        state.users = action.payload;
        state.status = 'succeeded';
      })
      .addCase(addUser.fulfilled, (state, action) => {
        state.users.push(action.payload);
      })
      .addCase(updateUser.fulfilled, (state, action) => {
        const index = state.users.findIndex(user => user._id === action.payload._id);
        state.users[index] = action.payload;
      })
      .addCase(deleteUser.fulfilled, (state, action) => {
        state.users = state.users.filter(user => user._id !== action.payload);
      });
  },
});

export default userSlice.reducer;


 

1. File Structure

Here’s how your React components should be structured:

src/
components/
AddUserForm.js
EditUserForm.js
UserList.js
App.js
userSlice.js
store.js


2. Add User Form

Create AddUserForm.js

This component will handle adding a new user.

import React, { useState } from 'react';
import { useDispatch } from 'react-redux';
import { addUser } from '../userSlice';

const AddUserForm = () => {
  const dispatch = useDispatch();
  const [form, setForm] = useState({ firstName: '', lastName: '', email: '', password: '' });

  const handleSubmit = (e) => {
    e.preventDefault();
    dispatch(addUser(form));
    setForm({ firstName: '', lastName: '', email: '', password: '' });
  };

  return (
    <form onSubmit={handleSubmit}>
      <h2>Add User</h2>
      <input
        type="text"
        placeholder="First Name"
        value={form.firstName}
        onChange={(e) => setForm({ ...form, firstName: e.target.value })}
        required
      />
      <input
        type="text"
        placeholder="Last Name"
        value={form.lastName}
        onChange={(e) => setForm({ ...form, lastName: e.target.value })}
        required
      />
      <input
        type="email"
        placeholder="Email"
        value={form.email}
        onChange={(e) => setForm({ ...form, email: e.target.value })}
        required
      />
      <input
        type="password"
        placeholder="Password"
        value={form.password}
        onChange={(e) => setForm({ ...form, password: e.target.value })}
        required
      />
      <button type="submit">Add User</button>
    </form>
  );
};

export default AddUserForm;


3. Edit User Form

Create EditUserForm.js

This component will handle editing an existing user. It requires editingUser to be passed as a prop.

import React, { useState, useEffect } from 'react';
import { useDispatch } from 'react-redux';
import { updateUser } from '../userSlice';

const EditUserForm = ({ editingUser, onCancel }) => {
  const dispatch = useDispatch();
  const [form, setForm] = useState(editingUser);

  useEffect(() => {
    setForm(editingUser);
  }, [editingUser]);

  const handleSubmit = (e) => {
    e.preventDefault();
    dispatch(updateUser({ id: editingUser._id, user: form }));
    onCancel(); // Reset the editing mode
  };

  return (
    <form onSubmit={handleSubmit}>
      <h2>Edit User</h2>
      <input
        type="text"
        placeholder="First Name"
        value={form.firstName}
        onChange={(e) => setForm({ ...form, firstName: e.target.value })}
        required
      />
      <input
        type="text"
        placeholder="Last Name"
        value={form.lastName}
        onChange={(e) => setForm({ ...form, lastName: e.target.value })}
        required
      />
      <input
        type="email"
        placeholder="Email"
        value={form.email}
        onChange={(e) => setForm({ ...form, email: e.target.value })}
        required
      />
      <input
        type="password"
        placeholder="Password"
        value={form.password}
        onChange={(e) => setForm({ ...form, password: e.target.value })}
        required
      />
      <button type="submit">Save Changes</button>
      <button type="button" onClick={onCancel}>Cancel</button>
    </form>
  );
};

export default EditUserForm;

 


4. User List

Create UserList.js

This component will display the list of users and provide buttons to edit or delete users.

import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { deleteUser } from '../userSlice';

const UserList = ({ onEdit }) => {
  const dispatch = useDispatch();
  const users = useSelector((state) => state.users.users);

  const handleDelete = (id) => {
    dispatch(deleteUser(id));
  };

  return (
    <div>
      <h2>User List</h2>
      <ul>
        {users.map((user) => (
          <li key={user._id}>
            {user.firstName} {user.lastName} - {user.email}
            <button onClick={() => onEdit(user)}>Edit</button>
            <button onClick={() => handleDelete(user._id)}>Delete</button>
          </li>
        ))}
      </ul>
    </div>
  );
};

export default UserList;


5. Main Component

Update App.js

Integrate all components into App.js.

import React, { useState } from 'react';
import { useDispatch } from 'react-redux';
import { fetchUsers } from './userSlice';
import AddUserForm from './components/AddUserForm';
import EditUserForm from './components/EditUserForm';
import UserList from './components/UserList';

const App = () => {
  const dispatch = useDispatch();
  const [editingUser, setEditingUser] = useState(null);

  React.useEffect(() => {
    dispatch(fetchUsers());
  }, [dispatch]);

  const handleEdit = (user) => {
    setEditingUser(user);
  };

  const handleCancelEdit = () => {
    setEditingUser(null);
  };

  return (
    <div>
      <h1>User Management</h1>
      {!editingUser ? (
        <>
          <AddUserForm />
          <UserList onEdit={handleEdit} />
        </>
      ) : (
        <EditUserForm editingUser={editingUser} onCancel={handleCancelEdit} />
      )}
    </div>
  );
};

export default App;


How It Works

  1. Add User Form: Handles creating a new user. After submission, the user is added to the database and Redux store.
  2. Edit User Form: Displays an editable form for the selected user. Changes are saved to the database and Redux store.
  3. User List: Shows all users with Edit and Delete buttons. The Edit button opens the EditUserForm, while Delete removes the user.

Benefits of This Approach

  • Modularity: Separate forms for adding and editing keep the code clean and reusable.
  • Scalability: Adding new features is simpler when each component has a focused responsibility.
  • Enhanced User Experience: Clear distinction between adding and editing improves usability.

Feel free to customize the forms and styles further to suit your requirements!

Leave a Reply

Your email address will not be published. Required fields are marked *