Behind the Scenes of the Web: [Part 2] — MVC Architecture and CRUD Applications
Welcome back to Part 2 of our series on backend development! In [PART 1], we laid the groundwork for building a backend server. Now, we’ll dive deeper into organizing our application with the MVC (Model-View-Controller) architecture, which is a popular design pattern that helps structure our code into manageable components.
In this part, we’ll also walk through implementing a simple CRUD (Create, Read, Update, Delete) application that manages user data, all while leveraging MongoDB for data storage. By the end of this guide, you’ll have a solid understanding of how to organize your backend code and perform basic database operations in a Node.js environment.
1. Introduction to MVC Architecture
The MVC pattern is a popular way to organize code in web applications, separating the logic into three main components:
1. Model
The Model represents the application’s data and business logic, serving as the layer that defines how data is structured and interacts with the database. It handles tasks like database operations, validations, and managing the rules that govern the data.
- For example, in an e-commerce application, the Product model would define the attributes of a product (such as name, price, and stock) and include methods to retrieve, update, or delete product data from the database. This ensures that all logic related to the application’s data is centralized and consistent.
2. View
The View represents the user interface or what the user interacts with. In backend applications, the “view” is often the JSON response sent to the client rather than a visual interface.
- For example, in an e-commerce app, the view could be the JSON response containing product details sent to the frontend or the rendered product listing page that users see. It focuses on presenting data in a user-friendly format.
3. Controller
The Controller serves as the intermediary between the Model and the View. It handles user input, such as API requests, processes the data, and communicates with the Model to fetch or update information.
- For example, when a user requests details about a product, the controller processes the request, retrieves the data from the Model, and sends it as a JSON response back to the client.
2. Setting Up Your Folder Structure
backend-app/
├── controllers/ // Controller files (business logic)
│ └── userController.js
├── models/ // Model files (database logic)
│ └── userModel.js
├── routes/ // Routes to handle incoming requests
│ └── userRoutes.js
└── server.js // Entry point for your app
3. What is CRUD?
CRUD stands for Create, Read, Update, and Delete — the fundamental persistent storage operations in most applications. These operations allow us to manipulate and interact with data effectively:
• Create: Add new data
• Read: Retrieve existing data
• Update: Modify existing data
• Delete: Remove data
These operations form the backbone of almost every backend application and allow us to build powerful systems to manage data efficiently. Let’s dive into building our CRUD application!
4. Setting Up Database
In this article, I will be using MongoDB Atlas to set up and manage the database for my project. We will then integrate this database with our Node.js application to handle data storage and management.
Note: MongoDB is a NoSQL database that stores data in flexible, JSON-like documents instead of traditional relational tables. This schema-less nature makes it easier to work with unstructured or semi-structured data and scale horizontally across multiple servers.
- What is MongoDB Atlas?
MongoDB Atlas is a cloud service provided by MongoDB to manage MongoDB databases. It is fully managed and takes care of scaling, security, backups, and maintenance.
2. Steps to setup the database:
Step 1: Create a MongoDB Atlas Account
- Go to the MongoDB Atlas website.
- Click on the Sign in button and login. If you don’t have an account, simply Sign Up.
- After logging in, you’ll be directed to the MongoDB Atlas Dashboard.
Step 2: Create a New Cluster
- On the MongoDB Atlas Dashboard, click Create a New Cluster.
- Choose your cloud provider (AWS, Google Cloud, or Azure) and select a region closest to you for the cluster location.
- You can select a Free Tier (M0) for the database if you’re just testing or developing.
- After selecting the region and other settings, click Create Deployment. This will take you to the Connect to Cluster page to create a User.
Step 3: Create a Database User
- Create your Username and Password.
- Click Create Database User. (Make sure to save the password securely.)
Step 4: Whitelist Your IP Address
1. In the Network Access section of the MongoDB Atlas Dashboard, click Add IP Address.
2. Click Allow Access from Anywhere to allow connections from any IP address (or you can whitelist a specific IP address for security).
3. Click Confirm.
Step 5: Get Your MongoDB Connection URI
1. Go to the Clusters section and click Connect for your cluster.
2. Select Connect your application.
3. Choose your version of Node.js.
4. Copy the provided connection string, which looks like this:
mongodb+srv://<username>:<password>@cluster0.mongodb.net/DatabaseName?retryWrites=true&w=majority
5. Setting up Mongoose and Node.JS Server
- Install Mongoose
npm install mongoose
2. Create the MongoDB Connection File
To set up the MongoDB connection, we’ll create a separate configuration file to handle all database-related logic. This will allow us to easily manage the database connection and use it throughout the application.
//File: config/db.js
const mongoose = require('mongoose');
const uri = "your-mongodb-connection-string";
const connectDB = async () => {
try {
await mongoose.connect(uri, {
useNewUrlParser: true,
useUnifiedTopology: true
});
console.log("Successfully connected to MongoDB with Mongoose!");
} catch (error) {
console.error("Error connecting to MongoDB:", error);
throw error;
}
};
module.exports = { connectDB };In this file:
- mongoose.connect(): This function connects your Node.js application to the MongoDB database using the provided connection string (URI).
- useNewUrlParser and useUnifiedTopology: These are options to use the latest connection string parser and the new MongoDB topology engine.
- db.js: This file exports the connectDB function, which you can call to establish the database connection when your application starts.
3. Create a User Model
Now, let’s create a User model using Mongoose to interact with the database. Mongoose uses schemas to define the structure of your documents in a collection.
// File: models/userModel.js
const mongoose = require('mongoose');
// Define the user schema with fields like name and email
const userSchema = new mongoose.Schema({
name: { type: String, required: true },
email: { type: String, required: true, unique: true }
});
const User = mongoose.model('User', userSchema);
module.exports = User;
- userSchema: This defines the structure of the User document. In this case, we are saying a user must have a name and email. The email field must be unique, so no two users can have the same email.
- User: This is the Mongoose model that corresponds to the users collection in the database. You use this model to perform operations like creating, reading, updating, and deleting users in the database.
Note: Till now we have create a conection with our database and have a userModel for schema for our database and now our folder structure should look like this.
backend-app/
├── controllers/ // Controller files (business logic)
│ ├── userController.js
├── models/ // Model files (database logic)
│ ├── userModel.js
├── routes/ // Routes to handle incoming requests
│ └── userRoutes.js
├── config/ // Configuration files
│ └── db.js
└── server.js // Entry point for your application (sets up the server)
6: Implementing CRUD Operations in Node.js
Now that we have set up our project and connected to MongoDB using Mongoose, let’s implement the CRUD operations for managing user data.
1. Setup the Controller.Js file
// File: controllers/userController.js
const User = require('../models/userModel');
// Create a new user
const createUser = async (req, res) => {
const { name, email } = req.body;
try {
// Check if the user already exists
const existingUser = await User.findOne({ email });
if (existingUser) {
return res.status(400).json({ message: 'User with this email already exists' });
}
const newUser = new User({ name, email });
await newUser.save();
res.status(201).json({ message: 'User created successfully', user: newUser });
} catch (error) {
res.status(400).json({ message: 'Error creating user', error: error.message });
}
};
// Get all users
const getAllUsers = async (req, res) => {
try {
const users = await User.find();
res.status(200).json({ users });
} catch (error) {
res.status(500).json({ message: 'Error retrieving users', error: error.message });
}
};
// Get a single user by ID
const getUserById = async (req, res) => {
const { userId } = req.params;
try {
const user = await User.findById(userId);
if (!user) {
return res.status(404).json({ message: 'User not found' });
}
res.status(200).json({ user });
} catch (error) {
res.status(500).json({ message: 'Error retrieving user', error: error.message });
}
};
// Update a user
const updateUser = async (req, res) => {
const { userId } = req.params;
const { name, email } = req.body;
try {
const updatedUser = await User.findByIdAndUpdate(
userId,
{ name, email },
{ new: true } // This option returns the modified document
);
if (!updatedUser) {
return res.status(404).json({ message: 'User not found' });
}
res.status(200).json({ message: 'User updated successfully', user: updatedUser });
} catch (error) {
res.status(400).json({ message: 'Error updating user', error: error.message });
}
};
// Delete a user
const deleteUser = async (req, res) => {
const { userId } = req.params;
try {
const deletedUser = await User.findByIdAndDelete(userId);
if (!deletedUser) {
return res.status(404).json({ message: 'User not found' });
}
res.status(200).json({ message: 'User deleted successfully' });
} catch (error) {
res.status(500).json({ message: 'Error deleting user', error: error.message });
}
};
module.exports = {
createUser, getAllUsers, getUserById, updateUser, deleteUser
};
Explanation:
- Create User (createUser):
- Extracts the name and email from the request body.
- Check if a user with the provided email already exists in the database.
- If not, create a new user and save it to the database. If successful, returns a response with the created user.
2. Get All Users (getAllUsers):
- Fetches all users from the database using User.find().
- Returns the list of users in the response.
3. Get User by ID (getUserById):
- Retrieves a user from the database by their unique ID (using req.params.userId).
- If the user exists, return the user. Otherwise, returns a “not found” error.
4. Update User (updateUser):
- Finds a user by their ID and updates their name and email (using req.params.userId for the ID and req.body for the updated data).
- If the user is updated successfully, returns the updated user.
5. Delete User (deleteUser):
- Finds a user by their ID and deletes them from the database.
- If the user is successfully deleted, send a success message; otherwise, an error.
2. Setup the UserRoutes.Js file
// File: routes/userRoutes.js
const http = require('http');
const userController = require('../controllers/userController');
const userRoutes = (req, res) => {
const { method, url } = req;
const urlParts = url.split('/');
const userId = urlParts[2];
if (method === 'POST' && url === '/user') {
let body = '';
req.on('data', chunk => {
body += chunk;
});
req.on('end', () => {
req.body = JSON.parse(body);
userController.createUser(req, res);
});
}
if (method === 'GET' && url === '/users') {
userController.getAllUsers(req, res);
}
if (method === 'GET' && url.startsWith('/user/') && userId) {
req.params = { userId };
userController.getUserById(req, res);
}
if (method === 'PUT' && url.startsWith('/user/') && userId) {
let body = '';
req.on('data', chunk => {
body += chunk;
});
req.on('end', () => {
req.body = JSON.parse(body);
req.params = { userId };
userController.updateUser(req, res);
});
}
if (method === 'DELETE' && url.startsWith('/user/') && userId) {
req.params = { userId };
userController.deleteUser(req, res);
}
};
module.exports = userRoutes;
Explanation:
- POST /user: The request body is parsed, and createUser is called to create a new user.
- GET /users: Calls getAllUsers to fetch and return all users.
- GET /user/:id: Extracts the userId from the URL and calls getUserById to fetch a single user by ID.
- PUT /user/:id: Extracts the userId and updated data from the request and calls updateUser to modify the user’s details.
- DELETE /user/:id: Extracts the userId from the URL and calls deleteUser to delete the user by ID.
3. Setup the server.Js file
// File: server.js
const http = require('http');
const mongoose = require('mongoose');
const { connectDB } = require('./config/db');
const userRoutes = require('./routes/userRoutes');
// Connect to MongoDB
connectDB();
// Create the server
const server = http.createServer(userRoutes);
// Start the server on port 5000
server.listen(5000, () => {
console.log('Server is running on http://localhost:5000');
});
Explanation:
- Database Connection: The connectDB function from the config/db.js file is called to establish a connection with MongoDB.
- HTTP Server: The server listens on port 5000 and uses userRoutes to handle the routes.
Conclusion
In this part, we’ve taken a solid step toward building a backend from scratch with Node.js. By setting up a simple server, managing different routes, and structuring our project using the MVC pattern, you’ve gained hands-on experience in backend development.
Creating a backend server from the ground up with Node.js not only strengthens your understanding of server-side programming but also prepares you to build lightweight and efficient applications. With each line of code, you get to see the flexibility and power of Node.js, which empowers developers to create robust backend systems.
Thank you for joining me on this journey! In the next part, we’ll take things further by introducing Express.js, an essential framework that simplifies routing and enhances the power of our server. We’ll also dive into middleware, which allows us to add custom functionality between the request and response cycle.
Stay tuned for the next chapter as we continue to build on this foundation and level up your backend development skills.
Love 💙 & Peace ☮️