Express.js Architecture Overview

Module 22: Web Frameworks I (JavaScript) - Monday: Express.js Fundamentals

Introduction to Express.js

Express.js is a minimal, flexible Node.js web application framework that provides a robust set of features for web and mobile applications. It's the most popular web framework for Node.js and has become the de facto standard for building web applications and APIs with Node.js.

flowchart TD Node[Node.js Runtime] --> Express[Express.js Framework] Express --> WebApps[Web Applications] Express --> REST[RESTful APIs] Express --> MVC[MVC Applications] Express --> Microservices[Microservices] style Express fill:#f9f,stroke:#333,stroke-width:2px

The Railway Track Analogy

Express.js can be thought of as a railway system. In this analogy:

  • Routes are like different railway tracks that lead to different destinations
  • Middleware functions are like stations along the track where trains (requests) stop for specific operations
  • Request objects are like trains carrying cargo (data) to their destination
  • Response objects are like trains returning with processed goods
  • The Express application is the central control system that manages the entire network

Just as a railway network provides a structured system for trains to travel efficiently, Express provides a structured framework for HTTP requests to be processed in an organized manner.

Core Features and Philosophy

Express embraces a minimalist philosophy, focusing on providing only essential features while allowing developers to add functionality through middleware. This approach promotes flexibility and prevents the framework from becoming bloated.

Key Features

Companies Using Express.js

Express.js powers the backends of many major companies and platforms:

  • Netflix: Uses Express for some of their internal services
  • Uber: Built many of their early services with Express
  • IBM: Uses Express in their cloud services
  • Accenture: Uses Express for client projects
  • Fox Sports: Powers their web platforms
  • PayPal: Used Express for their Node.js migration

Express.js Architecture

Express follows a modular architecture that consists of several key components working together.

graph TD A[Client Request] --> B[Express Application] B --> C{Routing} C --> D[Middleware Chain] D --> E[Route Handler] E --> F[Response] F --> G[Client] style B fill:#f9d71c,stroke:#333,stroke-width:2px style C fill:#a1ffa8,stroke:#333,stroke-width:2px style D fill:#a1d9ff,stroke:#333,stroke-width:2px

Request-Response Cycle

The heart of Express is its request-response cycle:

  1. A client sends an HTTP request to the Express server
  2. Express receives the request and creates request and response objects
  3. The request passes through a series of middleware functions
  4. The appropriate route handler processes the request
  5. The route handler sends a response back to the client

Express Application Object

The Express application object is the main component that:

Creating an Express Application


// Import the express module
const express = require('express');

// Create an Express application
const app = express();

// Define a route
app.get('/', (req, res) => {
    res.send('Hello World!');
});

// Start the server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
    console.log(`Server running on port ${PORT}`);
});
                

This simple example demonstrates the creation of an Express application, defining a basic route, and starting the server. The app object is the core of our Express application, and all middleware, routes, and other configurations will be added to this object.

Layered Architecture Pattern

While Express itself is minimal, most Express applications follow a layered architecture pattern for better organization and separation of concerns.

graph TB Client[Client] subgraph Express Application Routes[Routes Layer] Controllers[Controllers Layer] Services[Services Layer] DataAccess[Data Access Layer] end Database[(Database)] Client <--> Routes Routes <--> Controllers Controllers <--> Services Services <--> DataAccess DataAccess <--> Database style Express Application fill:#f0f0f0,stroke:#333,stroke-width:1px

Common Express Project Structure


project-root/
  ├── node_modules/
  ├── public/           # Static files (CSS, images, client-side JS)
  ├── src/              # Application source code
  │   ├── config/       # Configuration files
  │   ├── controllers/  # Route handlers
  │   ├── middleware/   # Custom middleware
  │   ├── models/       # Data models
  │   ├── routes/       # Route definitions
  │   ├── services/     # Business logic
  │   ├── utils/        # Utility functions
  │   └── app.js        # Express application setup
  ├── tests/            # Test files
  ├── .env              # Environment variables
  ├── .gitignore
  ├── package.json
  └── README.md
                

This structured approach helps maintain a clean separation of concerns and makes the codebase more manageable as the application grows.

Middleware Architecture

Middleware is a fundamental concept in Express.js. It consists of functions that have access to the request object, the response object, and the next middleware function in the application's request-response cycle.

graph LR A[Request] --> B[Middleware 1] B --> C[Middleware 2] C --> D[Middleware 3] D --> E[Route Handler] E --> F[Response] style B fill:#f9d71c,stroke:#333,stroke-width:2px style C fill:#f9d71c,stroke:#333,stroke-width:2px style D fill:#f9d71c,stroke:#333,stroke-width:2px

The Assembly Line Analogy

Middleware in Express works like an assembly line in a factory:

  • Each station (middleware) performs a specific operation on the product (request)
  • The product moves from one station to the next in a predefined order
  • Any station can modify the product or stop the process if something is wrong
  • The final station (route handler) completes the product (generates the response)

This assembly line approach allows complex processing to be broken down into smaller, focused steps, making the system more modular and maintainable.

Middleware Example


const express = require('express');
const app = express();

// Middleware function 1: Logger
app.use((req, res, next) => {
    console.log(`${new Date().toISOString()} - ${req.method} ${req.url}`);
    next(); // Pass control to the next middleware
});

// Middleware function 2: Request time tracker
app.use((req, res, next) => {
    req.requestTime = Date.now();
    next();
});

// Route with middleware function 3: Authentication check
app.get('/protected', 
    (req, res, next) => {
        // Check if user is authenticated
        const isAuthenticated = req.query.token === 'secret';
        if (!isAuthenticated) {
            return res.status(401).send('Unauthorized');
        }
        next();
    },
    (req, res) => {
        // This is the route handler (final step)
        const responseTime = Date.now() - req.requestTime;
        res.send(`Protected content accessed. Response time: ${responseTime}ms`);
    }
);

// Basic route
app.get('/', (req, res) => {
    const responseTime = Date.now() - req.requestTime;
    res.send(`Hello World! Response time: ${responseTime}ms`);
});

app.listen(3000, () => {
    console.log('Server running on port 3000');
});
                

In this example, we define two application-level middleware functions that execute for every request, and a route-specific middleware that only runs for the '/protected' route. Each middleware has a specific responsibility, demonstrating the single responsibility principle.

Common Types of Express Middleware

Express middleware can be categorized based on their purposes and scope:

Application-level Middleware

Bound to the app object using app.use() or app.METHOD(). These execute for all routes or specific routes.


// Application-level middleware for all routes
app.use((req, res, next) => {
    console.log('Time:', Date.now());
    next();
});

// Application-level middleware for specific path
app.use('/user', (req, res, next) => {
    console.log('Request Type:', req.method);
    next();
});
            

Router-level Middleware

Bound to an instance of express.Router(). These work like application-level middleware but are limited to the router instance.


const router = express.Router();

router.use((req, res, next) => {
    console.log('Router-specific middleware');
    next();
});

router.get('/user', (req, res) => {
    res.send('User page');
});

app.use('/', router);
            

Error-handling Middleware

Takes four arguments (err, req, res, next) and handles errors that occur in the middleware chain.


app.use((err, req, res, next) => {
    console.error(err.stack);
    res.status(500).send('Something broke!');
});
            

Built-in Middleware

Express includes a few built-in middleware functions:


// Serve static files from 'public' directory
app.use(express.static('public'));

// Parse JSON request bodies
app.use(express.json());

// Parse URL-encoded request bodies
app.use(express.urlencoded({ extended: true }));
            

Third-party Middleware

External middleware packages that add functionality to Express applications:


const morgan = require('morgan');
const helmet = require('helmet');
const cors = require('cors');
const compression = require('compression');

app.use(morgan('dev'));
app.use(helmet());
app.use(cors());
app.use(compression());
            

Express vs Other Node.js Frameworks

Express.js is one of many Node.js frameworks, each with its own strengths and weaknesses:

Framework Philosophy Pros Cons Best For
Express.js Minimalist, flexible
  • Lightweight
  • Highly flexible
  • Large ecosystem
  • Simple learning curve
  • Requires manual setup of common features
  • No built-in structure
APIs, microservices, small to medium web apps
Nest.js Structured, Angular-inspired
  • Built-in architecture
  • TypeScript integration
  • Dependency injection
  • Steeper learning curve
  • More boilerplate code
Large enterprise applications, complex systems
Koa.js Next-generation Express
  • Modern async/await
  • Smaller footprint
  • Better error handling
  • Smaller ecosystem
  • Fewer middleware options
Modern applications leveraging latest ES features
Hapi.js Configuration-centric
  • Built-in validation
  • Security features
  • Plugin system
  • More verbose
  • Less flexible in some areas
Enterprise applications, security-focused apps

Why Express Dominates

Despite newer frameworks, Express remains dominant because of:

  • Maturity: Established in 2010, it has a proven track record
  • Ecosystem: Vast library of compatible middleware and extensions
  • Community: Large community means abundant resources and support
  • Flexibility: Doesn't impose rigid structures or patterns
  • Learning Curve: Relatively easy to learn and get started with

Express.js Use Cases

Express.js is versatile and can be used in various scenarios:

RESTful APIs

Express excels at building RESTful APIs due to its routing capabilities and middleware system.


// Simple REST API example
const express = require('express');
const app = express();
app.use(express.json());

let users = [
    { id: 1, name: 'Alice' },
    { id: 2, name: 'Bob' }
];

// GET all users
app.get('/api/users', (req, res) => {
    res.json(users);
});

// GET a single user
app.get('/api/users/:id', (req, res) => {
    const user = users.find(u => u.id === parseInt(req.params.id));
    if (!user) return res.status(404).send('User not found');
    res.json(user);
});

// POST a new user
app.post('/api/users', (req, res) => {
    const user = {
        id: users.length + 1,
        name: req.body.name
    };
    users.push(user);
    res.status(201).json(user);
});

app.listen(3000);
            

Server-rendered Web Applications

With template engines, Express can render dynamic HTML pages on the server.


const express = require('express');
const app = express();

// Set up EJS as the view engine
app.set('view engine', 'ejs');
app.set('views', './views');

// Serve static files
app.use(express.static('public'));

// Route that renders a template
app.get('/profile/:username', (req, res) => {
    // In a real app, you'd fetch this data from a database
    const userData = {
        username: req.params.username,
        bio: 'Lorem ipsum dolor sit amet',
        joinDate: '2023-01-15',
        posts: ['Post 1', 'Post 2', 'Post 3']
    };
    
    res.render('profile', userData);
});

app.listen(3000);
            

Real-time Applications

Combined with Socket.IO, Express can power real-time applications like chat systems or live dashboards.


const express = require('express');
const http = require('http');
const socketIo = require('socket.io');

const app = express();
const server = http.createServer(app);
const io = socketIo(server);

app.use(express.static('public'));

app.get('/', (req, res) => {
    res.sendFile(__dirname + '/public/index.html');
});

// Socket.IO connection handling
io.on('connection', (socket) => {
    console.log('A user connected');
    
    socket.on('chat message', (msg) => {
        io.emit('chat message', msg); // Broadcast to all connected clients
    });
    
    socket.on('disconnect', () => {
        console.log('A user disconnected');
    });
});

server.listen(3000, () => {
    console.log('Server running on port 3000');
});
            

Microservices

Express is lightweight enough to be used in microservice architectures, where each service is a small, focused Express application.


// user-service.js
const express = require('express');
const app = express();
app.use(express.json());

app.get('/users', async (req, res) => {
    // Get users from database
    const users = await db.collection('users').find().toArray();
    res.json(users);
});

app.listen(3001, () => {
    console.log('User service running on port 3001');
});

// product-service.js
const express = require('express');
const app = express();
app.use(express.json());

app.get('/products', async (req, res) => {
    // Get products from database
    const products = await db.collection('products').find().toArray();
    res.json(products);
});

app.listen(3002, () => {
    console.log('Product service running on port 3002');
});
            

Practical Exercises

Try these exercises to practice and reinforce your understanding of Express.js architecture:

Exercise 1: Set Up a Basic Express Server

Objective: Create a simple Express server with routes for a blog application.

Tasks:

  1. Initialize a new Node.js project and install Express
  2. Create a basic Express server that listens on port 3000
  3. Add routes for the following endpoints:
    • GET /posts - Return a list of blog posts
    • GET /posts/:id - Return a single blog post
    • GET /authors - Return a list of authors
    • GET /authors/:id - Return a single author
  4. Use a middleware to log all incoming requests
  5. Implement error handling for non-existent routes

Exercise 2: Middleware Chain

Objective: Create an Express application with multiple middleware functions to understand the middleware flow.

Tasks:

  1. Create an Express server with the following middleware:
    • A logger middleware that logs the request method, URL, and timestamp
    • A request time tracker middleware that adds the request start time to req object
    • An authentication middleware that checks for a query parameter called 'token'
  2. Create routes that use these middleware in different combinations
  3. Add a custom error handler middleware
  4. Test different request scenarios and observe how the middleware chain behaves

Exercise 3: Express Application Structure

Objective: Organize an Express application using a proper directory structure.

Tasks:

  1. Create a new Express project with the following structure:
    
    project/
      ├── src/
      │   ├── controllers/
      │   ├── middleware/
      │   ├── routes/
      │   ├── services/
      │   └── app.js
      ├── public/
      ├── package.json
      └── README.md
                            
  2. Implement a simple blog API with posts and comments
  3. Organize the code according to the directory structure:
    • Routes in the routes directory
    • Controller logic in the controllers directory
    • Custom middleware in the middleware directory
    • Business logic in the services directory
  4. Use dependency injection to pass services to controllers

Further Resources