Topic 27 of 56 · Full Stack Advanced

Topic 2 : Configuring Express

Lesson TL;DRTopic 2: Configuring Express 📖 10 min read · 🎯 beginner · 🧭 Prerequisites: introductiontoreact, themodelviewcontrollerpattern Why this matters Up until now, Node.js alone can run JavaScript on your...
10 min read·beginner·express · nodejs · middleware · routing

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 init and npm 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 using express.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

  1. Middleware — Functions that execute during the request-response cycle, in the order they are registered.
  2. Routing — Defines application endpoints and specifies 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, and others.
  4. 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 http module in a routing and middleware system built for fast web-server development.
  • Middleware functions (app.use) form an ordered stack; each must call next() or send a response.
  • morgan handles HTTP request logging and express.json() / express.urlencoded() handle body parsing out of the box.
  • Route parameters (:id) capture dynamic URL segments via req.params; query strings are available on req.query.
  • express.static serves entire asset directories with zero custom route code.
  • EJS integrates with Express via app.set('view engine', 'ejs') and renders templates with res.render(name, data).

📚 Further Reading

Like this topic? It’s one of 56 in Full Stack Advanced.

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