March 20, 2024
Express.js - From Zero to Hero


Source code: https://github.com/sahil-172002/Express.js-Blog-1
Don't forget to star the repo if you find it helpful!
Live demo here
Introduction to Express.js
If you're already familiar with Express.js basics, you can skip to the advanced section
Express.js is a minimal and flexible Node.js web application framework that provides a robust set of features for web and mobile applications. It's the de facto standard server framework for Node.js, making it an essential tool for any backend developer.
In this comprehensive guide, we'll explore Express.js from the ground up, covering everything from basic routing to advanced concepts like authentication, middleware, and deployment.
Getting Started
Setting Up Your First Express Application
Basic Setup
Let's start by creating a simple Express application. First, initialize your project and install Express:
mkdir express-app
cd express-app
npm init -y
npm install express
Create your first Express server:
const express = require('express');
const app = express();
const port = 3000;
app.get('/', (req, res) => {
res.send('Hello World!');
});
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}`);
});
Understanding Routing
Basic Routing
Express provides a robust routing system. Here's how to handle different HTTP methods:
// GET request
app.get('/users', (req, res) => {
res.send('Get all users');
});
// POST request
app.post('/users', (req, res) => {
res.send('Create a new user');
});
// Dynamic routes with parameters
app.get('/users/:id', (req, res) => {
res.send(`Get user with ID: ${req.params.id}`);
});
Middleware
Understanding Middleware
Middleware functions are the backbone of Express.js. They have access to the request and response objects, and the next middleware function in the application's request-response cycle.
// Custom middleware example
const loggerMiddleware = (req, res, next) => {
console.log(`${req.method} ${req.url} - ${new Date()}`);
next();
};
app.use(loggerMiddleware);
// Built-in middleware
app.use(express.json()); // for parsing application/json
app.use(express.urlencoded({ extended: true })); // for parsing application/x-www-form-urlencoded
Advanced Concepts
Error Handling
Error Management
Proper error handling is crucial for any production application:
// Custom error handler middleware
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).send('Something broke!');
});
// Async error handling
app.get('/async', async (req, res, next) => {
try {
const data = await someAsyncOperation();
res.json(data);
} catch (error) {
next(error); // Pass errors to Express
}
});
Authentication and Authorization
Security Implementation
Implementing authentication using JSON Web Tokens (JWT):
const jwt = require('jsonwebtoken');
// Authentication middleware
const authenticateToken = (req, res, next) => {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) return res.sendStatus(401);
jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
if (err) return res.sendStatus(403);
req.user = user;
next();
});
};
// Protected route example
app.get('/protected', authenticateToken, (req, res) => {
res.json({ message: 'Protected data!', user: req.user });
});
Database Integration
Database Connection
Example using MongoDB with Mongoose:
const mongoose = require('mongoose');
mongoose.connect('mongodb://localhost/myapp', {
useNewUrlParser: true,
useUnifiedTopology: true
});
// User model
const User = mongoose.model('User', {
name: String,
email: String,
password: String
});
// CRUD operations
app.post('/users', async (req, res) => {
try {
const user = new User(req.body);
await user.save();
res.status(201).json(user);
} catch (error) {
res.status(400).json({ error: error.message });
}
});
API Rate Limiting
Rate Limiting
Implementing rate limiting to protect your API:
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100 // limit each IP to 100 requests per windowMs
});
app.use(limiter);
Best Practices
Project Structure
Organize your Express.js application using a modular structure:
project/
├── config/
│ └── database.js
├── controllers/
│ └── userController.js
├── middleware/
│ └── auth.js
├── models/
│ └── User.js
├── routes/
│ └── users.js
├── services/
│ └── emailService.js
└── app.js
Environment Configuration
Use environment variables for configuration:
require('dotenv').config();
const app = express();
const port = process.env.PORT || 3000;
const mongoUri = process.env.MONGODB_URI;
Security Best Practices
Implement essential security measures:
const helmet = require('helmet');
const cors = require('cors');
app.use(helmet()); // Adds various HTTP headers
app.use(cors()); // Enable CORS
// XSS Protection
app.use((req, res, next) => {
res.setHeader('X-XSS-Protection', '1; mode=block');
next();
});
Testing
API Testing
Example using Jest and Supertest:
const request = require('supertest');
const app = require('../app');
describe('User API', () => {
test('GET /users should return all users', async () => {
const response = await request(app)
.get('/users')
.expect(200);
expect(Array.isArray(response.body)).toBeTruthy();
});
});
Deployment
Production Deployment
Preparing your application for production:
if (process.env.NODE_ENV === 'production') {
// Production-specific middleware
app.use(compression());
// Error logging
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).send('Server Error');
});
}
Demo
Here's a complete Express.js REST API example combining all the concepts we've covered:
const express = require('express');
const mongoose = require('mongoose');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
const rateLimit = require('express-rate-limit');
const helmet = require('helmet');
const cors = require('cors');
const app = express();
// Middleware
app.use(express.json());
app.use(helmet());
app.use(cors());
// Rate limiting
const limiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 100
});
app.use(limiter);
// Database connection
mongoose.connect(process.env.MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true
});
// User model
const User = mongoose.model('User', {
username: String,
password: String,
email: String
});
// Authentication middleware
const authenticateToken = (req, res, next) => {
const token = req.headers['authorization']?.split(' ')[1];
if (!token) return res.sendStatus(401);
jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
if (err) return res.sendStatus(403);
req.user = user;
next();
});
};
// Routes
app.post('/register', async (req, res) => {
try {
const hashedPassword = await bcrypt.hash(req.body.password, 10);
const user = new User({
username: req.body.username,
password: hashedPassword,
email: req.body.email
});
await user.save();
res.status(201).json({ message: 'User created successfully' });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.post('/login', async (req, res) => {
try {
const user = await User.findOne({ username: req.body.username });
if (!user) return res.status(400).json({ error: 'User not found' });
const validPassword = await bcrypt.compare(req.body.password, user.password);
if (!validPassword) return res.status(400).json({ error: 'Invalid password' });
const token = jwt.sign({ id: user._id }, process.env.JWT_SECRET);
res.json({ token });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.get('/profile', authenticateToken, async (req, res) => {
try {
const user = await User.findById(req.user.id).select('-password');
res.json(user);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
const port = process.env.PORT || 3000;
app.listen(port, () => {
console.log(`Server running on port ${port}`);
});
Conclusion
Express.js is a powerful and flexible framework that can handle everything from simple APIs to complex web applications. By following the concepts and best practices covered in this guide, you'll be well-equipped to build robust and secure web applications.
Remember to:
- Always implement proper error handling
- Use middleware for cross-cutting concerns
- Follow security best practices
- Test your code thoroughly
- Structure your project properly
Happy coding! 🚀
If you have any questions, feel free to reach out to me on Twitter.