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