Creating Your First Express Server

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"
  }
}
Note: You do not need a .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.
Tip: Keep your server’s entry point as 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.
Warning: Always add 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(...))

🧠 Test Yourself

Your Express server has three routes defined, followed by a 404 handler. A client requests a path that does not match any of the three routes. What happens?