As developers, we all strive for efficient and robust codebases that are easy to understand, modify, and extend. By adopting best practices and exploring advanced techniques, we can unlock the true potential of NodeJS and significantly improve the quality of our applications. In this blog, we will focus on five advanced techniques of NodeJS. So, fasten your seatbelts and get ready to explore them.
1. Add Middleware
Instead of adding the middleware to each route, add it at the top of the route list using the use method. This way, any routes defined below the middleware will automatically go through the middleware before reaching their respective route handlers.
const route = express.Router();
const {login} = require("../controllers/auth");
route.get('/login', login)
// isAuthenticated is middleware that checks whether
// you are authenticated or not
// // ❌ Avoid this: middleware on each route
route.get('/products', isAuthenticated, fetchAllProducts);
route.get('/product/:id', isAuthenticated, getProductById)
// ✅ Instead, do this
// Route without middleware
route.get('/login', login)
// Middleware function: isAuthenticated
// This will be applied to all routes defined after this point
route.use(isAuthenticated);
// Routes that will automatically check the middleware
route.get('/products', fetchAllProducts);
route.get('/product/:id', getProductById);
This approach helps keep the code organized and avoids repeating the middleware for each route individually.
2. Use Global Error Handling
Instead of structuring the error response on every controller, we can use NodeJS Global error-handling feature. First, Create a custom AppError class derived from the built-in Error class. This custom class allows you to customize the error object with additional properties like statusCode and status.
// Custom Error class
module.exports = class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
this.status = statusCode < 500 ? "error" : "fail";
Error.captureStackTrace(this, this.constructor);
}
};
Once you have created a Custom Error class, add a global error handler middleware inside your root router file. This middleware function takes four parameters (err, req, res, next) and handles errors throughout the application. Inside the global error handler, you format the error response based on the error object’s statusCode, status, and message properties. You can customize this response format to suit your needs. Additionally, the stack property is included for development environments.
// Express setup
const express = require('express');
const app = express();
app.use('/', (req, res) => {
res.status(200).json({ message: "it works" });
});
app.use('*', (req, res) => {
res.status(404).json({
message: `Can't find ${req.originalUrl} this route`,
});
});
// 👇 add a global error handler after all the routes.
app.use((err, req, res, next) => {
err.status = err.status || "fail";
err.statusCode = err.statusCode || 500;
res.status(err.statusCode).json({
status: err.status,
message: transformMessage(err.message),
stack: process.env.NODE_ENV === "development" ? err.stack : undefined,
});
});
After you’ve added it, you can throw an error using next(new AppError(message, statusCode)). The next function automatically passes the error to the global error handler middleware.
// inside controllers
// route.get('/login', login);
exports.login = async (req, res, next) => {
try {
const { email, password } = req.body;
const user = await User.findOne({ email }).select("+password +lastLoginAt");
if (!user || !(await user.correctPassword(password, user.password))) {
// 👇 like this
return next(new AppError("Invalid Email / Password / Method", 404));
}
// Custom logic for generating a token
const token = 'generated_token';
res.status(200).json({ token });
} catch(error) {
next(error
}
});
Overall, this approach simplifies error handling by centralizing it in one place, making it easier to maintain and customize error responses across your application.
3. Use Custom Try-Catch Function
Instead of manually wrapping each controller function with a try-catch block, we can use a customized function that achieves the same purpose.
// ❌ Avoid this
// Using try-catch block each controllers
exports.login = async (req, res, next) => {
try {
// logic here
} catch(error) {
res.status(400).json({ message: 'You error message'}
}
});
The tryCatchFn function accepts a function (fn) as input and returns a new function that wraps the original function with a try-catch block. If an error occurs within the wrapped function, it is caught using the catch method, and the error is passed to the next function to be handled by the Global error handler.
// ✅ Instead, do this
const tryCatchFn = (fn) => {
return (req, res, next) => {
fn(req, res, next).catch(next);
};
}
// To use this custom function, you can wrap your controller
// functions with tryCatchFn:
exports.login = tryCatchFn(async (req, res, next) => {
// logic here
});
By wrapping your controller functions with tryCatchFn, you ensure that any errors thrown within those functions will be automatically caught and passed to the global error handler, eliminating the need to add try-catch blocks individually.
This approach helps to centralize error handling in a cleaner and more concise way, making your code more maintainable and reducing repetitive error handling code.
4. Separate the Main file into two parts.
When developing a NodeJS application using Express, it is common to have a main file that contains all the business logic, route definitions, and server setup. However, managing and maintaining a single file that handles everything can become difficult as the application grows.
One recommended technique to address this issue and keep the codebase cleaner and more organized is to separate the main file into two parts: one for routes and another for server setup or configuration. Here’s an example:
// app.js
const express = require('express');
const app = express();
/* Middlewares */
app.get('/', (req, res) => {
res.status(200).json({ message: "it works" });
})
app.use(/* Global Error Handler */);
module.exports = app;
// server.js
const app = require('./app');
const port = process.env.PORT || 5001;
app.listen(port, () => console.log('Server running at', port));
5. Separate routes from controllers
To achieve a more organized and modular codebase, I recommended separating routes from controllers. This practice helps to maintain a clear separation of concerns and improves code readability and maintainability. Here’s an example that demonstrates the separation of routes and controllers.
// ❌ Avoid this
const route = express.Router();
route.get('/login', tryCatchFn(req, res, next) => {
// logic here
}))
// ✅ Do this
const route = express.Router();
const {login} = require("../controllers/auth");
route.get('/login', login);
Conclusion
In this article, we have discussed different advanced techniques to write NodeJS code that is clean and easy to maintain. There are many best practices available that can significantly improve the quality of your application’s code. Feel free to explore these techniques and apply them to enhance your codebase.
I hope you find this article enjoyable.
Stay curious; keep coding!
Top comments (0)