What is Express.js?
Express.js (or simply Express) is a minimal and flexible web application framework for Node.js. It provides a robust set of features for building single-page, multi-page, and hybrid web applications, as well as APIs. Express has become the de facto standard framework for Node.js web application development due to its simplicity, flexibility, and extensive ecosystem.
Definition
Express.js is a fast, unopinionated, minimalist web framework for Node.js that simplifies the process of building web applications and APIs by providing a thin layer of fundamental web application features without obscuring Node.js features.
Why Use Express.js?
- Simplicity: Express provides a thin layer of fundamental web application features without obscuring Node.js features
- Flexibility: It doesn't enforce specific patterns, allowing developers to structure applications as they see fit
- Performance: Built for speed and optimized for high-throughput web applications
- Middleware Support: Powerful middleware architecture for handling request/response cycle
- Routing: Simple and expressive route definition with support for HTTP methods
- Ecosystem: Large ecosystem of middleware modules and extensions
- Community: Active community and extensive documentation
The LEGO Building Blocks Analogy
Think of Express.js as a specialized set of LEGO building blocks for constructing web applications:
- Node.js provides the foundation board (the runtime environment)
- Express.js gives you pre-designed blocks that fit together perfectly (routing, middleware, etc.)
- You can add exactly the blocks you need without unnecessary pieces (minimalist approach)
- Blocks can be assembled in various ways (flexibility in application structure)
- Additional specialized block sets are available when needed (middleware modules)
- You can still use the standard blocks alongside your specialized ones (direct access to Node.js features)
Just like LEGO, Express lets you build something simple quickly, yet provides all the pieces needed to construct highly complex structures when required.
Express.js vs. Vanilla Node.js
To understand the value Express brings, let's compare a simple "Hello World" server in vanilla Node.js versus Express:
Vanilla Node.js
// Vanilla Node.js Server
const http = require('http');
const server = http.createServer((req, res) => {
// Parse URL to extract path
const url = req.url;
// Route handling
if (url === '/') {
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end('<h1>Hello, World!</h1>');
} else if (url === '/about') {
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end('<h1>About Page</h1>');
} else if (url === '/api/users') {
const users = [
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' }
];
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(users));
} else {
res.writeHead(404, { 'Content-Type': 'text/html' });
res.end('<h1>404 Not Found</h1>');
}
});
const PORT = 3000;
server.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Express.js
// Express.js Server
const express = require('express');
const app = express();
// Route handling
app.get('/', (req, res) => {
res.send('<h1>Hello, World!</h1>');
});
app.get('/about', (req, res) => {
res.send('<h1>About Page</h1>');
});
app.get('/api/users', (req, res) => {
const users = [
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' }
];
res.json(users);
});
// 404 route (must be defined last)
app.use((req, res) => {
res.status(404).send('<h1>404 Not Found</h1>');
});
const PORT = 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Key Differences
| Feature | Vanilla Node.js | Express.js |
|---|---|---|
| Routing | Manual URL parsing and conditional logic | Method-based routing (app.get, app.post, etc.) |
| Content Type | Manual header setting (res.writeHead) | Automatic content type inference (res.send, res.json) |
| Status Codes | Set in writeHead method | Chainable methods (res.status(404).send()) |
| Error Handling | Manual try/catch blocks | Built-in error handling middleware |
| Middleware | Must be implemented manually | Rich middleware ecosystem |
| Extensions | Custom implementation required | Wide range of plug-and-play modules |
| Code Complexity | Increases rapidly with application size | Scales well with modular approach |
When to Use Vanilla Node.js vs. Express
Use Vanilla Node.js when:
- Building extremely simple applications
- Maximum performance is critical and you need to avoid any overhead
- Learning the basics of Node.js before moving to frameworks
- Developing specialized servers with unique requirements
Use Express.js when:
- Building web applications or APIs of any significant complexity
- You need a structured approach to routing and middleware
- You want to leverage the ecosystem of middleware modules
- Development speed and maintainability are priorities
Setting Up Express.js
Let's get started with Express by setting up a basic application.
Prerequisites
- Node.js installed (preferably the LTS version)
- npm (Node Package Manager) - comes with Node.js
- A code editor (VS Code, Sublime Text, etc.)
- Terminal or command prompt
Step 1: Initialize a New Node.js Project
// Create a new directory for your project
mkdir express-demo
cd express-demo
// Initialize a new Node.js project
npm init -y
This creates a package.json file with default values. The -y flag accepts all defaults without prompting.
Step 2: Install Express
npm install express
This installs the Express framework and adds it to your project's dependencies in package.json.
Step 3: Create Your First Express Server
Create a file named app.js in your project directory:
// app.js
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;
// Middleware to parse JSON bodies
app.use(express.json());
// Middleware to serve static files
app.use(express.static('public'));
// Route for the homepage
app.get('/', (req, res) => {
res.send('<h1>Welcome to Express!</h1>');
});
// Route with URL parameter
app.get('/users/:id', (req, res) => {
res.send(`User ID: ${req.params.id}`);
});
// Route for API endpoint
app.get('/api/info', (req, res) => {
res.json({
name: 'Express Demo API',
version: '1.0.0',
status: 'active'
});
});
// POST route example
app.post('/api/data', (req, res) => {
console.log('Request Body:', req.body);
res.status(201).json({
message: 'Data received successfully',
data: req.body
});
});
// Start the server
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Step 4: Create a Public Directory for Static Files
mkdir public
Create a file named index.html in the public directory:
// public/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Express.js Demo</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
h1 {
color: #333;
}
.container {
border: 1px solid #ddd;
padding: 20px;
border-radius: 5px;
}
.btn {
background-color: #4CAF50;
color: white;
padding: 10px 15px;
border: none;
border-radius: 4px;
cursor: pointer;
}
.response {
margin-top: 20px;
padding: 10px;
background-color: #f5f5f5;
border-radius: 4px;
min-height: 100px;
}
</style>
</head>
<body>
<h1>Express.js Demo</h1>
<div class="container">
<h2>Test API Endpoints</h2>
<div>
<button class="btn" id="getInfoBtn">Get API Info</button>
<button class="btn" id="postDataBtn">Post Data</button>
</div>
<div class="response" id="responseContainer">
Response will appear here...
</div>
</div>
<script>
// Get elements
const getInfoBtn = document.getElementById('getInfoBtn');
const postDataBtn = document.getElementById('postDataBtn');
const responseContainer = document.getElementById('responseContainer');
// Event listeners
getInfoBtn.addEventListener('click', getApiInfo);
postDataBtn.addEventListener('click', postData);
// Get API info
async function getApiInfo() {
try {
const response = await fetch('/api/info');
const data = await response.json();
responseContainer.innerHTML = '<pre>' + JSON.stringify(data, null, 2) + '</pre>';
} catch (error) {
responseContainer.innerHTML = 'Error: ' + error.message;
}
}
// Post data
async function postData() {
try {
const data = {
name: 'Test User',
email: 'test@example.com',
message: 'Hello from the client!',
timestamp: new Date().toISOString()
};
const response = await fetch('/api/data', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
const responseData = await response.json();
responseContainer.innerHTML = '<pre>' + JSON.stringify(responseData, null, 2) + '</pre>';
} catch (error) {
responseContainer.innerHTML = 'Error: ' + error.message;
}
}
</script>
</body>
</html>
Step 5: Run Your Express Server
node app.js
Open your browser and navigate to http://localhost:3000 to see your application in action.
Express.js Core Concepts
The Application Object
The express() function creates an Express application object, which has methods for:
- Routing HTTP requests (
app.get(),app.post(), etc.) - Configuring middleware (
app.use()) - Rendering HTML views (
app.render()) - Registering template engines (
app.engine()) - Starting the server (
app.listen())
const express = require('express');
const app = express(); // Create an Express application
// Configuration
app.set('view engine', 'ejs');
app.set('views', './views');
// Start the server
app.listen(3000, () => {
console.log('Server started on port 3000');
});
Routing
Routing refers to determining how an application responds to a client request to a particular endpoint, which is a URI (or path) and a specific HTTP request method (GET, POST, etc.).
// Basic route structure
app.METHOD(PATH, HANDLER);
// Examples
app.get('/', (req, res) => {
res.send('Hello World!');
});
app.post('/login', (req, res) => {
// Handle login logic
res.send('Login processed');
});
app.put('/users/:id', (req, res) => {
res.send(`Update user with ID: ${req.params.id}`);
});
app.delete('/users/:id', (req, res) => {
res.send(`Delete user with ID: ${req.params.id}`);
});
Route Parameters
Route parameters are named URL segments used to capture values at specific positions in the URL. The captured values are stored in the req.params object.
// Route with parameters
app.get('/users/:userId/books/:bookId', (req, res) => {
res.send(`User ID: ${req.params.userId}, Book ID: ${req.params.bookId}`);
});
// Example URL: /users/34/books/8989
// req.params: { "userId": "34", "bookId": "8989" }
Middleware
Middleware functions are functions that have access to the request object (req), the response object (res), and the next middleware function in the application's request-response cycle. Middleware can:
- Execute any code
- Make changes to the request and response objects
- End the request-response cycle
- Call the next middleware in the stack
// Middleware function structure
function myMiddleware(req, res, next) {
// Do something with req and res
console.log('Middleware executed');
// Call next() to pass control to the next middleware
next();
}
// Apply middleware to all routes
app.use(myMiddleware);
// Apply middleware to a specific route
app.get('/protected', myMiddleware, (req, res) => {
res.send('Protected route');
});
// Middleware for error handling
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).send('Something broke!');
});
Common Express Middleware
express.json(): Parses incoming requests with JSON payloadsexpress.urlencoded(): Parses incoming requests with URL-encoded payloadsexpress.static(): Serves static filesmorgan: HTTP request loggercors: Enables Cross-Origin Resource Sharinghelmet: Helps secure Express apps with various HTTP headerscompression: Compresses response bodies
// Example of using common middleware
const express = require('express');
const morgan = require('morgan');
const cors = require('cors');
const helmet = require('helmet');
const compression = require('compression');
const app = express();
// Apply middleware
app.use(morgan('dev')); // Logging
app.use(cors()); // Enable CORS for all routes
app.use(helmet()); // Security headers
app.use(compression()); // Compress responses
app.use(express.json()); // Parse JSON bodies
app.use(express.urlencoded({ extended: true })); // Parse URL-encoded bodies
Request and Response Objects
Express.js extends Node.js's request and response objects with additional properties and methods that simplify common tasks.
Request Object (req)
| Property/Method | Description | Example |
|---|---|---|
req.params |
URL parameters | /users/:id → req.params.id |
req.query |
Query string parameters | /search?q=express → req.query.q |
req.body |
Request body (requires middleware) | req.body.username |
req.headers |
HTTP headers | req.headers['user-agent'] |
req.cookies |
Cookies (requires cookie-parser) | req.cookies.sessionId |
req.path |
Path part of URL | /users/profile → req.path |
req.method |
HTTP method | 'GET', 'POST', etc. |
Response Object (res)
| Method | Description | Example |
|---|---|---|
res.send() |
Sends the HTTP response | res.send('Hello World!'); |
res.json() |
Sends a JSON response | res.json({ name: 'John' }); |
res.status() |
Sets the HTTP status code | res.status(404).send('Not Found'); |
res.sendFile() |
Sends a file | res.sendFile('index.html'); |
res.render() |
Renders a view template | res.render('index', { title: 'Home' }); |
res.redirect() |
Redirects to another URL | res.redirect('/login'); |
res.cookie() |
Sets a cookie | res.cookie('session', '12345'); |
res.clearCookie() |
Clears a cookie | res.clearCookie('session'); |
// Example using request and response objects
app.get('/api/search', (req, res) => {
const query = req.query.q;
const limit = parseInt(req.query.limit) || 10;
if (!query) {
return res.status(400).json({
error: 'Missing search query'
});
}
// Log headers for debugging
console.log('Request Headers:', req.headers);
// Perform search (mock data)
const results = mockDatabase.search(query, limit);
// Send JSON response with search results
res.json({
query,
limit,
count: results.length,
results
});
});
Real-World Express.js Application Structure
In real-world applications, you'll want to organize your code in a modular way. Here's a common project structure for Express applications:
project-root/
├── node_modules/
├── public/
│ ├── css/
│ ├── js/
│ └── images/
├── src/
│ ├── config/
│ │ └── database.js
│ ├── controllers/
│ │ ├── userController.js
│ │ └── productController.js
│ ├── middleware/
│ │ ├── auth.js
│ │ └── errorHandler.js
│ ├── models/
│ │ ├── User.js
│ │ └── Product.js
│ ├── routes/
│ │ ├── userRoutes.js
│ │ └── productRoutes.js
│ └── utils/
│ └── helpers.js
├── views/
│ ├── layouts/
│ │ └── main.ejs
│ ├── partials/
│ │ ├── header.ejs
│ │ └── footer.ejs
│ ├── index.ejs
│ └── user.ejs
├── app.js
├── package.json
└── README.md
Let's look at how each component would be organized:
Main Application File (app.js)
// app.js
const express = require('express');
const path = require('path');
const cookieParser = require('cookie-parser');
const logger = require('morgan');
const errorHandler = require('./src/middleware/errorHandler');
// Import routes
const userRoutes = require('./src/routes/userRoutes');
const productRoutes = require('./src/routes/productRoutes');
// Create Express app
const app = express();
// View engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');
// Middleware
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
// Routes
app.use('/api/users', userRoutes);
app.use('/api/products', productRoutes);
// Home route
app.get('/', (req, res) => {
res.render('index', { title: 'Express App' });
});
// Error handling middleware (should be last)
app.use(errorHandler);
// Start server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
module.exports = app;
Routes File
// src/routes/userRoutes.js
const express = require('express');
const router = express.Router();
const userController = require('../controllers/userController');
const authMiddleware = require('../middleware/auth');
// GET all users
router.get('/', userController.getAllUsers);
// GET single user
router.get('/:id', userController.getUserById);
// POST new user
router.post('/', userController.createUser);
// PUT update user (protected route requiring authentication)
router.put('/:id', authMiddleware.verifyToken, userController.updateUser);
// DELETE user (protected route requiring authentication)
router.delete('/:id', authMiddleware.verifyToken, userController.deleteUser);
module.exports = router;
Controller File
// src/controllers/userController.js
const User = require('../models/User');
// Get all users
exports.getAllUsers = async (req, res, next) => {
try {
const users = await User.findAll();
res.status(200).json(users);
} catch (error) {
next(error); // Pass to error handler middleware
}
};
// Get user by ID
exports.getUserById = async (req, res, next) => {
try {
const user = await User.findById(req.params.id);
if (!user) {
return res.status(404).json({ message: 'User not found' });
}
res.status(200).json(user);
} catch (error) {
next(error);
}
};
// Create new user
exports.createUser = async (req, res, next) => {
try {
const { name, email, password } = req.body;
// Validate input
if (!name || !email || !password) {
return res.status(400).json({ message: 'Please provide name, email, and password' });
}
// Check if user already exists
const existingUser = await User.findByEmail(email);
if (existingUser) {
return res.status(409).json({ message: 'Email already in use' });
}
// Create user
const newUser = await User.create({ name, email, password });
res.status(201).json({
message: 'User created successfully',
user: {
id: newUser.id,
name: newUser.name,
email: newUser.email
}
});
} catch (error) {
next(error);
}
};
// Update user
exports.updateUser = async (req, res, next) => {
try {
const { name, email } = req.body;
const userId = req.params.id;
// Check if user exists
const user = await User.findById(userId);
if (!user) {
return res.status(404).json({ message: 'User not found' });
}
// Update user
const updatedUser = await User.update(userId, { name, email });
res.status(200).json({
message: 'User updated successfully',
user: updatedUser
});
} catch (error) {
next(error);
}
};
// Delete user
exports.deleteUser = async (req, res, next) => {
try {
const userId = req.params.id;
// Check if user exists
const user = await User.findById(userId);
if (!user) {
return res.status(404).json({ message: 'User not found' });
}
// Delete user
await User.delete(userId);
res.status(200).json({
message: 'User deleted successfully'
});
} catch (error) {
next(error);
}
};
Middleware File
// src/middleware/auth.js
const jwt = require('jsonwebtoken');
exports.verifyToken = (req, res, next) => {
// Get auth header
const authHeader = req.headers.authorization;
if (!authHeader) {
return res.status(401).json({ message: 'Access denied. No token provided.' });
}
// Check if bearer token
const parts = authHeader.split(' ');
if (parts.length !== 2 || parts[0] !== 'Bearer') {
return res.status(401).json({ message: 'Token format is invalid.' });
}
const token = parts[1];
try {
// Verify token
const decoded = jwt.verify(token, process.env.JWT_SECRET);
// Add user data to request
req.user = decoded;
// Proceed to the next middleware or route handler
next();
} catch (error) {
return res.status(401).json({ message: 'Invalid token.' });
}
};
Error Handler Middleware
// src/middleware/errorHandler.js
// Central error handling middleware
module.exports = (err, req, res, next) => {
// Log error
console.error(err.stack);
// Set default error status and message
const status = err.statusCode || 500;
const message = err.message || 'Something went wrong on the server';
// Send error response
res.status(status).json({
status: 'error',
statusCode: status,
message
});
};
Express.js Best Practices
- Use environment variables for configuration (port, database credentials, secret keys)
- Implement proper error handling with middleware
- Structure your application in a modular way (routes, controllers, models)
- Use async/await for cleaner asynchronous code
- Validate user input to prevent security issues
- Set appropriate security headers with middleware like helmet
- Use logging for debugging and monitoring
- Implement rate limiting to prevent abuse
- Use compression to improve performance
- Follow RESTful principles for API design
Practical Exercise
Exercise: Build a RESTful API for a Task Manager
Let's create a simple RESTful API for a task manager application using Express.js.
Step 1: Setup Project
mkdir task-manager-api
cd task-manager-api
npm init -y
npm install express uuid
Step 2: Create the Main Application File
Create a file named app.js:
// app.js
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;
const taskRoutes = require('./routes/tasks');
// Middleware to parse JSON
app.use(express.json());
// Simple logging middleware
app.use((req, res, next) => {
console.log(`${new Date().toISOString()} - ${req.method} ${req.url}`);
next();
});
// Root route
app.get('/', (req, res) => {
res.send('Task Manager API - Use /api/tasks to access the API');
});
// Task routes
app.use('/api/tasks', taskRoutes);
// Error handling middleware
app.use((req, res, next) => {
res.status(404).json({ message: 'Route not found' });
});
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({ message: 'Internal server error' });
});
// Start server
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
module.exports = app;
Step 3: Create Routes Directory and Tasks Routes
mkdir routes
Create routes/tasks.js:
// routes/tasks.js
const express = require('express');
const router = express.Router();
const {
getAllTasks,
getTaskById,
createTask,
updateTask,
deleteTask
} = require('../controllers/taskController');
// GET all tasks
router.get('/', getAllTasks);
// GET single task
router.get('/:id', getTaskById);
// POST new task
router.post('/', createTask);
// PUT update task
router.put('/:id', updateTask);
// DELETE task
router.delete('/:id', deleteTask);
module.exports = router;
Step 4: Create Controllers Directory and Task Controller
mkdir controllers
Create controllers/taskController.js:
// controllers/taskController.js
const { v4: uuidv4 } = require('uuid');
// In-memory database
let tasks = [
{
id: '1',
title: 'Complete project proposal',
description: 'Write up the project proposal for the client',
status: 'pending',
created_at: '2025-05-01T10:30:00.000Z'
},
{
id: '2',
title: 'Buy groceries',
description: 'Milk, eggs, bread, and vegetables',
status: 'completed',
created_at: '2025-05-02T14:20:00.000Z'
}
];
// Get all tasks
exports.getAllTasks = (req, res) => {
// Support filtering by status
const { status } = req.query;
if (status) {
const filteredTasks = tasks.filter(task => task.status === status);
return res.json(filteredTasks);
}
res.json(tasks);
};
// Get task by ID
exports.getTaskById = (req, res) => {
const task = tasks.find(task => task.id === req.params.id);
if (!task) {
return res.status(404).json({ message: 'Task not found' });
}
res.json(task);
};
// Create new task
exports.createTask = (req, res) => {
const { title, description, status } = req.body;
// Validate input
if (!title) {
return res.status(400).json({ message: 'Title is required' });
}
const newTask = {
id: uuidv4(),
title,
description: description || '',
status: status || 'pending',
created_at: new Date().toISOString()
};
tasks.push(newTask);
res.status(201).json(newTask);
};
// Update task
exports.updateTask = (req, res) => {
const { title, description, status } = req.body;
const taskId = req.params.id;
// Find task index
const taskIndex = tasks.findIndex(task => task.id === taskId);
if (taskIndex === -1) {
return res.status(404).json({ message: 'Task not found' });
}
// Update task
const updatedTask = {
...tasks[taskIndex],
title: title || tasks[taskIndex].title,
description: description !== undefined ? description : tasks[taskIndex].description,
status: status || tasks[taskIndex].status,
updated_at: new Date().toISOString()
};
tasks[taskIndex] = updatedTask;
res.json(updatedTask);
};
// Delete task
exports.deleteTask = (req, res) => {
const taskId = req.params.id;
// Find task index
const taskIndex = tasks.findIndex(task => task.id === taskId);
if (taskIndex === -1) {
return res.status(404).json({ message: 'Task not found' });
}
// Remove task
tasks.splice(taskIndex, 1);
res.json({ message: 'Task deleted successfully' });
};
Step 5: Test Your API
- Start your server:
node app.js - Use a tool like Postman, Insomnia, or curl to test your API endpoints:
- GET all tasks:
GET http://localhost:3000/api/tasks - GET task by ID:
GET http://localhost:3000/api/tasks/1 - Create task:
POST http://localhost:3000/api/taskswith JSON body:{ "title": "Learn Express.js", "description": "Complete Express.js tutorial and exercises", "status": "in-progress" } - Update task:
PUT http://localhost:3000/api/tasks/1with JSON body:{ "status": "completed" } - Delete task:
DELETE http://localhost:3000/api/tasks/2
- GET all tasks:
Bonus Challenges
- Add Validation Middleware: Create a middleware that validates task data before it reaches the controller
- Implement Pagination: Add support for paginating tasks with
limitandpagequery parameters - Add Search Functionality: Allow searching tasks by title or description
- Create a Frontend: Build a simple HTML/CSS/JS frontend that consumes your API
- Persist Data: Replace the in-memory database with file-based storage using the fs module
Summary
- Express.js is a minimal and flexible Node.js web application framework
- Express simplifies the process of building web servers compared to vanilla Node.js
- Key features include routing, middleware, and enhanced request/response objects
- Routing allows you to handle different endpoints and HTTP methods
- Middleware functions process requests before they reach the route handlers
- Request (req) and Response (res) objects provide methods for handling HTTP interactions
- Real-world applications typically use a modular structure with separate files for routes, controllers, and middleware
- Express is highly extensible through its middleware ecosystem
- Best practices include proper error handling, validation, and security measures