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.
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
- Routing: Define application endpoints and HTTP methods
- Middleware: Process requests through a chain of functions
- Template Engines: Render dynamic HTML using various templating libraries
- Error Handling: Centralized error processing mechanisms
- Static File Serving: Efficiently serve static assets
- HTTP Utility Methods: Simplified HTTP response handling
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.
Request-Response Cycle
The heart of Express is its request-response cycle:
- A client sends an HTTP request to the Express server
- Express receives the request and creates request and response objects
- The request passes through a series of middleware functions
- The appropriate route handler processes the request
- The route handler sends a response back to the client
Express Application Object
The Express application object is the main component that:
- Configures middleware
- Defines routes
- Starts the HTTP server
- Handles errors
- Manages other application settings
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.
- Routes Layer: Defines endpoints and HTTP methods, delegates to controllers
- Controllers Layer: Processes HTTP requests/responses, calls services for business logic
- Services Layer: Contains business logic, coordinates between multiple data sources
- Data Access Layer: Handles database interactions and data operations
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.
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:
express.static: Serves static assetsexpress.json: Parses incoming requests with JSON payloadsexpress.urlencoded: Parses incoming requests with URL-encoded payloads
// 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:
morgan: HTTP request loggerhelmet: Helps secure Express apps by setting HTTP headerscors: Enables Cross-Origin Resource Sharingcompression: Compresses response bodies
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 |
|
|
APIs, microservices, small to medium web apps |
| Nest.js | Structured, Angular-inspired |
|
|
Large enterprise applications, complex systems |
| Koa.js | Next-generation Express |
|
|
Modern applications leveraging latest ES features |
| Hapi.js | Configuration-centric |
|
|
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:
- Initialize a new Node.js project and install Express
- Create a basic Express server that listens on port 3000
- 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
- Use a middleware to log all incoming requests
- 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:
- 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'
- Create routes that use these middleware in different combinations
- Add a custom error handler middleware
- 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:
- Create a new Express project with the following structure:
project/ ├── src/ │ ├── controllers/ │ ├── middleware/ │ ├── routes/ │ ├── services/ │ └── app.js ├── public/ ├── package.json └── README.md - Implement a simple blog API with posts and comments
- 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
- Use dependency injection to pass services to controllers