message
Web Application Development

MERN Stack 2025: Build Full‑Stack Apps with MongoDB, Express, React & Node.

MERN Stack 2025: Build Full‑Stack Apps with MongoDB, Express, React & Node.Blog banner

The MERN Stack is expected to continue dominating full-stack web development in 2025, offering unmatched flexibility, speed, and scalability. With advancements in each component, this stack has evolved to support modern application demands, from real-time dashboards to AI-powered tools.

What is the MERN Stack?

MERN is a popular full-stack development framework, which includes:

MongoDB: A NoSQL database that stores data in flexible JSON-like documents.

Express.js: A lightweight backend web application framework running on Node.js.

React.js: A JavaScript library developed by Facebook for building rich and interactive UIs.

Node.js: A runtime environment that executes JavaScript code on the server side.

Together, these tools enable developers to use a single language (JavaScript) across the entire stack, reducing friction and boosting productivity.

Why MERN Stack Is Still a Top Choice in 2025

  • One Language, Faster Development: JavaScript across the stack minimises context switching, accelerating development and collaboration.
  • Built for Scale and Performance: MERN offers rapid UI development, efficient real-time data handling, and robust database scalability.
  • Modern, Flexible, and Future-Ready: The stack supports cloud-native, microservices, headless architectures, and easily integrates with modern tech like PWAs and AI.
  • Strong Community, Lower Costs: Extensive open-source community support and no licensing fees make MERN a cost-effective and well-supported choice.

Let's build a simple blog CRUD app using MERN Stack

Prerequisites:

Ensure you have the following installed on your system:

  • Node.js.
  • MongoDB.
  • IDE like VSCode.

Setup the backend first

The folder structure for the backend project will look something like the following:

Code

blog-backend/
├── models/
│   └── Post.js
├── routes/
│   └── posts.js
├── .env
├── server.js
├── package.json        
      

1. Create project folder:

Code

mkdir blog-backend
cd blog-backend    
      

2. Initialise the Project

Code

npm init -y   
      

3. Install required dependencies

Code

npm install express mongoose dotenv cors
            

4. Create Project Files

Code

touch server.js .env
mkdir models routes
touch models/Post.js
touch routes/posts.js       
      

5. Setup Environment Variable:

File: .env
It contains the port and the MongoDB DB connection url

Code

PORT=5000
MONGO_URI=mongodb+srv://:@cluster.mongodb.net/blog2025     
      

Replace <username> and <password> with your actual MongoDB Atlas credentials.

6. Create a Mongoose Model for Blog
File :

Code

// models/Post.js

import mongoose from 'mongoose';

const postSchema = new mongoose.Schema({
  title: {
    type: String,
    required: true,
    trim: true
  },
  content: {
    type: String,
    required: true
  },
  createdAt: {
    type: Date,
    default: Date.now
  }
});

export default mongoose.model('Post', postSchema);    
      

7. Create Express Routes

Code

//routes/posts.js

import express from 'express';
import Post from '../models/Post.js';

const router = express.Router();

// GET all blog posts
router.get('/', async (req, res) => {
  try {
    const posts = await Post.find().sort({ createdAt: -1 });
    res.json(posts);
  } catch (err) {
    res.status(500).json({ error: 'Server Error' });
  }
});

// POST a new blog post
router.post('/', async (req, res) => {
  const { title, content } = req.body;

  try {
    const newPost = new Post({ title, content });
    await newPost.save();
    res.status(201).json(newPost);
  } catch (err) {
    res.status(400).json({ error: 'Invalid data' });
  }
});

// GET /api/posts/:id
router.get('/:id', async (req, res) => {
  try {
    const post = await Post.findById(req.params.id);
    if (!post) return res.status(404).json({ message: 'Post not found' });
    res.json(post);
  } catch (err) {
    console.error(err);
    res.status(500).json({ message: 'Server error' });
  }
});

router.delete('/:id', async (req, res) => {
  try {
    const deletedPost = await Post.findByIdAndDelete(req.params.id);
    if (!deletedPost) {
      return res.status(404).json({ message: 'Post not found' });
    }
    res.json({ message: 'Post deleted successfully' });
  } catch (err) {
    console.error(err);
    res.status(500).json({ message: 'Server error' });
  }
});

export default router;      
      

8. Setup Express Server

Code

// server.js


import express from 'express';
import mongoose from 'mongoose';
import dotenv from 'dotenv';
import cors from 'cors';
import postRoutes from './routes/posts.js';

dotenv.config();

const app = express();
const PORT = process.env.PORT || 5000;

// Middlewares
app.use(cors());
app.use(express.json());

// Routes
app.use('/api/posts', postRoutes);

// MongoDB Connection
mongoose.connect(process.env.MONGO_URI)
  .then(() => console.log('✅ MongoDB connected'))
  .catch(err => console.error('❌ MongoDB connection failed:', err));

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

9. Replace Script in package.json file:

Code

"type":"module",
"scripts": {
  "start": "node server.js",
}      
      

10. Run the Server

Code

npm start    
      

You will get a log-in console as 🚀 Server running at http://localhost:5000

Now let's create a React project to utilise the api we just created above .

The project folder structure will look something like this

Code

blog-frontend/
├── vite.config.js
├── src/
│   ├── App.jsx
│   ├── components/
│   │   ├── AddPost.jsx
│   │   ├── AddPost.css
│   │   ├── PostList.jsx
│   │   ├── PostDetail.jsx             
      

1. Create React Project

Code

npm create vite@latest blog-frontend -- --template react
cd blog-frontend
npm install     
      

2. Configure Proxy to Express Backend

Code

// vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  server: {
    proxy: {
      '/api': 'http://localhost:5000',
    },
  },
})       
      

3. Create a “components” folder

Code

mkdir src/components      
      

4. Create AddPost Component:

Code

// src/components/AddPost.js

import React, { useState } from 'react';

function AddPost({ onPostAdded }) {
  const [form, setForm] = useState({ title: '', content: '' });

  const handleChange = (e) => {
    setForm(prev => ({ ...prev, [e.target.name]: e.target.value }));
  };

  const handleSubmit = async (e) => {
    e.preventDefault();

    try {
      const res = await fetch('/api/posts', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(form)
      });

      if (res.ok) {
        setForm({ title: '', content: '' });
        onPostAdded(); // refresh post list
      } else {
        console.error('Failed to create post');
      }
    } catch (err) {
      console.error('Error:', err);
    }
  };

  return (
    <form onSubmit={handleSubmit} style={{ marginBottom: '30px' }}>
      <h2>Add New Post</h2>
      <input
        type="text"
        name="title"
        placeholder="Title"
        value={form.title}
        onChange={handleChange}
        required
        style={{ display: 'block', width: '100%', marginBottom: '10px' }}
      />
      <textarea
        name="content"
        placeholder="Content"
        value={form.content}
        onChange={handleChange}
        required
        rows={4}
        style={{ display: 'block', width: '100%', marginBottom: '10px' }}
      />
      <button type="submit">Submit</button>
    </form>
  );
}

export default AddPost;        
      

5. Create the AddPost.css file

Code

.add-post-form {
  background: #fff;
  border-radius: 12px;
  box-shadow: 0 2px 16px rgba(0,0,0,0.08);
  padding: 2rem 1.5rem;
  max-width: 500px;
  margin: 0 auto 2rem auto;
}
.add-post-form h2 {
  margin-bottom: 1.5rem;
  font-size: 1.5rem;
  color: #333;
  text-align: center;
}
.add-post-form input,
.add-post-form textarea {
  width: 100%;
  padding: 5px;
  margin-bottom: 1.2rem;
  border: 1px solid #e0e0e0;
  border-radius: 6px;
  font-size: 1rem;
  background: #fafbfc;
  transition: border 0.2s;
}
.add-post-form input:focus,
.add-post-form textarea:focus {
  border: 1.5px solid #0077ff;
  outline: none;
  background: #fff;
}
.add-post-form button {
  width: 100%;
  padding: 0.8rem 0;
  background: linear-gradient(90deg, #0077ff 0%, #00c6ff 100%);
  color: #fff;
  border: none;
  border-radius: 6px;
  font-size: 1.1rem;
  font-weight: 600;
  cursor: pointer;
  transition: background 0.2s;
}
.add-post-form button:hover {
  background: linear-gradient(90deg, #005fcc 0%, #009fcc 100%);
}

6. Create PostList Component:

Code

// src/components/PostList.js

import React, { useEffect, useState } from 'react';

function PostList({ refreshKey, handleRefresh, onSelectPost }) {
  const [posts, setPosts] = useState([]);

  useEffect(() => {
    fetch('/api/posts')
      .then((res) => res.json())
      .then((data) => setPosts(data))
      .catch((err) => console.error('Failed to fetch posts:', err));
  }, [refreshKey]);

  const deletePost = (id) => {
    fetch(`/api/posts/${id}`, {
      method: 'DELETE',
    })
      .then(() => handleRefresh())
      .catch((err) => console.error('Failed to fetch posts:', err));
  };

  return (
    <div>
      <h2>Blog Posts</h2>
      {posts.length === 0 ? (
        <p>No posts found.</p>
      ) : (
        posts.map((post) => (
          <div
            key={post._id}
            style={{
              marginBottom: '20px',
              padding: '20px',
              border: '1px solid #ccc',
            }}
          >
            <h3>{post.title}</h3>
            <p>{`${post.content.substring(0, 100)}...`}</p>
            <small>{new Date(post.createdAt).toLocaleString()}</small>
            <div>
              <button
                onClick={() => deletePost(post._id)}
                className="mt-2 text-red-600 hover:underline"
              >
                Delete
              </button>

              <button
                onClick={() => onSelectPost(post._id)}
                className="mt-2 text-red-600 hover:underline"
              >
                View
              </button>
            </div>
          </div>
        ))
      )}
    </div>
  );
}
        
export default PostList;      
      

7. Add PostDetails Component:

Code

import React, { useEffect, useState } from 'react';

const PostDetail = ({ id, onBack }) => {
  const [post, setPost] = useState(null);

  useEffect(() => {
    fetch(`/api/posts/${id}`)
      .then((res) => res.json())
      .then((data) => setPost(data))
      .catch((err) => console.error('Error fetching post:', err));
  }, [id]);

  if (!post) return <p>Loading...</p>;

  return (
    <div>
      <h2 className="text-2xl font-bold mb-4">{post.title}</h2>
      <p>{post.content}</p>
      <p className="text-gray-500 mt-2 text-sm">
        Created: {new Date(post.createdAt).toLocaleString()}
      </p>
      <button onClick={onBack} className="mt-4 text-blue-600 hover:underline">
        ← Back to Posts
      </button>
    </div>
  );
};

export default PostDetail;      
      

8. Combine in App.js

Code

import React, { useState } from 'react';
import './App.css';
import AddPost from './components/AddPost';
import PostList from './components/PostList';
import PostDetail from './components/PostDetail';

export default function App() {
  const [refreshKey, setRefreshKey] = useState(0);
  const [currentPostId, setCurrentPostId] = useState(null);

  const handleRefresh = () => setRefreshKey((prev) => prev + 1);

  return (
    <div className="center-container">
      <main className="max-w-2xl w-full p-6">
        <h1 className="text-3xl font-bold mb-6 text-blue-600">🚀 MERN Blog</h1>
        {currentPostId ? (
          <PostDetail
            id={currentPostId}
            onBack={() => setCurrentPostId(null)}
          />
        ) : (
          <>
            <AddPost onPostAdded={handleRefresh} />
            <PostList
              refreshKey={refreshKey}
              handleRefresh={handleRefresh}
              onSelectPost={(id) => setCurrentPostId(id)}
            />
          </>
        )}
      </main>
    </div>
  );
}   
      

9. Replace index.css with the following:

Code

:root {
  font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
  line-height: 1.5;
  font-weight: 400;
  
  color-scheme: light dark;
  color: rgba(255, 255, 255, 0.87);
  background-color: #242424;
  
  font-synthesis: none;
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  }
  
  a {
  font-weight: 500;
  color: #646cff;
  text-decoration: inherit;
  }
  a:hover {
  color: #535bf2;
}
  
body {
  margin: 0;
  display: flex;
  place-items: center;
  min-width: 320px;
  min-height: 100vh;
}

h1 {
  font-size: 3.2em;
  line-height: 1.1;
}

button {
  border-radius: 8px;
  border: 1px solid transparent;
  padding: 0.6em 1.2em;
  font-size: 1em;
  font-weight: 500;
  font-family: inherit;
  background-color: #1a1a1a;
  cursor: pointer;
  transition: border-color 0.25s;
}
button:hover {
  border-color: #646cff;
}
button:focus,
button:focus-visible {
  outline: 4px auto -webkit-focus-ring-color;
}

@media (prefers-color-scheme: light) {
  :root {
    color: #213547;
    background-color: #ffffff;
  }
  a:hover {
    color: #747bff;
  }
  button {
    background-color: #f9f9f9;
  }
}

#root {
  max-width: 1280px;
  margin: 0 auto;
  padding: 2rem;
  text-align: center;
}

.logo {
  height: 6em;
  padding: 1.5em;
  will-change: filter;
  transition: filter 300ms;
}
.logo:hover {
  filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
  filter: drop-shadow(0 0 2em #61dafbaa);
}

@keyframes logo-spin {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}

@media (prefers-reduced-motion: no-preference) {
  a:nth-of-type(2) .logo {
    animation: logo-spin infinite 20s linear;
  }
}

.card {
  padding: 2em;
}

.read-the-docs {
  color: #888;
}

.center-container {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  justify-content: flex-start;
  align-items: center;
  background: #f5f7fa;
  padding-top: 40px;
}             
      

10. Start the React App

Code

npm run dev     
      

You’ll be able to open the project on URL: http://localhost:3000

Conclusion

In this guide, we walked through setting up a complete MERN stack application, from initializing the backend with Express and Mongoose to building a modern frontend using Vite. We used native fetch() to simplify API calls and demonstrated how to create and display blog posts in a clean and maintainable way.

Whether you're building internal tools, SaaS platforms, or consumer-facing apps, mastering the MERN stack gives you the versatility to build and scale applications efficiently. Start with the foundation we’ve laid out here, then expand it with features like authentication, pagination, image uploads, and deployment pipelines as your project grows.

Now that you’ve seen how easy it is to create a full-stack app with MERN in 2025, it’s time to start building.

Ready to build your own MERN stack app or need expert help?

Contact our team today for tailored full-stack development solutions that scale with your business needs. We're just one message away – get in touch!

card user img
Twitter iconLinked icon

Passionate developer with expertise in building scalable web applications and solving complex problems. Loves exploring new technologies and sharing coding insights.

Book a FREE Consultation

No strings attached, just valuable insights for your project

Valid number
Please complete the reCAPTCHA verification.
Claim My Spot!
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.
download ready
Thank You
Your submission has been received.
We will be in touch and contact you soon!
View All Blogs