
Project Structuring & Error Handling
Hey Devs!
As your backend applications grow from a single file to a more complex service, you will quickly find that keeping all your code in index.js
is not a long-term solution. A disorganized codebase is hard to debug, difficult to expand, and a challenge for teams to work on.
Think of it like building a house. You don't just dump all the bricks, cement, and pipes in one big pile. You have separate plans for the foundation, the plumbing, the electrical wiring, and the rooms. A good project structure is the blueprint for your code, and good error handling is the safety system that prevents the house from falling apart when something unexpected happens.
Today, we will learn how to create a professional project structure and implement a smart error handling system for our backend applications.
Table of Contents |
---|
Why a Good Structure is a Must |
A Scalable Project Structure in Detail |
How the Pieces Work Together: A Request's Journey |
Error Handling: From Basic to Professional |
Level 1: The Basic Error Catcher |
Level 2: The Smarter Error Manager |
Level 3: The Professional Error System |
Conclusion |
Why a Good Structure is a Must
When you start, putting everything in one file is easy. But imagine your project grows to have 20, 50, or 100 different API endpoints. How will you find anything?
A good structure, based on the principle of Separation of Concerns, helps you by:
- Making code easy to find: You know exactly where to look for routing logic, database logic, or request handling.
- Improving maintainability: Fixing a bug or adding a feature becomes simpler because the relevant code is isolated.
- Helping with teamwork: Multiple developers can work on different parts (like routes and controllers) without creating conflicts.
A Scalable Project Structure in Detail
Let's design a blueprint for our code that can handle growth. This structure is used in many professional projects.
/your-project
├── src/
│ ├── controllers/
│ │ └── user.controller.js
│ ├── models/
│ │ └── user.model.js
│ ├── routes/
│ │ └── user.routes.js
│ ├── services/
│ │ └── user.service.js
│ ├── middlewares/
│ │ └── errorHandler.js
│ └── utils/
│ └── helpers.js
├── .env
├── .gitignore
├── index.js
└── package.json
Let's use an analogy of a large office building to understand each part.
-
src/routes/
: The Reception Desk. This is the first point of contact. It defines all the URLs (endpoints) your application has, like/users
or/login
. When a request comes in, the router looks at the URL and directs the request to the correct department manager (the Controller). -
src/controllers/
: The Department Managers. A controller's job is to manage the request. It receives the request from the router, understands what needs to be done, but it doesn't do the hard work itself. Instead, it delegates the task to the specialists (the Services). Once the work is done, the controller takes the result and prepares a proper response to send back to the user. -
src/services/
: The Specialist Employees. This is where the main business logic happens. If the controller needs to get user data, it will ask the user service. The service will handle all the complex tasks, like talking to a database, performing calculations, or calling other APIs. Services do the actual "work". -
src/models/
: The File Blueprints. A model defines the structure of your data. For example, auser.model.js
would define that a user must have a name, an email, and a password. It's like a template that ensures all your data is consistent and follows the rules before it's saved. -
src/middlewares/
: The Security Guards. Middleware is code that runs between the request arriving and the response being sent. Just like a security guard who checks your ID before letting you enter a building, middleware can check if a user is logged in, log request details, or, as we will see, catch errors. -
src/utils/
: The Office Supply Room. This folder holds common tools and helper functions that can be used by any part of the application. Things like a function to format dates or, in our case, a custom error class. -
index.js
: The Building's Main Entrance. This file is the starting point of your entire application. It sets up the Express server, connects all the routes, and applies global middleware.
How the Pieces Work Together: A Request's Journey
Let's trace a request for a user's details through our structure:
- A
GET
request arrives athttp://yourapi.com/api/users/123
. - The main
index.js
file directs the/api/users
part of the URL to the router insrc/routes/user.routes.js
. - The user router sees the
/123
part and calls thegetUserById
function fromsrc/controllers/user.controller.js
. - The
getUserById
controller function doesn't search the database itself. It calls thefindUser
function fromsrc/services/user.service.js
, passing the ID123
. - The
findUser
service function performs the database query to find the user. - Once the user data is found, the service returns it to the controller.
- The controller formats a successful JSON response and sends it back to the client.
This organized flow keeps every part of your code clean and focused on a single responsibility.
Error Handling: From Basic to Professional
Now for the safety system. When something goes wrong, how do we handle it without crashing the server and without writing the same try...catch
code in every single file?
Let’s think of it like managing a company. You don't want every employee to handle customer complaints in their own way. That would be chaos. You want a standard procedure where all problems are reported to a central complaints manager who handles them professionally. In Express, this "manager" is an error-handling middleware.
Level 1: The Basic Error Catcher (For Beginners)
Let's start simple. Our goal is to catch any error that happens anywhere in our app and send a generic "Something went wrong" message, so our server doesn't crash.
In your main index.js
file, add this code at the very end, after all your routes.
// index.js
// ... all your other app.use() and route definitions go above this
// A simple route to test our error handler
app.get('/broken-page', (req, res, next) => {
// We create a new error and pass it to next()
const err = new Error('This page is intentionally broken.');
next(err);
});
// The Error Handling Middleware (Our Central Manager)
// It must have 4 arguments: err, req, res, next
app.use((err, req, res, next) => {
console.error(err.stack); // For the developer to see the detailed error
res.status(500).send('Oops! Something went wrong on our end.');
});
// ... your app.listen() goes here
How it works:
- When you use
next(err)
, Express skips all normal routes and looks for a middleware with four arguments. - Our middleware catches the
err
, logs its details for us (the developers), and sends a safe, user-friendly message. Your app stays alive!
Level 2: The Smarter Error Manager (For Intermediate Devs)
The basic handler is good, but it always sends a 500
status code, which means "Internal Server Error". What if the error was a "Not Found" (404
) or a "Bad Request" (400
)?
We can make our handler smarter by attaching a status code to our errors.
// In a controller or route file
app.get('/users/:id', (req, res, next) => {
const user = findUserById(req.params.id); // Assume this function returns null if not found
if (!user) {
const err = new Error('User with this ID was not found.');
err.statusCode = 404; // We add a custom property
return next(err);
}
res.json(user);
});
// Update your error handling middleware in index.js
app.use((err, req, res, next) => {
const statusCode = err.statusCode || 500;
const message = err.message || 'Internal Server Error';
console.error(`[ERROR] ${statusCode}: ${message}`);
res.status(statusCode).json({
success: false,
message: message,
});
});
Now our manager is more intelligent. It checks if the error has a specific statusCode
. If it does, it uses it. If not, it defaults to 500
. This gives you much more control over your API's responses.
Level 3: The Professional Error System (The Expert Way)
For large applications, creating errors like const err = new Error(...)
and then adding properties can become repetitive. The professional way is to create a standard blueprint for all our application's errors using a custom class.
-
Create the Blueprint (
ApiError.js
)In
src/utils/
, create a fileApiError.js
.// src/utils/ApiError.js class ApiError extends Error { constructor(statusCode, message = 'Something went wrong') { super(message); // Call the parent Error constructor this.statusCode = statusCode; this.success = false; // This helps in capturing a clean stack trace Error.captureStackTrace(this, this.constructor); } } module.exports = ApiError;
This class is a template. It makes creating structured errors very easy.
-
Use the Blueprint in Your Code
Now, throwing a detailed error is clean and simple.
// In a controller file const ApiError = require('../utils/ApiError'); // ... inside a function if (!user) { return next(new ApiError(404, 'User with this ID was not found.')); }
This is much cleaner and ensures all your errors have a consistent structure. Your
errorHandler
middleware from Level 2 will work perfectly with this, no changes needed!
Conclusion
You have now learned the blueprint for building professional, scalable, and robust backend applications. A well-organized project structure makes development faster and easier, while a centralized error handler makes your application stable and predictable.
Start applying this structure to your new projects, and even consider refactoring old ones. It is a fundamental skill that will set you apart as a developer and make your code a pleasure to work with.
Have fun coding with Express!
Enjoy the content here?
Sign up on our platform or join our WhatsApp channel here to get more hands-on guides like this, delivered regularly.
See you in the next blog. Until then, keep practicing and happy learning!
1 Reactions
0 Bookmarks