In this tutorial, you’ll learn how to create custom middleware and implement proper error handling in your Express API. Middleware functions can be used for logging requests, validating input data, and catching errors. We’ll also explore how to handle common HTTP errors in a consistent manner.
What You Will Learn Today:
- Creating custom middleware for logging
- Adding validation middleware using
express-validator
- Implementing global error handling middleware
- Handling common HTTP errors (e.g., 404, 500)
- Testing middleware with Postman
Step 1: Creating a Logging Middleware
Let’s start by creating a middleware function to log incoming requests. This will help track which routes are being accessed and when.
Creating the Logger Middleware
- Inside your project directory, create a new folder called
middleware
(if you haven’t already), and create a file calledlogger.js
:
touch middleware/logger.js
- Open
middleware/logger.js
and add the following logging middleware:
const logger = (req, res, next) => {
console.log(`${req.method} ${req.url} - ${new Date().toISOString()}`);
next(); // Pass control to the next middleware or route handler
};
module.exports = logger;
Explanation:
req.method
: Logs the HTTP method (GET, POST, etc.).req.url
: Logs the URL being accessed.new Date().toISOString()
: Logs the current timestamp in ISO format.next()
: Calls the next middleware function in the stack.
Using the Logger Middleware
Now, let’s apply the logger middleware globally so it runs for all requests.
- Open
index.js
and add the logger middleware:
const express = require('express');
const mongoose = require('mongoose');
const logger = require('./middleware/logger');
const app = express();
const PORT = process.env.PORT || 5000;
// Middleware
app.use(logger); // Use the logger middleware for all routes
app.use(express.json()); // Parse incoming JSON data
// MongoDB connection
mongoose.connect('your_mongo_connection_string', { useNewUrlParser: true, useUnifiedTopology: true })
.then(() => console.log('MongoDB connected'))
.catch((err) => console.log('MongoDB connection error:', err));
// Routes
const userRoutes = require('./routes/users');
app.use('/api/users', userRoutes);
// Start the server
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
Explanation:
- We added the logger middleware globally using
app.use(logger)
. This means every request made to the server will be logged.
Step 2: Adding Validation Middleware
Data validation is an important part of building APIs, ensuring that the data sent by the client meets the expected format and constraints. We’ll use express-validator
to handle input validation.
Installing express-validator
- Install the
express-validator
package by running the following command:
npm install express-validator
Adding Validation to the User Registration Route
Let’s add validation to the user registration route to ensure that the name
, email
, and password
fields are provided and meet certain criteria.
- Open
routes/users.js
and modify the/register
route to include validation:
const express = require('express');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const { check, validationResult } = require('express-validator');
const router = express.Router();
const User = require('../models/User');
// Register a new user with validation
router.post(
'/register',
[
check('name', 'Name is required').not().isEmpty(),
check('email', 'Please include a valid email').isEmail(),
check('password', 'Password must be at least 6 characters long').isLength({ min: 6 }),
],
async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const { name, email, password } = req.body;
try {
const existingUser = await User.findOne({ email });
if (existingUser) {
return res.status(400).json({ message: 'User already exists' });
}
const hashedPassword = await bcrypt.hash(password, 10);
const newUser = new User({ name, email, password: hashedPassword });
const savedUser = await newUser.save();
res.status(201).json(savedUser);
} catch (error) {
res.status(500).json({ message: 'Error registering user', error });
}
}
);
Explanation:
check()
: This is used to define validation rules for specific fields. For example:name
must not be empty.email
must be a valid email address.password
must be at least 6 characters long.validationResult(req)
: This checks if any validation errors occurred. If there are errors, it returns a400 Bad Request
response with the error messages.
Step 3: Implementing Global Error Handling Middleware
To handle errors in a centralized manner, we’ll create a global error-handling middleware. This will catch any errors that occur during the execution of your route handlers and return consistent error messages to the client.
Creating an Error Handling Middleware
- Inside the
middleware
folder, create a new file callederrorHandler.js
:
touch middleware/errorHandler.js
- Open
middleware/errorHandler.js
and define the error-handling middleware:
const errorHandler = (err, req, res, next) => {
console.error(err.stack);
res.status(500).json({ message: 'Server error', error: err.message });
};
module.exports = errorHandler;
Explanation:
err.stack
: Logs the error stack trace to the console for debugging.res.status(500)
: Returns a500 Internal Server Error
response with a generic error message.
Using the Error Handler
- Open
index.js
and add the error-handling middleware at the bottom, after all routes:
const errorHandler = require('./middleware/errorHandler');
// Other middleware and routes here...
// Error handling middleware (should be the last middleware)
app.use(errorHandler);
Explanation:
- The error handler is added after all routes and middleware. This ensures that it catches any errors that occur during request processing.
Step 4: Handling Common HTTP Errors (404 and Others)
In addition to server errors, we should handle 404 Not Found errors when a user tries to access a non-existent route.
Handling 404 Errors
- Add a route to catch all undefined routes and respond with a 404 error:
// Catch-all route for 404 errors
app.use((req, res, next) => {
res.status(404).json({ message: 'Resource not found' });
});
Explanation:
- This route catches any requests to undefined routes and returns a
404 Not Found
response.
Step 5: Testing Middleware and Error Handling with Postman
Let’s test the middleware and error handling to ensure everything is working properly.
1. Testing the Logger Middleware
- Restart your server by running:
node index.js
- Open Postman and make a GET request to
http://localhost:5000/api/users
. In your terminal, you should see a log message similar to this:
GET /api/users - 2024-10-01T14:37:22.345Z
This confirms that the logger middleware is working.
2. Testing Validation Middleware
- Make a POST request to
http://localhost:5000/api/users/register
with the following invalid JSON body (no password):
{
"name": "John Doe",
"email": "[email protected]"
}
- You should receive a
400 Bad Request
response with the validation errors:
{
"errors": [
{ "msg": "Password must be at least 6 characters long", "param": "password", "location": "body" }
]
}
3. Testing Global Error Handling
- Make an invalid GET request to
http://localhost:5000/api/users/invalid_id
. - You should receive a
500 Internal Server Error
response with the generic error message.
4. Testing 404 Errors
- Make a GET request to a non-existent route, such as
http://localhost:5000/api/unknown
. - You should receive a
404 Not Found
response with the message:
{
"message": "Resource not found"
}
Step 6: Recap and Summary
In this tutorial, you learned how to implement custom middleware and error handling in your
Express API. Here’s a quick summary of what you’ve done:
- Created a logger middleware to log incoming requests.
- Added validation middleware using
express-validator
to validate user input. - Implemented global error handling middleware to catch and respond to errors consistently.
- Handled 404 errors for undefined routes.
- Tested the middleware and error handling using Postman.
With these improvements, your API is now more robust and handles errors gracefully.
Next Up: Day 6 – Securing Your API with HTTPS and Environment Variables
In Day 6, we’ll focus on securing your API by using HTTPS, managing environment variables, and implementing security best practices.
Stay tuned for more advanced features tomorrow!