The fastest way to understand Express is to build something with it. In this lesson you will install Express, write a server from scratch, define your first routes, and confirm the server responds correctly. By the end you will have a running Express server that serves JSON responses โ the foundation every subsequent chapter in this series will build on. Keep this server file open as you follow along; every new concept you learn will be added directly to it.
Step 1 โ Project Setup
# Create and enter your server directory
mkdir mern-blog && cd mern-blog
mkdir server && cd server
# Initialise npm
npm init -y
# Install Express and dotenv
npm install express dotenv
# Install nodemon as a dev dependency (auto-restart on save)
npm install -D nodemon
# Open in VS Code
code .
// server/package.json โ add scripts section
{
"scripts": {
"start": "node index.js",
"dev": "nodemon index.js"
}
}
.env file yet for this first server โ you will add one in a later lesson when connecting to MongoDB. For now the PORT variable will fall back to the default 5000 via the || 5000 in the code below.index.js at the root of the server/ directory. This is the conventional name Node.js looks for when you run node . (dot = current directory), and it is what hosting platforms like Render expect by default when you set the start command to node index.js.require('dotenv').config() as the very first line of index.js โ before any other requires. If you load dotenv after requiring other modules, those modules will have already initialised without access to your environment variables, causing hard-to-debug undefined errors.Step 2 โ Write the Server
// server/index.js
require('dotenv').config(); // load .env first โ before anything else
const express = require('express');
const app = express();
const PORT = process.env.PORT || 5000;
// โโ Middleware โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
app.use(express.json()); // parse incoming JSON request bodies
// โโ Routes โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
app.get('/', (req, res) => {
res.json({ message: 'Welcome to the MERN Blog API' });
});
app.get('/api/health', (req, res) => {
res.json({
status: 'ok',
environment: process.env.NODE_ENV || 'development',
timestamp: new Date().toISOString(),
});
});
// โโ 404 handler โ must come AFTER all routes โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
app.use((req, res) => {
res.status(404).json({ success: false, message: 'Route not found' });
});
// โโ Start server โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
Step 3 โ Run and Verify
# Start the server with nodemon
npm run dev
# Expected console output:
# [nodemon] starting `node index.js`
# Server running on http://localhost:5000
Testing in the browser or Postman:
GET http://localhost:5000/
Response: { "message": "Welcome to the MERN Blog API" }
GET http://localhost:5000/api/health
Response: {
"status": "ok",
"environment": "development",
"timestamp": "2025-05-28T10:00:00.000Z"
}
GET http://localhost:5000/api/nonexistent
Response: 404 { "success": false, "message": "Route not found" }
Understanding the Express Application Object
const app = express();
// app is a function โ it IS the request handler (can be passed to http.createServer)
// app also has methods for:
app.use() // register middleware or mount a sub-router
app.get() // handle HTTP GET
app.post() // handle HTTP POST
app.put() // handle HTTP PUT
app.patch() // handle HTTP PATCH
app.delete() // handle HTTP DELETE
app.listen() // start the HTTP server on a port
app.set() // configure app settings
app.get('setting') // read app settings (overloaded โ also GET route if path given)
// App settings
app.set('trust proxy', 1); // trust first reverse proxy (needed for Render/Heroku)
app.set('x-powered-by', false); // hide Express version header
The req and res Objects โ First Look
app.get('/api/demo', (req, res) => {
// โโ req โ the incoming request โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
console.log(req.method); // 'GET'
console.log(req.path); // '/api/demo'
console.log(req.url); // '/api/demo?name=Jane'
console.log(req.headers); // { host: 'localhost:5000', ... }
console.log(req.query); // { name: 'Jane' } (from ?name=Jane)
console.log(req.params); // {} (no URL params in this route)
console.log(req.body); // {} (no body on a GET)
console.log(req.ip); // '::1' (localhost)
// โโ res โ sending the response โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
res.status(200) // set HTTP status code
.json({ received: req.query }); // send JSON response
});
Structuring for Growth
// index.js โ clean structure ready to grow
require('dotenv').config();
const express = require('express');
const cors = require('cors'); // npm install cors
const app = express();
const PORT = process.env.PORT || 5000;
// โโ Global middleware โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: false })); // parse form data too
// โโ API routes (will be extracted to separate files in Chapter 6) โโโโโโโโโโโโโ
app.get('/api/health', (req, res) => res.json({ status: 'ok' }));
// โโ Global error handler (must have 4 arguments) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
app.use((err, req, res, next) => {
const statusCode = err.status || 500;
res.status(statusCode).json({
success: false,
message: err.message || 'Internal Server Error',
});
});
// โโ 404 handler โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
app.use((req, res) => {
res.status(404).json({ success: false, message: `Cannot ${req.method} ${req.path}` });
});
app.listen(PORT, () => console.log(`Server running โ http://localhost:${PORT}`));
Common Mistakes
Mistake 1 โ Putting the 404 handler before routes
โ Wrong โ 404 middleware defined before routes catches everything:
app.use((req, res) => res.status(404).json({ message: 'Not found' }));
app.get('/api/health', handler); // never reached โ 404 handler caught it first
โ
Correct โ the 404 handler must be the very last app.use() call, after all routes:
app.get('/api/health', handler); // routes first
app.use((req, res) => res.status(404).json({ message: 'Not found' })); // 404 last
Mistake 2 โ Not calling res.json() or res.end()
โ Wrong โ route handler that never sends a response:
app.get('/api/posts', (req, res) => {
const posts = getPosts();
console.log(posts); // logs but never sends โ client hangs forever
});
โ Correct โ every route handler must send exactly one response:
app.get('/api/posts', (req, res) => {
const posts = getPosts();
res.json({ success: true, data: posts }); // always send a response โ
});
Mistake 3 โ Calling res.json() twice
โ Wrong โ sending two responses from one handler:
app.get('/api/posts', (req, res) => {
if (someCondition) res.json({ data: [] });
res.json({ data: allPosts }); // Error: Cannot set headers after they are sent
});
โ
Correct โ use return to stop execution after sending:
if (someCondition) return res.json({ data: [] }); // return prevents fall-through โ
res.json({ data: allPosts });
Quick Reference
| Task | Code |
|---|---|
| Create app | const app = express() |
| Parse JSON bodies | app.use(express.json()) |
| Define GET route | app.get('/path', (req, res) => {}) |
| Send JSON response | res.json({ key: value }) |
| Send with status | res.status(201).json(data) |
| Get query string | req.query.paramName |
| Get URL parameter | req.params.id |
| Get request body | req.body |
| Start listening | app.listen(PORT, callback) |
| 404 fallthrough | app.use((req, res) => res.status(404).json(...)) |