Topic 2: Configuring Express
📖 10 min read · 🎯 beginner · 🧭 Prerequisites: introduction-to-react, the-model-view-controller-pattern
Why this matters
Up until now, Node.js alone can run JavaScript on your server — but it's pretty bare-bones. Every time you want to handle a request, you're writing low-level code that gets messy fast. That's exactly where Express comes in. Express is a lightweight framework that sits on top of Node.js and gives you a clean, structured way to handle routes, process requests, serve files, and build APIs. Once you configure Express properly, your server stops being a rough script and starts feeling like a real application. Let's wire it up.
What You'll Learn
- How to scaffold a new Express application from scratch with
npm initandnpm install express - How to layer middleware — built-in, third-party (
morgan), and custom — onto an Express app - How to define RESTful routes handling GET, POST, PUT, and DELETE requests, including parameterized routes
- How to serve static files from a
public/directory usingexpress.static - How to configure and render dynamic HTML with the EJS templating engine
The Analogy
Think of an Express application as a hotel concierge desk. Every guest (HTTP request) who walks through the front door passes through a sequence of staff members before reaching their room (the route handler): security checks bags (body-parser middleware), a logger notes their arrival (morgan), and a greeter hands them their key (custom middleware). The concierge desk itself — Express — coordinates all of this, and each staff member can either pass the guest along (next()) or send them away with a response right there. Once the guest reaches their room, the room service menu (routing) determines exactly what happens next.
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 abstracting Node's raw http module into a clean, composable API.
Key Features of Express.js
- Middleware — Functions that execute during the request-response cycle, in the order they are registered.
- Routing — Defines application endpoints and specifies how they respond to client requests by HTTP method and URL pattern.
- Templating — Renders dynamic content using template engines such as EJS, Pug, Handlebars, and others.
- Static Files — Serves static assets — images, CSS, and JavaScript — directly from the filesystem.
Chapter 2: Setting Up an Express Application
the trainer walked the class through the four steps to get a server running.
Step 1: Create a New Node.js Project
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:
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!.
sequenceDiagram
participant Browser
participant Express
participant RouteHandler
Browser->>Express: GET /
Express->>RouteHandler: match route "/"
RouteHandler-->>Express: res.send("Hello, Vizag!")
Express-->>Browser: HTTP 200 "Hello, Vizag!"
Chapter 3: Middleware Configuration
Middleware functions sit between the incoming request and the final route handler. Each function receives (req, res, next) and must either send a response or call next() to pass control downstream.
Step 1: Using Built-in Middleware
Express ships with middleware for parsing request bodies:
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;
// Parse JSON bodies (application/json)
app.use(express.json());
// Parse URL-encoded bodies (application/x-www-form-urlencoded)
app.use(express.urlencoded({ extended: true }));
app.get('/', (req, res) => {
res.send('Hello, Vizag!');
});
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Step 2: Using Third-Party Middleware
morgan is a popular HTTP request logger. Install it first:
npm install morgan
Then register it before your routes:
const express = require('express');
const morgan = require('morgan');
const app = express();
const PORT = process.env.PORT || 3000;
// Log every incoming request to stdout
app.use(morgan('dev'));
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 produces compact, colorized output: GET / 200 4.123 ms - 17.
Step 3: Creating Custom Middleware
Custom middleware is any function with the (req, res, next) signature. Here the class logs the HTTP method and URL of every request:
const express = require('express');
const morgan = require('morgan');
const app = express();
const PORT = process.env.PORT || 3000;
// Custom middleware — must call next() to continue the chain
app.use((req, res, next) => {
console.log(`${req.method} ${req.url}`);
next();
});
app.use(morgan('dev'));
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}`);
});
flowchart LR
A[Incoming Request] --> B[Custom Logger Middleware]
B --> C[morgan]
C --> D[express.json]
D --> E[express.urlencoded]
E --> F[Route Handler]
F --> G[Response]
Chapter 4: Routing Configuration
Routing maps HTTP method + URL patterns to handler functions. Express supports app.get(), app.post(), app.put(), and app.delete() (plus others like app.patch()).
Step 1: Define Routes for All CRUD Operations
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!');
});
// List all books
app.get('/books', (req, res) => {
res.json([{ id: 1, title: '1984' }, { id: 2, title: 'Brave New World' }]);
});
// Create 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);
});
// 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.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Step 2: Using Route Parameters
A colon prefix (:id) marks a dynamic segment. Express captures its value on req.params:
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' }]);
});
// Parameterized route — req.params.id holds the captured value
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;
res.status(201).json(newBook);
});
app.put('/books/:id', (req, res) => {
const bookId = req.params.id;
const updatedBook = req.body;
res.json({ id: bookId, ...updatedBook });
});
app.delete('/books/:id', (req, res) => {
const bookId = req.params.id;
res.status(204).send();
});
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
A request to GET /books/42 sets req.params.id to "42" and returns { id: "42", title: "Mock Book" }.
Chapter 5: Serving Static Files
Express can serve an entire directory of static assets — images, CSS, JavaScript — with a single middleware call.
Step 1: Create a Public Directory
Structure your project so static assets live under public/:
express-config-demo/
├── public/
│ ├── images/
│ ├── css/
│ └── js/
├── server.js
└── package.json
Step 2: Register express.static
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 }));
// Serve everything inside /public at the root URL path
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. No route handler needed — Express resolves it automatically.
Chapter 6: Using a Templating Engine
Template engines let route handlers pass data into HTML files instead of building HTML strings manually. EJS (Embedded JavaScript) uses <%= variable %> tags and compiles to standard HTML.
Step 1: Install EJS
npm install ejs
Step 2: Configure Express to Use EJS
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')));
// Tell Express which engine to use and where to find templates
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));
app.get('/', (req, res) => {
// res.render(templateName, dataObject)
res.render('index', { title: 'Home', message: 'Welcome to Vizag!' });
});
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Step 3: Create the EJS Template
Create views/index.ejs:
<!DOCTYPE html>
<html>
<head>
<title><%= title %></title>
</head>
<body>
<h1><%= message %></h1>
</body>
</html>
Navigating to http://localhost:3000 renders the template with title = "Home" and message = "Welcome to Vizag!", producing a page that displays Welcome to Vizag! in an <h1> tag.
🧪 Try It Yourself
Task: Extend the books API with a search query parameter.
Add a route that accepts GET /books/search?title=1984 and returns any mock book whose title contains the query string (case-insensitive).
Starter snippet:
app.get('/books/search', (req, res) => {
const query = req.query.title || '';
const books = [
{ id: 1, title: '1984' },
{ id: 2, title: 'Brave New World' },
{ id: 3, title: 'Fahrenheit 451' },
];
const results = books.filter(b =>
b.title.toLowerCase().includes(query.toLowerCase())
);
res.json(results);
});
Place this route before /books/:id — otherwise Express will treat "search" as an ID parameter.
Success criterion: curl "http://localhost:3000/books/search?title=1984" returns [{"id":1,"title":"1984"}] in your terminal.
🔍 Checkpoint Quiz
Q1. What does calling next() inside a middleware function do?
A) Sends a 200 response to the client
B) Skips all remaining middleware and routes
C) Passes control to the next middleware or route handler in the stack
D) Restarts the Express server
Q2. Given this snippet, what is logged to the console when a client sends GET /books/7?
app.use((req, res, next) => {
console.log(`${req.method} ${req.url}`);
next();
});
A) GET /books
B) GET /books/7
C) 7
D) Nothing — req.url is undefined
Q3. What is the bug in the following route registration order?
app.get('/books/:id', (req, res) => res.json({ id: req.params.id }));
app.get('/books/search', (req, res) => res.json({ results: [] }));
A) app.get cannot be called twice for the same base path
B) /books/search will never be reached because :id matches "search" first
C) req.params.id is undefined for parameterized routes
D) There is no bug — Express evaluates all routes simultaneously
Q4. You want every .css file inside a public/styles/ folder to be accessible at http://localhost:3000/styles/. Which single line of middleware accomplishes this?
A) app.use('/styles', express.static('styles'));
B) app.use(express.static(path.join(__dirname, 'public')));
C) app.get('/styles/*', express.static('public'));
D) app.use('/public', express.static(__dirname));
A1. C — next() hands off to the next middleware or route handler in the registered order. Without calling it (and without sending a response), the request stalls.
A2. B — req.url is the full request path as the client sent it, so GET /books/7 is printed exactly.
A3. B — Express matches routes top-to-bottom. Because /books/:id is registered first, a request to /books/search matches :id = "search" and never reaches the /books/search handler. Fix it by registering the static route before the parameterized one.
A4. B — express.static(path.join(__dirname, 'public')) mounts the entire public/ directory at the root URL, so public/styles/main.css is accessible at /styles/main.css. Option A mounts from a relative styles/ path, not from public/.
🪞 Recap
- Express.js wraps Node's
httpmodule in a routing and middleware system built for fast web-server development. - Middleware functions (
app.use) form an ordered stack; each must callnext()or send a response. morganhandles HTTP request logging andexpress.json()/express.urlencoded()handle body parsing out of the box.- Route parameters (
:id) capture dynamic URL segments viareq.params; query strings are available onreq.query. express.staticserves entire asset directories with zero custom route code.- EJS integrates with Express via
app.set('view engine', 'ejs')and renders templates withres.render(name, data).
📚 Further Reading
- Express.js Official Documentation — the source of truth for all Express APIs and guides
- morgan on npm — full list of log format tokens and stream options
- EJS Official Site — complete EJS tag syntax and template partials reference
- Node.js
pathmodule docs — whypath.join(__dirname, 'public')is safer than a raw string - ⬅️ Previous: The Model View Controller Pattern
- ➡️ Next: Insert, Select, Update, Delete Queries