Topic 12 of 18 · Node Expert

Configuring Express

Lesson TL;DRTopic 2: Configuring Express 📖 10 min read · 🎯 beginner · 🧭 Prerequisites: introductionandfoundation, themodelviewcontrollerpattern Why this matters Up until now, you've probably installed Express ...
10 min read·beginner·express · node-js · middleware · routing

Topic 2: Configuring Express

📖 10 min read · 🎯 beginner · 🧭 Prerequisites: introduction-and-foundation, the-model-view-controller-pattern

Why this matters

Up until now, you've probably installed Express and maybe sent back a "Hello World" response — and it worked, which felt great. But here's the thing — a real web server isn't just one line. It has layers: something that reads incoming data, something that handles routes, something that serves your images and CSS, something that renders HTML pages. Express gives you all of that, but you have to wire it together. In this lesson, we'll do exactly that — configure Express properly, layer by layer, so your app actually behaves like a professional web server.

What You'll Learn

  • Set up a new Express application from a bare Node.js project
  • Layer built-in, third-party (morgan), and custom middleware in the correct order
  • Define GET, POST, PUT, and DELETE routes including parameterised endpoints
  • Serve static assets from a public/ directory using express.static
  • Render dynamic HTML with the EJS templating engine

The Analogy

Think of an Express application as an airport terminal. Every passenger (HTTP request) enters through the same gate and must pass a series of checkpoints — security screening (body-parser), boarding-pass logging (morgan), and customs (your own middleware) — before finally reaching their gate (a route handler) and boarding the plane (the response). The checkpoints always run in the order they were installed; skip one and a passenger slips through unprocessed. Just as you wouldn't install the departure lounge before the security scanner, middleware registration order in Express is everything.

Chapter 1: Introduction to Express.js

Express.js is a flexible Node.js web application framework that provides a robust set of features for developing web and mobile applications. It simplifies building server-side applications by sitting as a thin, un-opinionated layer on top of Node's built-in http module.

Key features of Express.js

  1. Middleware — functions that execute during the request-response cycle, in registration order
  2. Routing — defines application endpoints and how they respond to client requests by HTTP method and URL pattern
  3. Templating — renders dynamic content using template engines such as EJS, Pug, Handlebars, etc.
  4. Static Files — serves images, CSS, and client-side JavaScript directly from a directory on disk
flowchart LR
    Client -->|HTTP Request| Middleware1[Custom Logger]
    Middleware1 --> Middleware2[morgan]
    Middleware2 --> Middleware3[express.json]
    Middleware3 --> Router[Route Handler]
    Router -->|HTTP Response| Client

Chapter 2: Setting Up an Express Application

Step 1: Create a New Node.js Project

Create a new directory and initialise a Node.js project inside it.

mkdir express-config-demo
cd express-config-demo
npm init -y

Step 2: Install Express

npm install express

Step 3: Create the Server File

Create server.js at the project root to handle incoming HTTP requests.

server.js

const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;

app.get('/', (req, res) => {
    res.send('Hello, Vizag!');
});

app.listen(PORT, () => {
    console.log(`Server running on port ${PORT}`);
});

Step 4: Run the Server

node server.js

Navigating to http://localhost:3000 in a web browser displays Hello, Vizag!.

Chapter 3: Middleware Configuration

Middleware functions intercept every request before it reaches a route handler. They receive (req, res, next) and must call next() to pass control forward.

Step 1: Using Built-in Middleware

Express ships two essential built-in middleware functions for parsing request bodies.

server.js

const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;

// Middleware to parse JSON and URL-encoded data
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

app.get('/', (req, res) => {
    res.send('Hello, Vizag!');
});

app.listen(PORT, () => {
    console.log(`Server running on port ${PORT}`);
});
  • express.json() — parses requests with Content-Type: application/json
  • express.urlencoded({ extended: true }) — parses HTML form submissions

Step 2: Using Third-Party Middleware

Install morgan, the standard HTTP request logger for Express.

npm install morgan

server.js

const express = require('express');
const morgan = require('morgan');
const app = express();
const PORT = process.env.PORT || 3000;

// Middleware to log HTTP requests
app.use(morgan('dev'));

// Middleware to parse JSON and URL-encoded data
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

app.get('/', (req, res) => {
    res.send('Hello, Vizag!');
});

app.listen(PORT, () => {
    console.log(`Server running on port ${PORT}`);
});

The 'dev' format token prints concise, colour-coded output: GET / 200 4.123 ms - 17.

Step 3: Creating Custom Middleware

Custom middleware is just a regular function with the (req, res, next) signature. Here the class adds a simple method-and-URL logger placed before morgan so every request is visible even if morgan is later removed.

server.js

const express = require('express');
const morgan = require('morgan');
const app = express();
const PORT = process.env.PORT || 3000;

// Custom middleware to log request method and URL
app.use((req, res, next) => {
    console.log(`${req.method} ${req.url}`);
    next();
});

// Middleware to log HTTP requests
app.use(morgan('dev'));

// Middleware to parse JSON and URL-encoded data
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

app.get('/', (req, res) => {
    res.send('Hello, Vizag!');
});

app.listen(PORT, () => {
    console.log(`Server running on port ${PORT}`);
});

Calling next() is mandatory — omitting it stalls the request permanently.

Chapter 4: Routing Configuration

Routes map an HTTP method + URL pattern to a handler function.

Step 1: Define Routes for All HTTP Methods

server.js (full CRUD example)

const express = require('express');
const morgan = require('morgan');
const app = express();
const PORT = process.env.PORT || 3000;

app.use(morgan('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// GET all books
app.get('/books', (req, res) => {
    res.json([{ id: 1, title: '1984' }, { id: 2, title: 'Brave New World' }]);
});

// POST a new book
app.post('/books', (req, res) => {
    const newBook = req.body;
    // Add the new book to the database (mock)
    res.status(201).json(newBook);
});

// PUT (update) a book by id
app.put('/books/:id', (req, res) => {
    const bookId = req.params.id;
    const updatedBook = req.body;
    // Update the book in the database (mock)
    res.json({ id: bookId, ...updatedBook });
});

// DELETE a book by id
app.delete('/books/:id', (req, res) => {
    const bookId = req.params.id;
    // Delete the book from the database (mock)
    res.status(204).send();
});

app.get('/', (req, res) => {
    res.send('Hello, Vizag!');
});

app.listen(PORT, () => {
    console.log(`Server running on port ${PORT}`);
});
MethodPathPurpose
GET/booksList all books
POST/booksCreate a book
PUT/books/:idReplace a book
DELETE/books/:idRemove a book

Step 2: Using Route Parameters

The :id segment in a path is a route parameter. Express captures it in req.params.

server.js (adding GET by id)

const express = require('express');
const morgan = require('morgan');
const app = express();
const PORT = process.env.PORT || 3000;

app.use(morgan('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

app.get('/', (req, res) => {
    res.send('Hello, Vizag!');
});

app.get('/books', (req, res) => {
    res.json([{ id: 1, title: '1984' }, { id: 2, title: 'Brave New World' }]);
});

// Fetch a single book by id
app.get('/books/:id', (req, res) => {
    const bookId = req.params.id;
    // Fetch the book from the database (mock)
    const book = { id: bookId, title: 'Mock Book' };
    res.json(book);
});

app.post('/books', (req, res) => {
    const newBook = req.body;
    // Add the new book to the database (mock)
    res.status(201).json(newBook);
});

app.put('/books/:id', (req, res) => {
    const bookId = req.params.id;
    const updatedBook = req.body;
    // Update the book in the database (mock)
    res.json({ id: bookId, ...updatedBook });
});

app.delete('/books/:id', (req, res) => {
    const bookId = req.params.id;
    // Delete the book from the database (mock)
    res.status(204).send();
});

app.listen(PORT, () => {
    console.log(`Server running on port ${PORT}`);
});

A GET /books/42 request populates req.params.id with "42" (always a string — parse with parseInt when needed).

Chapter 5: Serving Static Files

Static files — images, CSS, client-side JS — should be served directly from disk, not through route handlers.

Step 1: Create a Public Directory

Organise static assets under a public/ folder at the project root.

express-config-demo/
├── public/
│   ├── images/
│   ├── css/
│   └── js/
├── server.js
└── package.json

Step 2: Serve Static Files

express.static accepts a root directory and mounts it on the request URL.

server.js

const express = require('express');
const morgan = require('morgan');
const path = require('path');
const app = express();
const PORT = process.env.PORT || 3000;

app.use(morgan('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// Middleware to serve static files
app.use(express.static(path.join(__dirname, 'public')));

app.get('/', (req, res) => {
    res.send('Hello, Vizag!');
});

app.listen(PORT, () => {
    console.log(`Server running on port ${PORT}`);
});

Navigating to http://localhost:3000/css/styles.css serves the file located at public/css/styles.css. path.join(__dirname, 'public') constructs an absolute path, which is safer than relative strings.

Chapter 6: Using a Templating Engine

Templating engines let you compose HTML on the server and inject dynamic data before sending the response. EJS (Embedded JavaScript) is the most straightforward choice — it is plain HTML with <%= expression %> tags.

Step 1: Install EJS

npm install ejs

Step 2: Set Up EJS

Tell Express which engine to use and where to find the template files.

server.js

const express = require('express');
const morgan = require('morgan');
const path = require('path');
const app = express();
const PORT = process.env.PORT || 3000;

app.use(morgan('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(express.static(path.join(__dirname, 'public')));

// Set EJS as the templating engine
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));

app.get('/', (req, res) => {
    res.render('index', { title: 'Home', message: 'Welcome to Vizag!' });
});

app.listen(PORT, () => {
    console.log(`Server running on port ${PORT}`);
});
  • app.set('view engine', 'ejs') — tells Express to use EJS to compile .ejs files
  • app.set('views', ...) — sets the directory Express searches for templates
  • res.render('index', { ... }) — compiles views/index.ejs and injects the data object

Step 3: Create an EJS Template

views/index.ejs

<!DOCTYPE html>
<html>
<head>
    <title><%= title %></title>
</head>
<body>
    <h1><%= message %></h1>
</body>
</html>

<%= title %> and <%= message %> are replaced at render time with the values passed to res.render. Navigating to http://localhost:3000 displays Welcome to Vizag! inside an <h1>.

🧪 Try It Yourself

Task: Extend the books API with a route that returns a single book's details AND a rendered EJS page that displays those details.

  1. Add a views/book.ejs template that accepts id and title variables and renders them in an <h1> and <p>.
  2. Add a GET /books/:id/view route that calls res.render('book', { id: req.params.id, title: 'Mock Book' }).
  3. Start the server with node server.js and visit http://localhost:3000/books/5/view.

Success criterion: The browser displays a page showing Book #5 in a heading and Mock Book in a paragraph — no raw JSON, rendered HTML.

Starter snippet for views/book.ejs:

<!DOCTYPE html>
<html>
<head><title>Book <%= id %></title></head>
<body>
    <h1>Book #<%= id %></h1>
    <p><%= title %></p>
</body>
</html>

🔍 Checkpoint Quiz

Q1. What is the purpose of calling next() inside a middleware function?

A) It ends the request-response cycle and sends a response to the client B) It skips all remaining middleware and jumps directly to the route handler C) It passes control to the next middleware or route handler in the stack D) It restarts the Express application

Q2. Given this server code, what does a GET /books/7 request return?

app.get('/books/:id', (req, res) => {
    const book = { id: req.params.id, title: 'Mock Book' };
    res.json(book);
});

A) { "id": 7, "title": "Mock Book" } B) { "id": "7", "title": "Mock Book" } C) An error because :id is undefined D) { "id": ":id", "title": "Mock Book" }

Q3. What is wrong with this middleware registration order?

app.get('/', (req, res) => res.send('Home'));
app.use(express.json());

A) Nothing — order doesn't matter in Express B) express.json() will never parse the body for GET / requests because the route handler is registered before it C) res.send should be res.json D) express.json() must always be the very first line

Q4. How would you serve a file at public/css/main.css on the path /styles/main.css (a custom mount path)?

A) app.use('/styles', express.static(path.join(__dirname, 'public/css'))) B) app.use(express.static('/styles', path.join(__dirname, 'public/css'))) C) app.get('/styles/main.css', express.static(...)) D) app.use('/public/css', express.static('styles'))

A1. C — next() passes control to the next function in the middleware stack. Without it the request hangs because Express never moves on.

A2. B — Route parameters are always captured as strings. req.params.id is "7", not 7. If you need a number, use parseInt(req.params.id, 10).

A3. B — Express executes middleware in registration order. Because the route handler is registered first, it matches and responds before express.json() ever runs. Body-parsing middleware must be registered before the routes that depend on it.

A4. A — app.use('/styles', express.static(...)) mounts the static middleware at a virtual prefix. A request for /styles/main.css is resolved to public/css/main.css.

🪞 Recap

  • An Express application is assembled by layering middleware functions in registration order; order is significant and intentional.
  • express.json() and express.urlencoded() are built-in middleware that parse request bodies; morgan is a popular third-party logger installed via npm.
  • Routes are matched by HTTP method and URL pattern; dynamic segments like :id are captured in req.params.
  • express.static mounts a file-system directory so assets are served without writing individual route handlers.
  • EJS renders server-side HTML by compiling .ejs templates with a data object passed to res.render.

📚 Further Reading

Like this topic? It’s one of 18 in Node Expert.

Block your seat for ₹2,500 and join the next cohort.