March 20, 2024

Express.js - From Zero to Hero

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.