Understanding Express.js — The Node.js Web Framework

Express.js is the “E” in MERN and the technology responsible for your application’s server-side logic. It sits between React and MongoDB — receiving HTTP requests from the browser, running middleware for authentication and validation, querying the database, and returning JSON responses. Express is intentionally minimal: it provides only what you need to handle HTTP without imposing an architecture on you. In this lesson you will understand exactly what Express does, why it exists on top of Node.js, and how it will serve as the REST API backbone of your MERN application.

What Problem Does Express Solve?

Node.js ships with a built-in http module that can create a server, but writing an entire API with it is tedious. Express wraps that module and provides clean abstractions for the things every API needs:

Raw Node.js http Express equivalent
Parse URL manually from req.url app.get('/api/posts', handler) — routing built in
Parse JSON body manually app.use(express.json()) — one line
Set response headers manually res.json(data) sets Content-Type automatically
No middleware concept app.use(middleware) — composable pipeline
No router organisation express.Router() — modular route files
Note: Express is described as unopinionated — it does not force a folder structure, a database library, a templating engine, or an authentication system on you. This is by design. You choose the packages that fit your project and compose them together. For MERN applications, that means you add Mongoose for MongoDB, cors for cross-origin requests, and jsonwebtoken for authentication — all as separate npm packages wired into Express via middleware.
Tip: Express 5 (the latest major version) became stable in 2024 and brings async error handling built in — you no longer need to wrap async route handlers in try/catch or pass errors to next(err) manually. If you are starting a new project, use Express 5: npm install express@5. Existing Express 4 code is largely compatible.
Warning: Express does not validate request data automatically. If a client sends a POST request with missing or malformed fields, Express will happily pass that data to your controller. Always validate incoming data using a library like express-validator or joi before saving it to the database. Never trust client-supplied data.

Your First Express Server

// server/index.js
const express = require('express');
const cors    = require('cors');
const app     = express();
const PORT    = process.env.PORT || 5000;

// ── Middleware ────────────────────────────────────────────────
app.use(cors());           // allow requests from React dev server
app.use(express.json());   // parse JSON request bodies

// ── Routes ────────────────────────────────────────────────────
app.get('/api/health', (req, res) => {
  res.json({ status: 'ok', message: 'MERN API is running' });
});

app.get('/api/posts', (req, res) => {
  // We will connect this to MongoDB with Mongoose later
  res.json({ success: true, data: [] });
});

// ── Start server ──────────────────────────────────────────────
app.listen(PORT, () => {
  console.log(`Server running on http://localhost:${PORT}`);
});

Anatomy of an Express Route Handler

app.get('/api/posts/:id', async (req, res) => {
//  ▲      ▲                ▲       ▲
//  │      │                │       └─ res: send back a response
//  │      │                └───────── req: incoming request object
//  │      └────────────────────────── route path with :id parameter
//  └───────────────────────────────── HTTP method (GET)

  const { id } = req.params;    // URL parameter  → /api/posts/abc123
  const search = req.query.q;   // Query string   → /api/posts?q=mern
  const body   = req.body;      // Request body   → JSON payload (POST/PUT)

  // After processing:
  res.status(200).json({ success: true, data: {} });  // send JSON
  // or
  res.status(404).json({ success: false, message: 'Not found' });
});

The Middleware Pipeline

Express processes every request through a pipeline of middleware functions. Each middleware receives req, res, and next. Calling next() passes the request to the next middleware in the chain.

Incoming Request
      │
      ▼
[cors middleware]       → adds CORS headers
      │
      ▼
[express.json()]        → parses request body from JSON string to object
      │
      ▼
[auth middleware]       → verifies JWT token, attaches user to req.user
      │
      ▼
[route handler]         → business logic + database query
      │
      ▼
[error middleware]      → catches any errors thrown above
      │
      ▼
JSON Response sent to browser

HTTP Methods and REST Conventions

HTTP Method Express Method REST Convention Example Endpoint
GET app.get() Read — retrieve data GET /api/posts
POST app.post() Create — add new data POST /api/posts
PUT app.put() Update — replace entire resource PUT /api/posts/:id
PATCH app.patch() Update — modify partial resource PATCH /api/posts/:id
DELETE app.delete() Delete — remove a resource DELETE /api/posts/:id

Express Project Structure for MERN

server/
├── index.js              ← entry point — creates app, connects DB, starts server
├── .env                  ← environment variables (never commit to Git)
├── package.json
└── src/
    ├── config/
    │   └── db.js         ← Mongoose connection
    ├── models/
    │   └── Post.js       ← Mongoose schema and model
    ├── routes/
    │   └── posts.js      ← Express Router for /api/posts
    ├── controllers/
    │   └── postController.js  ← route handler functions
    └── middleware/
        ├── auth.js       ← JWT verification middleware
        └── errorHandler.js   ← global error middleware

Common Mistakes

Mistake 1 — Putting all routes in index.js

❌ Wrong — one giant file with all routes and logic mixed together:

// index.js — 500+ lines of mixed routes, DB logic, and middleware
app.get('/api/posts', ...);
app.post('/api/posts', ...);
app.get('/api/users', ...);
app.post('/api/auth/login', ...);

✅ Correct — separate each resource into its own router file and mount it:

// index.js — clean and organised
app.use('/api/posts', require('./src/routes/posts'));
app.use('/api/users', require('./src/routes/users'));
app.use('/api/auth',  require('./src/routes/auth'));

Mistake 2 — Forgetting express.json() middleware

❌ Wrong — req.body is undefined because the JSON parser was not added:

app.post('/api/posts', (req, res) => {
  console.log(req.body); // undefined — express.json() is missing!
});

✅ Correct — always add express.json() before your route definitions:

app.use(express.json()); // ← must come before routes
app.post('/api/posts', (req, res) => {
  console.log(req.body); // { title: '...', body: '...' } ✓
});

Mistake 3 — No error handling middleware

❌ Wrong — unhandled async errors crash the server or return an unhelpful HTML error page to React:

app.get('/api/posts', async (req, res) => {
  const posts = await Post.find(); // if this throws — no catch!
  res.json(posts);
});

✅ Correct — use try/catch (Express 4) or rely on Express 5’s built-in async error handling, and always add a global error middleware:

// Global error middleware — must have 4 parameters
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).json({ success: false, message: err.message });
});

Quick Reference

Task Express Code
Create app const app = express()
Parse JSON bodies app.use(express.json())
Enable CORS app.use(cors())
Define GET route app.get('/path', handler)
URL parameter req.params.id
Query string req.query.search
Request body req.body
Send JSON response res.status(200).json(data)
Create router const router = express.Router()
Start server app.listen(PORT, callback)

🧠 Test Yourself

A POST request to /api/posts is received by Express but req.body is undefined. What is the most likely cause?