logo
Projects August 18, 2025 | 12 min read

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).

Full-Stack Development

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
M E R N

Step 1: Project Setup and Structure

Let's start by setting up our project structure and initializing both our backend and frontend.

Backend Setup

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}`);
});
Frontend Setup

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.

User Model

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);
Task Model

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);
Auth Routes

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.

Auth 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;
Task Routes

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.

Auth Context

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}
    
  );
};
Task Components

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.

Environment Variables

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
Build Scripts

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!

Stay Updated with WBS Coding School

Subscribe to our newsletter for the latest blog posts, coding tips, and course updates.

We respect your privacy. Unsubscribe at any time.