Building Your First Full-Stack Application: A Step-by-Step Guide
Follow our detailed guide to create your first complete web application using the MERN stack (MongoDB, Express, React, Node.js).

Building your first full-stack application is an exciting milestone in your development journey. In this comprehensive guide, we'll walk through creating a task management application using the MERN stack, covering everything from setup to deployment.
What We're Building: TaskMaster Pro
We'll create a task management application with the following features:
- User authentication (register/login)
- Create, read, update, and delete tasks
- Task categories and priorities
- Due dates and reminders
- Responsive design that works on all devices
Step 1: Project Setup and Structure
Let's start by setting up our project structure and initializing both our backend and frontend.
Initialize Node.js Server
Create a new directory for your project and set up the backend structure:
mkdir taskmaster-pro
cd taskmaster-pro
mkdir backend frontend
cd backend
npm init -y
npm install express mongoose cors dotenv bcryptjs jsonwebtoken
npm install -D nodemon
Create the basic Express server in server.js
:
const express = require('express');
const mongoose = require('mongoose');
const cors = require('cors');
require('dotenv').config();
const app = express();
// Middleware
app.use(cors());
app.use(express.json());
// MongoDB Connection
mongoose.connect(process.env.MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
})
.then(() => console.log('MongoDB connected'))
.catch(err => console.log(err));
// Basic route
app.get('/', (req, res) => {
res.json({ message: 'TaskMaster API is running!' });
});
const PORT = process.env.PORT || 5000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Create React Application
Set up the frontend using Create React App:
cd ../frontend
npx create-react-app .
npm install axios react-router-dom
Install additional dependencies for styling and UI components:
npm install @mui/material @emotion/react @emotion/styled
npm install @mui/icons-material
Set up proxy in package.json to connect to our backend:
"proxy": "http://localhost:5000"
Step 2: Database Models and User Authentication
Now let's define our data models and implement user authentication.
Create User Schema
Create a models directory and define the User model:
// backend/models/User.js
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const userSchema = new mongoose.Schema({
username: {
type: String,
required: true,
unique: true,
trim: true,
minlength: 3,
maxlength: 30
},
email: {
type: String,
required: true,
unique: true,
trim: true,
lowercase: true
},
password: {
type: String,
required: true,
minlength: 6
}
}, {
timestamps: true
});
// Hash password before saving
userSchema.pre('save', async function(next) {
if (!this.isModified('password')) return next();
this.password = await bcrypt.hash(this.password, 12);
next();
});
// Compare password method
userSchema.methods.comparePassword = async function(candidatePassword) {
return await bcrypt.compare(candidatePassword, this.password);
};
module.exports = mongoose.model('User', userSchema);
Create Task Schema
Define the Task model with various properties:
// backend/models/Task.js
const mongoose = require('mongoose');
const taskSchema = new mongoose.Schema({
title: {
type: String,
required: true,
trim: true,
maxlength: 100
},
description: {
type: String,
trim: true,
maxlength: 500
},
priority: {
type: String,
enum: ['low', 'medium', 'high'],
default: 'medium'
},
category: {
type: String,
trim: true,
maxlength: 50
},
dueDate: {
type: Date
},
completed: {
type: Boolean,
default: false
},
user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
}
}, {
timestamps: true
});
module.exports = mongoose.model('Task', taskSchema);
Implement Authentication Routes
Create routes for user registration and login:
// backend/routes/auth.js
const express = require('express');
const jwt = require('jsonwebtoken');
const User = require('../models/User');
const router = express.Router();
// Register
router.post('/register', async (req, res) => {
try {
const { username, email, password } = req.body;
// Check if user exists
const existingUser = await User.findOne({
$or: [{ email }, { username }]
});
if (existingUser) {
return res.status(400).json({
message: 'User already exists'
});
}
// Create new user
const user = new User({ username, email, password });
await user.save();
// Generate token
const token = jwt.sign(
{ userId: user._id },
process.env.JWT_SECRET,
{ expiresIn: '7d' }
);
res.status(201).json({
message: 'User created successfully',
token,
user: { id: user._id, username: user.username, email: user.email }
});
} catch (error) {
res.status(500).json({ message: 'Server error', error: error.message });
}
});
// Login
router.post('/login', async (req, res) => {
try {
const { email, password } = req.body;
// Find user and check password
const user = await User.findOne({ email });
if (!user || !(await user.comparePassword(password))) {
return res.status(401).json({ message: 'Invalid credentials' });
}
// Generate token
const token = jwt.sign(
{ userId: user._id },
process.env.JWT_SECRET,
{ expiresIn: '7d' }
);
res.json({
message: 'Login successful',
token,
user: { id: user._id, username: user.username, email: user.email }
});
} catch (error) {
res.status(500).json({ message: 'Server error', error: error.message });
}
});
module.exports = router;
Step 3: Task API Routes and Middleware
Create protected routes for task operations with authentication middleware.
Create Authentication Middleware
Implement middleware to protect routes:
// backend/middleware/auth.js
const jwt = require('jsonwebtoken');
const User = require('../models/User');
const auth = async (req, res, next) => {
try {
const token = req.header('Authorization')?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({ message: 'No token, authorization denied' });
}
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const user = await User.findById(decoded.userId).select('-password');
if (!user) {
return res.status(401).json({ message: 'Token is not valid' });
}
req.user = user;
next();
} catch (error) {
res.status(401).json({ message: 'Token is not valid' });
}
};
module.exports = auth;
Implement Task CRUD Operations
Create routes for all task operations:
// backend/routes/tasks.js
const express = require('express');
const Task = require('../models/Task');
const auth = require('../middleware/auth');
const router = express.Router();
// Get all tasks for user
router.get('/', auth, async (req, res) => {
try {
const tasks = await Task.find({ user: req.user._id }).sort({ createdAt: -1 });
res.json(tasks);
} catch (error) {
res.status(500).json({ message: 'Server error', error: error.message });
}
});
// Create new task
router.post('/', auth, async (req, res) => {
try {
const task = new Task({
...req.body,
user: req.user._id
});
await task.save();
res.status(201).json(task);
} catch (error) {
res.status(400).json({ message: 'Error creating task', error: error.message });
}
});
// Update task
router.put('/:id', auth, async (req, res) => {
try {
const task = await Task.findOneAndUpdate(
{ _id: req.params.id, user: req.user._id },
req.body,
{ new: true, runValidators: true }
);
if (!task) {
return res.status(404).json({ message: 'Task not found' });
}
res.json(task);
} catch (error) {
res.status(400).json({ message: 'Error updating task', error: error.message });
}
});
// Delete task
router.delete('/:id', auth, async (req, res) => {
try {
const task = await Task.findOneAndDelete({
_id: req.params.id,
user: req.user._id
});
if (!task) {
return res.status(404).json({ message: 'Task not found' });
}
res.json({ message: 'Task deleted successfully' });
} catch (error) {
res.status(500).json({ message: 'Server error', error: error.message });
}
});
module.exports = router;
Step 4: React Frontend Implementation
Now let's build the React frontend with components for authentication and task management.
Create Authentication Context
Set up React context for authentication state management:
// frontend/src/context/AuthContext.js
import React, { createContext, useState, useContext, useEffect } from 'react';
import axios from 'axios';
const AuthContext = createContext();
export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
};
export const AuthProvider = ({ children }) => {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const token = localStorage.getItem('token');
const userData = localStorage.getItem('user');
if (token && userData) {
axios.defaults.headers.common['Authorization'] = `Bearer ${token}`;
setUser(JSON.parse(userData));
}
setLoading(false);
}, []);
const login = async (email, password) => {
try {
const response = await axios.post('/api/auth/login', { email, password });
const { token, user } = response.data;
localStorage.setItem('token', token);
localStorage.setItem('user', JSON.stringify(user));
axios.defaults.headers.common['Authorization'] = `Bearer ${token}`;
setUser(user);
return { success: true };
} catch (error) {
return {
success: false,
message: error.response?.data?.message || 'Login failed'
};
}
};
const register = async (userData) => {
try {
const response = await axios.post('/api/auth/register', userData);
const { token, user } = response.data;
localStorage.setItem('token', token);
localStorage.setItem('user', JSON.stringify(user));
axios.defaults.headers.common['Authorization'] = `Bearer ${token}`;
setUser(user);
return { success: true };
} catch (error) {
return {
success: false,
message: error.response?.data?.message || 'Registration failed'
};
}
};
const logout = () => {
localStorage.removeItem('token');
localStorage.removeItem('user');
delete axios.defaults.headers.common['Authorization'];
setUser(null);
};
const value = {
user,
login,
register,
logout,
loading
};
return (
{!loading && children}
);
};
Create Task Management Components
Build components for displaying and managing tasks:
// frontend/src/components/TaskList.js
import React, { useState, useEffect } from 'react';
import {
List,
ListItem,
ListItemText,
IconButton,
Checkbox,
Typography,
Box
} from '@mui/material';
import { Delete, Edit } from '@mui/icons-material';
import axios from 'axios';
export default TaskList;
Step 5: Deployment Preparation
Prepare your application for deployment to platforms like Heroku, Vercel, or Netlify.
Configure Environment Variables
Create environment files for development and production:
// backend/.env
MONGODB_URI=mongodb://localhost:27017/taskmaster
JWT_SECRET=your-super-secret-jwt-key-here
PORT=5000
// frontend/.env
REACT_APP_API_URL=http://localhost:5000
Add Build Scripts and Configuration
Update package.json files with build scripts and deployment configuration:
// backend/package.json
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js",
"heroku-postbuild": "NPM_CONFIG_PRODUCTION=false npm install"
}
// frontend/package.json
"scripts": {
"build": "react-scripts build",
"build:prod": "GENERATE_SOURCEMAP=false react-scripts build"
}
Next Steps and Enhancements
Congratulations! You've built a complete full-stack application. Here are some ideas to enhance it further:
- Add real-time updates with Socket.io
- Implement file uploads for task attachments
- Add email notifications for due tasks
- Create a mobile app version with React Native
- Add team collaboration features
- Implement data export functionality
"The best way to learn full-stack development is by building complete applications. Each project teaches you how different parts of the stack work together."
Conclusion
Building your first full-stack application is a significant achievement that demonstrates your ability to work with both frontend and backend technologies. The MERN stack provides a powerful foundation for modern web applications, and the patterns you've learned in this guide apply to many other types of projects.
Remember that development is an iterative process. Start with a basic working version, then gradually add features and refine your code. At WBS Coding School, we emphasize this project-based learning approach to help students build portfolio pieces that showcase their skills to potential employers.
Keep coding, keep learning, and don't hesitate to explore beyond the MERN stack as you continue your development journey!