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 usingexpress.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
- Middleware — functions that execute during the request-response cycle, in registration order
- Routing — defines application endpoints and how they respond to client requests by HTTP method and URL pattern
- Templating — renders dynamic content using template engines such as EJS, Pug, Handlebars, etc.
- 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 withContent-Type: application/jsonexpress.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}`);
});
| Method | Path | Purpose |
|---|---|---|
| GET | /books | List all books |
| POST | /books | Create a book |
| PUT | /books/:id | Replace a book |
| DELETE | /books/:id | Remove 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.ejsfilesapp.set('views', ...)— sets the directory Express searches for templatesres.render('index', { ... })— compilesviews/index.ejsand 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.
- Add a
views/book.ejstemplate that acceptsidandtitlevariables and renders them in an<h1>and<p>. - Add a
GET /books/:id/viewroute that callsres.render('book', { id: req.params.id, title: 'Mock Book' }). - Start the server with
node server.jsand visithttp://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()andexpress.urlencoded()are built-in middleware that parse request bodies;morganis a popular third-party logger installed via npm.- Routes are matched by HTTP method and URL pattern; dynamic segments like
:idare captured inreq.params. express.staticmounts a file-system directory so assets are served without writing individual route handlers.- EJS renders server-side HTML by compiling
.ejstemplates with a data object passed tores.render.
📚 Further Reading
- Express.js Official Docs — the source of truth for routing, middleware, and configuration options
- morgan on npm — full list of predefined format tokens and custom format strings
- EJS Official Site — complete tag reference, partials, and layout patterns
- Node.js
pathmodule — whypath.join(__dirname, ...)is safer than string concatenation for file paths - ⬅️ Previous: The Model View Controller Pattern
- ➡️ Next: Node Projects