Express.js - Simplifying Backend Development


Express.js has revolutionized Node.js backend development by providing a streamlined framework that simplifies server creation, routing, and middleware implementation. In this comprehensive article, I'll cover key aspects of Express that make it the go-to choice for Node.js developers.
How to Create a Simple Web Server with Node.js and Express
Raw Node.js Server Implementation
Let's first look at how we create a basic web server using raw Node.js:
const http = require('http');
const server = http.createServer((req, res) => {
// Set response headers
res.writeHead(200, {'Content-Type': 'text/plain'});
// Handle different routes manually
if (req.url === '/') {
res.end('Home Page');
} else if (req.url === '/about') {
res.end('About Page');
} else {
res.writeHead(404, {'Content-Type': 'text/plain'});
res.end('Page Not Found');
}
});
// Start server on port 3000
const PORT = 3000;
server.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
This raw Node.js implementation requires manual handling of routes, status codes, and content types. As your application grows, this approach becomes unwieldy and difficult to maintain.
Express.js Server Implementation
Now, let's see how Express.js simplifies this:
const express = require('express');
const app = express();
// Define routes
app.get('/', (req, res) => {
res.send('Home Page');
});
app.get('/about', (req, res) => {
res.send('About Page');
});
// Catch-all route for 404s
app.use((req, res) => {
res.status(404).send('Page Not Found');
});
// Start server
const PORT = 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
The Express implementation is cleaner, more readable, and offers built-in methods for setting status codes and sending responses. You can see that Express provides an intuitive API for defining routes and handling HTTP methods.
Creating Routes and Handling Requests with Express
Express provides a sophisticated routing system that makes handling different HTTP methods and URL patterns simple and organized.
Basic Route Handling
const express = require('express');
const app = express();
// Basic route handling
app.get('/', (req, res) => {
res.send('GET request to homepage');
});
app.post('/', (req, res) => {
res.send('POST request to homepage');
});
app.put('/user', (req, res) => {
res.send('PUT request to /user');
});
app.delete('/user', (req, res) => {
res.send('DELETE request to /user');
});
// All HTTP methods
app.all('/secret', (req, res) => {
res.send('Accessing the secret section with any HTTP method');
});
Route Parameters
Express makes it easy to extract values from the URL:
// Route parameters
app.get('/users/:userId/books/:bookId', (req, res) => {
// Access parameters
const { userId, bookId } = req.params;
res.send(`Fetched book ${bookId} for user ${userId}`);
});
Route Handlers with Multiple Callback Functions
You can provide multiple callback functions for a single route:
// Multiple callback functions
app.get('/example',
(req, res, next) => {
console.log('First callback');
next(); // Pass control to the next handler
},
(req, res) => {
res.send('Second callback sends the response');
}
);
Route Modularity with Express Router
For larger applications, Express Router helps organize routes into modular units:
// users.js
const express = require('express');
const router = express.Router();
router.get('/', (req, res) => {
res.send('Get all users');
});
router.get('/:id', (req, res) => {
res.send(`Get user with ID ${req.params.id}`);
});
router.post('/', (req, res) => {
res.send('Create new user');
});
module.exports = router;
// main app.js
const express = require('express');
const userRoutes = require('./users');
const app = express();
app.use('/users', userRoutes);
app.listen(3000);
This modular approach keeps your code organized and maintainable as your application grows.
What is Middleware in Express and How to Use It
Middleware functions are the backbone of Express applications. They have access to the request object (req), the response object (res), and the next middleware function in the application's request-response cycle.
Middleware Functionality
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
Application-Level Middleware
const express = require('express');
const app = express();
// Logger middleware
app.use((req, res, next) => {
console.log(`${new Date().toISOString()} - ${req.method} ${req.url}`);
next(); // Pass control to the next middleware
});
// Route with middleware
app.get('/', (req, res) => {
res.send('Home page');
});
app.listen(3000);
Router-Level Middleware
const router = express.Router();
// Middleware that runs only for this router
router.use((req, res, next) => {
console.log('Router middleware');
next();
});
router.get('/special', (req, res) => {
res.send('Special route with middleware');
});
app.use('/admin', router);
Error-Handling Middleware
Error-handling middleware always takes four arguments:
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).send('Something broke!');
});
Built-in Middleware
Express provides several built-in middleware functions:
// Parse JSON bodies
app.use(express.json());
// Parse URL-encoded bodies
app.use(express.urlencoded({ extended: true }));
// Serve static files
app.use(express.static('public'));
Third-Party Middleware
You can also use third-party middleware to add functionality:
const morgan = require('morgan');
const cookieParser = require('cookie-parser');
// HTTP request logger
app.use(morgan('dev'));
// Parse cookies
app.use(cookieParser());
Middleware Chain Flow
The request-middleware-controller pattern in Express follows a flow where each request passes through a series of middleware functions before reaching the final route handler. This can be visualized as:
Client Request
↓
app.use(middleware1)
↓
app.use(middleware2)
↓
app.get('/path', controller)
↓
Response to Client
Middleware executes in the order it's defined, with each function calling next()
to pass control to the next middleware in the chain.
URL Parameters vs Query Strings
Express makes it easy to work with both URL parameters and query strings, but they serve different purposes.
URL Parameters
URL parameters are part of the route definition and are used for essential path variables:
// URL: /users/123
app.get('/users/:id', (req, res) => {
const userId = req.params.id; // "123"
res.send(`User ID: ${userId}`);
});
// URL: /books/javascript/chapters/5
app.get('/books/:category/chapters/:number', (req, res) => {
const { category, number } = req.params;
res.send(`Category: ${category}, Chapter: ${number}`);
});
Query Strings
Query strings are optional and typically used for filtering, sorting, or pagination:
// URL: /products?category=electronics&sort=price
app.get('/products', (req, res) => {
const category = req.query.category; // "electronics"
const sort = req.query.sort; // "price"
res.send(`Category: ${category}, Sort by: ${sort}`);
});
When to Use Each
URL Parameters:
Use for required values that identify a specific resource
Define the structure of your API
Make the route more semantic
Example:
/users/123
,/articles/nodejs-express
Query Strings:
Use for optional parameters
Filtering, sorting, pagination
Non-hierarchical data
Example:
/search?q=express&limit=10
Practical Example Combining Both
// URL: /api/posts/2023/javascript?sort=newest&limit=10
app.get('/api/posts/:year/:tag', (req, res) => {
// URL Parameters
const year = req.params.year; // "2023"
const tag = req.params.tag; // "javascript"
// Query Strings
const sort = req.query.sort || 'default'; // "newest"
const limit = parseInt(req.query.limit) || 20; // 10
res.send({
filters: { year, tag },
options: { sort, limit }
});
});
This demonstrates how parameters and query strings can work together to create flexible and powerful APIs.
Advanced Express Features and Best Practices
Nested Routes
Express allows you to create nested routes for hierarchical resources:
// blogs.js
const express = require('express');
const router = express.Router();
// Get all blogs
router.get('/', (req, res) => {
res.send('All blogs');
});
// Get a specific blog
router.get('/:blogId', (req, res) => {
res.send(`Blog ${req.params.blogId}`);
});
// Get comments for a specific blog
router.get('/:blogId/comments', (req, res) => {
res.send(`Comments for blog ${req.params.blogId}`);
});
// Get a specific comment for a specific blog
router.get('/:blogId/comments/:commentId', (req, res) => {
res.send(`Comment ${req.params.commentId} for blog ${req.params.blogId}`);
});
module.exports = router;
// app.js
app.use('/blogs', blogRouter);
This creates a tree structure of routes:
/blogs
├── /
├── /:blogId
├── /:blogId/comments
└── /:blogId/comments/:commentId
Controller Pattern
Separating route handling from business logic using controllers:
// userController.js
exports.getAllUsers = (req, res) => {
// Logic to fetch all users
res.send('All users');
};
exports.getUserById = (req, res) => {
// Logic to fetch specific user
res.send(`User ${req.params.id}`);
};
// routes/users.js
const express = require('express');
const router = express.Router();
const userController = require('../controllers/userController');
router.get('/', userController.getAllUsers);
router.get('/:id', userController.getUserById);
module.exports = router;
Error Handling
Proper error handling with custom error classes and middleware:
// Custom error class
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
this.status = `${statusCode}`.startsWith('4') ? 'fail' : 'error';
this.isOperational = true;
Error.captureStackTrace(this, this.constructor);
}
}
// Error handling middleware
const errorHandler = (err, req, res, next) => {
err.statusCode = err.statusCode || 500;
err.status = err.status || 'error';
res.status(err.statusCode).json({
status: err.status,
message: err.message,
stack: process.env.NODE_ENV === 'development' ? err.stack : undefined
});
};
// Using the error handler
app.all('*', (req, res, next) => {
next(new AppError(`Can't find ${req.originalUrl} on this server!`, 404));
});
app.use(errorHandler);
Conclusion
Express.js has become the standard framework for Node.js web applications because it strikes the perfect balance between simplicity and power. It provides just enough structure to make development efficient without being overly prescriptive.
Key takeaways:
Express dramatically simplifies server creation and route handling compared to raw Node.js
Middleware is the heart of Express applications, enabling clean separation of concerns
Express Router enables modular, organized code structures for larger applications
Understanding when to use URL parameters vs. query strings helps create intuitive APIs
By mastering these core concepts, you'll be well on your way to building robust, maintainable web applications with Express.js.
Subscribe to my newsletter
Read articles from Dikshant Koriwar directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Dikshant Koriwar
Dikshant Koriwar
Hi, I'm Dikshant – a passionate developer with a knack for transforming ideas into robust, scalable applications. I thrive on crafting clean, efficient code and solving challenging problems with innovative solutions. Whether I'm diving into the latest frameworks, optimizing performance, or contributing to open source, I'm constantly pushing the boundaries of what's possible with technology. On Hashnode, I share my journey, insights, and the occasional coding hack—all fueled by curiosity and a love for continuous learning. When I'm not immersed in code, you'll likely find me exploring new tech trends, tinkering with side projects, or simply enjoying a great cup of coffee. Let's connect, collaborate, and build something amazing—one line of code at a time!