Back to Blog

Getting Started with the MERN Stack — A Practical Guide

The MERN stack lets you build full-stack web apps with one language end-to-end. In this guide I break down each layer, show how they connect, and share lessons from my internship building real products with it.

The MERN stack — MongoDB, Express.js, React, Node.js — is one of the most productive full-stack JavaScript ecosystems you can learn today. Every layer uses JavaScript or TypeScript, which means you write one language from the database query all the way to the UI component.

In this post I’ll walk you through each layer, show how they connect, and share what I learned building real projects at my internship at Octalogicx.

What Does MERN Stand For?

LetterTechnologyRole
MMongoDBDatabase — NoSQL, document-based, JSON-like documents
EExpress.jsBackend web framework that runs on Node.js
RReactFrontend UI library for building component-based UIs
NNode.jsJavaScript runtime that powers the backend

Setting Up the Backend

Start with Node.js and Express. I recommend creating the backend and frontend as separate folders inside one monorepo.

mkdir mern-app && cd mern-app
mkdir server && cd server
npm init -y
npm install express mongoose dotenv cors
npm install -D typescript @types/express @types/node ts-node

A minimal Express server (server/src/index.ts):

import express from 'express';
import mongoose from 'mongoose';
import cors from 'cors';
import 'dotenv/config';

const app = express();
app.use(cors());
app.use(express.json());

await mongoose.connect(process.env.MONGO_URI!);
console.log('Connected to MongoDB');

app.get('/api/health', (_req, res) => {
  res.json({ status: 'ok' });
});

app.listen(5000, () => console.log('Server running on :5000'));

Defining a Mongoose Model

Mongoose adds schema validation and TypeScript types on top of MongoDB:

import mongoose, { type InferSchemaType } from 'mongoose';

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

export type Post = InferSchemaType<typeof postSchema>;
export const PostModel = mongoose.model('Post', postSchema);

Then a simple REST route:

// GET /api/posts
app.get('/api/posts', async (_req, res) => {
  const posts = await PostModel.find().sort({ createdAt: -1 });
  res.json(posts);
});

// POST /api/posts
app.post('/api/posts', async (req, res) => {
  const post = await PostModel.create(req.body);
  res.status(201).json(post);
});

Setting Up the Frontend with Vite + React

cd ..
npm create vite@latest client -- --template react-ts
cd client && npm install axios

Fetching data from your Express API:

import { useEffect, useState } from 'react';
import axios from 'axios';
import type { Post } from '../../server/src/models/Post';

export function PostList() {
  const [posts, setPosts] = useState<Post[]>([]);

  useEffect(() => {
    axios.get<Post[]>('/api/posts').then(res => setPosts(res.data));
  }, []);

  return (
    <ul>
      {posts.map(p => (
        <li key={String(p._id)}>{p.title}</li>
      ))}
    </ul>
  );
}

Add a Vite proxy so /api/* requests hit your Express server during development:

// vite.config.ts
export default defineConfig({
  server: {
    proxy: {
      '/api': 'http://localhost:5000',
    },
  },
});

Key Lessons From My Internship

Building a production appointment system at Octalogicx taught me things tutorials don’t cover:

1. Validate data on both ends.
Express middleware (e.g., Zod or express-validator) catches bad API requests. React form validation catches bad user input. Never trust one without the other.

2. Use environment variables for everything sensitive.
.env files, not hardcoded strings. Add .env to .gitignore on day one.

3. Structure by feature, not by type.
Don’t create one global controllers/ and one global models/ folder. Instead:

src/
  features/
    posts/
      posts.router.ts
      posts.controller.ts
      posts.model.ts
    auth/
      auth.router.ts
      ...

This scales much better as the codebase grows.

4. Test your API with Postman before connecting the frontend.
If the API works correctly in Postman, most frontend bugs are actually on the frontend — much easier to debug.

5. Add TypeScript from day one.
Migrating a JS codebase to TypeScript mid-project is painful. The effort upfront is worth it.

What I Would Do Differently

Looking back at my Online Appointment System:

  • Use React Query (TanStack Query) instead of manual useEffect for data fetching
  • Add JWT refresh tokens, not just access tokens
  • Write API integration tests from the start, not as an afterthought

Final Thoughts

The MERN stack is a fantastic choice for full-stack development. The JavaScript-everywhere approach reduces context switching, the npm ecosystem is massive, and MongoDB Atlas has a generous free tier to get you started.

Start small. Build a todo app, a simple blog, a CRUD product list. The architecture becomes obvious when you’re solving real problems.


Questions or feedback? Reach me at hussnaindawood2@gmail.com or connect on LinkedIn.