What is Express.js?

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

Think of Express as a toolbox that simplifies building web servers. While you can build servers with raw Node.js (using the http module), Express makes it much easier - like using power tools instead of hand tools. It handles routing, request parsing, and much more with simple, clean code.

Why Use Express.js?

  • Simple and Minimal: Unopinionated framework, gives you freedom to structure your app
  • Fast Development: Less code to write, faster time to market
  • Robust Routing: Clean and powerful routing system
  • Middleware Ecosystem: Hundreds of plugins for common tasks
  • Industry Standard: Used by IBM, Uber, Accenture, Fox Sports
  • Great for APIs: Perfect for building RESTful APIs
  • Large Community: Extensive documentation, tutorials, and support

Express vs Other Frameworks

Express.js (Minimalist)
✓ Lightweight and fast
✓ Flexible, unopinionated
✓ Large ecosystem
✗ Need to choose libraries yourself
✗ More setup required

Nest.js (Full Framework)
✓ TypeScript-first
✓ Angular-like structure
✓ Everything included
✗ Steeper learning curve
✗ More opinionated

Fastify (Performance)
✓ Faster than Express
✓ Built-in schema validation
✓ Modern async/await
✗ Smaller ecosystem
✗ Less mature

Koa (Lightweight)
✓ By Express creators
✓ Modern async/await
✓ Smaller core
✗ Smaller community
✗ Need more middleware

When to choose Express:
- Building REST APIs
- Need flexibility and control
- Want largest ecosystem
- Quick prototyping/MVPs
- Learning backend development

Getting Started with Express

// Install Express
npm install express

// Basic Express server
const express = require('express');
const app = express();
const PORT = 3000;

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

// Start server
app.listen(PORT, () => {
    console.log(`Server running on http://localhost:${PORT}`);
});

// Compare with raw Node.js
// Express (clean and simple):
app.get('/', (req, res) => res.send('Hello'));

// Raw Node.js (verbose):
const server = http.createServer((req, res) => {
    if (req.url === '/' && req.method === 'GET') {
        res.writeHead(200, { 'Content-Type': 'text/plain' });
        res.end('Hello');
    }
});

Express saves you from manually:
- Parsing URLs
- Setting headers
- Handling different HTTP methods
- Managing complex routing

Routing: Handling Different URLs

Routing determines how your application responds to client requests to specific endpoints (URLs).

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

// GET request
app.get('/', (req, res) => {
    res.send('Home Page');
});

// POST request
app.post('/users', (req, res) => {
    res.send('Create new user');
});

// PUT request
app.put('/users/:id', (req, res) => {
    res.send(`Update user ${req.params.id}`);
});

// DELETE request
app.delete('/users/:id', (req, res) => {
    res.send(`Delete user ${req.params.id}`);
});

// Route parameters
app.get('/users/:id', (req, res) => {
    const userId = req.params.id;
    res.send(`User ID: ${userId}`);
});

// Multiple parameters
app.get('/posts/:postId/comments/:commentId', (req, res) => {
    const { postId, commentId } = req.params;
    res.json({ postId, commentId });
});

// Query parameters (?name=John&age=25)
app.get('/search', (req, res) => {
    const { name, age } = req.query;
    res.send(`Search: ${name}, ${age}`);
});

// Route handlers (multiple callbacks)
app.get('/example',
    (req, res, next) => {
        console.log('First handler');
        next(); // Pass to next handler
    },
    (req, res) => {
        res.send('Second handler');
    }
);

// Route chaining
app.route('/book')
    .get((req, res) => res.send('Get book'))
    .post((req, res) => res.send('Add book'))
    .put((req, res) => res.send('Update book'));

// Express Router (organize routes)
const router = express.Router();

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

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

app.use('/user', router);
// Routes: /user/profile, /user/settings

Middleware: The Express Pipeline

Middleware functions are functions that have access to the request and response objects. They can modify them, end the request, or pass control to the next middleware.

// Middleware flow:
Request → Middleware 1 → Middleware 2 → Route Handler → Response

// Application-level middleware (runs on every request)
app.use((req, res, next) => {
    console.log(`${req.method} ${req.url}`);
    next(); // Must call next() to continue
});

// Built-in middleware

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

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

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

// Third-party middleware

// CORS - allow cross-origin requests
const cors = require('cors');
app.use(cors());

// Morgan - logging
const morgan = require('morgan');
app.use(morgan('dev'));

// Helmet - security headers
const helmet = require('helmet');
app.use(helmet());

// Custom middleware
const logger = (req, res, next) => {
    console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
    next();
};
app.use(logger);

// Authentication middleware
const authenticate = (req, res, next) => {
    const token = req.headers.authorization;
    if (!token) {
        return res.status(401).json({ error: 'No token provided' });
    }
    // Verify token
    req.user = { id: 1, name: 'John' }; // Add user to request
    next();
};

// Apply to specific routes
app.get('/protected', authenticate, (req, res) => {
    res.json({ message: 'Protected data', user: req.user });
});

// Route-level middleware
const validateUser = (req, res, next) => {
    const { email } = req.body;
    if (!email) {
        return res.status(400).json({ error: 'Email required' });
    }
    next();
};

app.post('/register', validateUser, (req, res) => {
    res.send('User registered');
});

// Error-handling middleware (has 4 parameters)
app.use((err, req, res, next) => {
    console.error(err.stack);
    res.status(500).json({
        error: 'Something went wrong!',
        message: err.message
    });
});

Request and Response Objects

The request (req) and response (res) objects contain all information about the HTTP request and provide methods to send responses.

// REQUEST OBJECT (req)

app.get('/example', (req, res) => {
    // Route parameters
    req.params.id // /users/:id

    // Query string
    req.query.name // /search?name=John

    // Request body (needs express.json() middleware)
    req.body.email // POST/PUT data

    // Headers
    req.headers['content-type']
    req.get('authorization')

    // Cookies (needs cookie-parser middleware)
    req.cookies.sessionId

    // Request info
    req.method // GET, POST, PUT, DELETE
    req.url // /users/123
    req.path // /users/123
    req.hostname // localhost
    req.ip // Client IP address
    req.protocol // http or https
});

// RESPONSE OBJECT (res)

app.get('/example', (req, res) => {
    // Send text
    res.send('Hello World');

    // Send JSON
    res.json({ message: 'Success', data: users });

    // Set status code
    res.status(404).send('Not Found');
    res.status(201).json({ message: 'Created' });

    // Redirect
    res.redirect('/home');
    res.redirect(301, '/new-url'); // Permanent redirect

    // Set headers
    res.set('Content-Type', 'text/html');
    res.type('json');

    // Send file
    res.sendFile('/path/to/file.pdf');

    // Download file
    res.download('/path/to/report.pdf', 'report.pdf');

    // Render template (with view engine)
    res.render('index', { title: 'Home' });

    // End response without sending data
    res.end();

    // Chain methods
    res
        .status(200)
        .set('X-Custom-Header', 'value')
        .json({ success: true });
});

Building a REST API

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

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

// In-memory database (for demo)
let users = [
    { id: 1, name: 'Alice', email: 'alice@example.com' },
    { id: 2, name: 'Bob', email: 'bob@example.com' }
];

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

// GET 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).json({ error: 'User not found' });
    }
    res.json(user);
});

// CREATE new user
app.post('/api/users', (req, res) => {
    const { name, email } = req.body;

    // Validation
    if (!name || !email) {
        return res.status(400).json({ error: 'Name and email required' });
    }

    const newUser = {
        id: users.length + 1,
        name,
        email
    };

    users.push(newUser);
    res.status(201).json(newUser);
});

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

    const { name, email } = req.body;
    if (name) user.name = name;
    if (email) user.email = email;

    res.json(user);
});

// DELETE user
app.delete('/api/users/:id', (req, res) => {
    const index = users.findIndex(u => u.id === parseInt(req.params.id));
    if (index === -1) {
        return res.status(404).json({ error: 'User not found' });
    }

    users.splice(index, 1);
    res.status(204).send(); // No content
});

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

Error Handling

// Synchronous error handling
app.get('/sync-error', (req, res) => {
    throw new Error('Something went wrong!');
    // Express catches this automatically
});

// Asynchronous error handling
app.get('/async-error', async (req, res, next) => {
    try {
        const data = await fetchData();
        res.json(data);
    } catch (err) {
        next(err); // Pass error to error handler
    }
});

// Custom error class
class AppError extends Error {
    constructor(message, statusCode) {
        super(message);
        this.statusCode = statusCode;
        this.isOperational = true;
    }
}

// Using custom error
app.get('/user/:id', async (req, res, next) => {
    try {
        const user = await User.findById(req.params.id);
        if (!user) {
            throw new AppError('User not found', 404);
        }
        res.json(user);
    } catch (err) {
        next(err);
    }
});

// 404 handler (must be after all routes)
app.use((req, res, next) => {
    res.status(404).json({
        error: 'Route not found',
        path: req.url
    });
});

// Global error handler (must have 4 parameters)
app.use((err, req, res, next) => {
    console.error(err.stack);

    const statusCode = err.statusCode || 500;
    const message = err.message || 'Internal Server Error';

    res.status(statusCode).json({
        error: message,
        ...(process.env.NODE_ENV === 'development' && { stack: err.stack })
    });
});

Organizing Routes with Express Router

// routes/users.js
const express = require('express');
const router = express.Router();

router.get('/', (req, res) => {
    res.json({ message: 'Get all users' });
});

router.post('/', (req, res) => {
    res.json({ message: 'Create user' });
});

router.get('/:id', (req, res) => {
    res.json({ message: `Get user ${req.params.id}` });
});

module.exports = router;

// routes/posts.js
const express = require('express');
const router = express.Router();

router.get('/', (req, res) => {
    res.json({ message: 'Get all posts' });
});

router.post('/', (req, res) => {
    res.json({ message: 'Create post' });
});

module.exports = router;

// app.js (main file)
const express = require('express');
const app = express();

const userRoutes = require('./routes/users');
const postRoutes = require('./routes/posts');

app.use('/api/users', userRoutes);
app.use('/api/posts', postRoutes);

// Routes become:
// GET  /api/users
// POST /api/users
// GET  /api/users/:id
// GET  /api/posts
// POST /api/posts

app.listen(3000);

Best Practices

  • Use environment variables: Store config in .env files, never hardcode
  • Validate input: Always validate and sanitize user input
  • Handle errors properly: Use try-catch and error middleware
  • Use async/await: Cleaner than callbacks and promises
  • Organize routes: Use Express Router to keep code modular
  • Enable CORS: Use cors middleware for cross-origin requests
  • Use compression: Compress responses with compression middleware
  • Implement rate limiting: Protect against abuse with express-rate-limit
  • Use helmet: Security middleware for setting HTTP headers
  • Log requests: Use morgan for development, winston for production

When to Use Express.js

  • REST APIs: Building backend services for web/mobile apps
  • Microservices: Small, focused services
  • Real-time Apps: With Socket.io for WebSocket support
  • Server-Side Rendering: Serve React/Angular apps
  • Proxy Server: Forward requests to other services
  • Authentication Services: Login, signup, password reset
  • File Upload Services: Handle file uploads with multer

When NOT to use Express: CPU-intensive tasks (use worker threads or separate service), extremely high-performance needs (consider Fastify or Go).

Build Production-Ready APIs with Express

Our Full Stack JavaScript program covers Express.js from basics to advanced patterns. Build scalable REST APIs with security, authentication, and best practices.

Explore JavaScript Program

Related Articles