DEV Community

Jordan Leal-Walker
Jordan Leal-Walker

Posted on

The Dog Exposed My Stacktrace! 😱 My Bespoke Solution to a Common API Security Issue

As someone who recently built a MERN stack backend, I was reminded of what every developer who has worked on the same already knows - catching and handling errors safely is a pain. This article seeks to cover the following:

  • ❗ Problems faced by developers implementing error handling in a MERN backend
  • πŸ”§ How other resources have addressed this problem
  • πŸ“¦ The lightweight NPM package I built to help address it
  • βš–οΈ The industry relevant ethical and technical issues relating to this problem
  • πŸ“ My planning process behind building the package and the tools chosen
  • 🌱 How this project helped me grow as a developer

If you've ever wished there was an easier, safer way to implement error handling middleware in your MERN backend, read on to see my answer to this common problem!

Contents

  1. The Problem
  2. Ethical Concerns
  3. Relevant Industry Trends
  4. How Has This Problem Already Been Addressed?
  5. My Solution
  6. Project Planning
  7. Skills, Technologies, Languages & Frameworks
  8. How This Project Improved My Skills
  9. What Could I Do Differently In The Future?
  10. Conclusion
  11. References

The Problem

Errors happen. That means when writing your routes in your API, you need to ensure they're handled gracefully, catching and responding to the error so the client gets a meaningful message rather than an unexplained crash. Most peoples first foray into catching and responding to an error in a route may have looked something like this:

// ❌ BAD PRACTICE - No centralized error handling, exposing stack to client
app.get("movies/:imdbId", async (request, response) => {
  try {
    /* Route logic */
  } catch (error) {
    console.log("Error occurred:", error); // ❌ Logging the entire error object to the console
    return response.status(500).json({
      success: false,
      message: "An error occurred while fetching movie",
      error, // ❌ Leaking the stack trace to the client!
    });
  }
});
Enter fullscreen mode Exit fullscreen mode

Now, hopefully to most of you it's immediately obvious what's wrong with this picture, and for those that it isn't, don't worry! We'll go through it:

  1. No Centralized Error Handling: Without a centralized error handler, each route either has to individually account for every expected error type, or return a generic, unhelpful response to the client
  2. Sending whole error object to the client: There is one particularly good reason to never send your full error object to the client - stack traces! Not only will this bury your response data in an avalanche of information, that information can contain information that attackers can use to help mount attacks (internal file paths, database query details, configuration values etc).1 Errors should be reformatted into safe, consistent responses before sending to the client.
  3. Logging the whole error object: Many developers think that logging full error objects server side is safe, after all these logs aren't exposed to the client right? Well, not according to most leading industry voices in security. For example, the OWASP 'Logging Cheat Sheet'2 explicitly states to remove, mask, sanitize or encrypt sensitive data from logs - and for good reason. If attackers ever gain access to logs, they can use the sensitive data within to expedite further attacks. This article by HackerOne3 is a useful resource for those wanting to better understand the security issues related to logging.

An example of a (sanitized for security) full error object, including stack trace from the above code:

Error occurred: ValidationError: Movie validation failed: title: "Path `title` is required."
    at /sanitized/path/controllers/MovieController.js:28:40
    at Layer.handle [as handle_request] (/sanitized/path/express/lib/router/layer.js:95:5)
    at next (/sanitized/path/express/lib/router/route.js:144:13)
    at /sanitized/path/utils/auth.js:45:22
Enter fullscreen mode Exit fullscreen mode

The solution to this problem is creating a centralized error handling middleware, something like this:

// utils/errorHandler.js
const errorHandler = (err, req, res, next) => {
  console.error("The following error occurred:", {
    name: err.name,
    code: err.code,
    message: err.message,
  });

  // Catch MongoDB validation errors
  if (err.name === "ValidationError") {
    return res.status(400).json({
      success: false,
      message: "Schema validation failed",
      // Map over each error to return array of error messages
      errors: Object.values(err.errors).map((e) => e.message),
    });
  }

  // Additional 'if' blocks to catch every type of expected error
};
Enter fullscreen mode Exit fullscreen mode

Allowing us to simplify our route from before (and all our other routes) to the following:

app.get("movies/:imdbId", async (request, response, next) => {
  try {
    /* Route logic */
  } catch (error) {
    return next(error); // Passes error to error-handling middleware
  }
});
Enter fullscreen mode Exit fullscreen mode

Problem solved right? Well, not quite. I provided a single example of an error catching if block in the errorHandler middleware, but in actuality you'll need alot more than one block. Setting up a robust error handling middleware involves doing the following:

  • Identify the errors (including third party library) errors your middleware needs to catch
  • Research common/expected error response structures
  • Understand the shape of each error being caught so it can be formatted consistently for the client (each library has its own error object structure)
  • Setup error logging
  • Change logging settings between environments (often we wish to see stack traces in development for debugging, but not in production)
  • Ensure we aren't catching errors that SHOULDN'T be caught
  • Writing tests to ensure middleware catches and formats errors as expected

And you have to set that up every time you make a new MERN backend! As you can see, there's alot of room for error. What if you add the error stack to your log during development and forget to turn it off for production? Or a slight difference in response format for one error causes an unexpected bug for the client?

Rather than dealing with this each time, I devised my own simple, lightweight package as an easy solution. But first, let's look at the ethical issues associated with this problem, how it relates to current industry trends, and approaches other people have made to address this.


Ethical Concerns

The ACM Code of Ethics4 is a globally recognized industry standard for ethics among software engineers. Section 2.9 states it is the responsibility of computing professionals to 'Design and implement systems that are robustly and usably secure.'

Another trusted ethics standard, IEEE-CS Code of Ethics5 1.03. states: "Approve software only if they have a well-founded belief that it is safe, meets specifications, passes appropriate tests, and does not diminish quality of life, diminish privacy or harm the environment. The ultimate effect of the work should be to the public good."

We discussed the potential security risks and problems associated with incorrect error handling above, but how relevant is it actually to our industry? Let's have a look at some of the latest industry data to see.

Salt's latest 'State of API Security Report 2025'.6 lists some troubling statistics:

  • 99% of surveyed organizations say they've encountered API security issues in the past year
  • 55% say they've slowed the rollout of an application due to API security concerns
  • 34% discovered they were exposing sensitive date through their API's (and that's only the ones that caught it!)
  • 54% of attacks fell under OWASP's 'API Security Top Ten - #5 Security Misconfiguration'7, the description of which includes: "Error handling reveals stack traces or other overly informative error messages to users."
  • 30% of organizations reported a 51-100% growth in the number of API's managed over the past year
  • 25% experienced growth of API's managed exceeding 100%

As you can see, not only is this exact issue extremely relevant in our industry currently, the explosive growth of API usage means it's becoming increasingly relevant over time. It is accordingly the ethical responsibility of any developer working with API's to have a safe, robust and reliable strategy for implementing error handling within their application.


Relevant Industry Trends

We already covered the explosive industry growth in API usage, by why does this article and my bespoke package focus on a MERN backend specifically? What other emerging trends effected my decisions in building this package?

MERN stack

Lets take a look at the results of StackOverflow's 20248 and 20259 Developer Survey to analyze current industry trends. For the following, we will be looking specifically at the responses of professional developers:

  • Javascript remains the most popular language, rising from 64.6% in 2024 to 68.8% in 2025
  • MongoDb is the second most popular NoSql database, being overtaken by Redis from its first place position in 2024
  • Node.js rose to become the most popular web framework/technology, from 40.7% usage in 2024 to 49.1% in 2025
  • Express rose from 7th place at 18.2% to 6th place at 20.3%

Stack Overflow Survey of programming languages
Javascript remains king of the programming languages

Looking at the Jetbrains' 2025 'State of the Developer Ecosystem'10 we can identify that 48% of functionality being implemented by developers is aimed at 'providing API's and services'.

The above data paints a clear picture - API creation is in high demand, and the usage of Mongoose/MongoDb, Express and Node.js is not only extremely popular - it's becoming increasingly popular over time. While data on the trends of those platforms being used together is difficult to find, this blog post11 claims "MERN stack developers are in high demand, with job postings on platforms like LinkedIn and Indeed showing a 20% year-over-year increase in 2025." (They did not provide a source for their claim).

So we can see that having robust error handling strategies for the base MERN stack backend is vital, but how about the common third party libraries used alongside them? As previously mentioned, different libraries throw different error shapes, so it's important to know what packages are commonly being used and how to handle them.

Popular MERN Packages

jsonwebtoken: Rising from 62 to 103 million weekly downloads over the last two years, the jsonwebtoken12 package remains the de-facto package for handling authentication in API's.

jsonwebtoken package monthly downloads over 2 years
Monthly downloads for the jsonwebtoken package over 2 years

zod: Exploding in popularity to 237 million monthly downloads from only 26 two years ago, it's no exaggeration to say this package13 is one of the industry relevant trends for MERN stack developers in 2025. A runtime validation library, zod validates your data before saving it to your database, allowing you to catch and validate bad data early. This doesn't replace mongoose, it works hand in hand with it, with mongoose still doing it's final validation upon saving to the database.

zod package monthly downloads over 2 years
Monthly downloads for the zod package over 2 years

Logging libraries (e.g. pino): Due to the limitations of the native console log, logging packages have become increasingly popular over time. One such example is the hyper-fast pino14 library, rising dramatically in weekly downloads over the last two years to jostle the reigning king winston for first place.

pino package monthly downloads over 2 years
Monthly downloads for the pino package over 2 years

The above data paints a clear picture - your error handling middleware will likely have to deal with errors from the jsonwebtoken and zod packages, and work with third party logging libraries to log errors.


How Has This Problem Already Been Addressed?

Lets recap what exactly it is we need to address. We need error handling middleware for a MERN api capable of the following:

  1. Gracefully handling errors from the core MERN backend libraries (Express, Mongoose/MongoDb)
  2. Gracefully handling errors from the most popular libraries used alongside this stack (jsonwebtoken, zod)
  3. Formatting of errors into consistent responses for the client regardless of error type
  4. Integration with third party logging tools
  5. Configuration options to decide whether to include stack traces in development for debugging, defaulted to not include for safety
  6. Ability to gracefully catch and respond to custom application errors
  7. Ability for user to pass additional custom error handlers to handle other library errors
  8. Catch-all error handler to ensure any unexpected errors are handled gracefully

Now lets look at the most popular npm packages that overlap our requirements:

Package Weekly Downloads Last Update Score
errorhandler15 ~1.6m 2018 2/8
strong-error-handler16 ~44,000 Nov 2025 5/8
express-error-toolkit17 90 Nov 2025 3.5/8
error-cure18 3 Sep 2025 4/8

errorhandler

An official package created by the Express.js team, this is designed to be used in development only for debugging. It will catch all errors and format a standardized response, but those responses are not meant for client integration and the package only handles errors when environment is set to 'development'.

Requirement Is it Addressed?
Specific formatting for core MERN API library errors ❌ Does not specifically format these errors
Specific formatting for jsonwebtoken & zod errors ❌ Does not specifically format these errors
Formats errors into consistent, client friendly response 🟑 Formats errors consistently, but not client friendly
Easy integration with third party logging tools ❌ No specific integration options with logging tools
Configurable, env based error & stack trace logging 🟑 Runs only in development, does not handle errors at all in production
Gracefully handles custom application errors 🟑 Will handle in development but not production
Accepts and integrates custom error handlers ❌ No integration options with custom error handlers
Catch-all error handler - no unhandled errors 🟑 Will catch all errors - but only in development

strong-error-handler

A solid option for many developer needs, this package is safe out of the box and highly configurable, with configuration options for response format, content type, logging, safe fields etc. However, it does not provide specific error formatting for our database and core third party library errors, nor easy addition/integration of custom error handlers. Excelling in configuration, this library has a relatively steep learning curve, and is accordingly less suitable for more basic/beginner friendly API's.

Requirement Is it Addressed?
Specific formatting for core MERN API library errors ❌ Does not specifically format these errors
Specific formatting for jsonwebtoken & zod errors ❌ Does not specifically format these errors
Formats errors into consistent, client friendly response βœ… Consistent and configurable responses
Easy integration with third party logging tools βœ… Readme shows example of how to integrate third party logger
Configurable, env based error & stack trace logging βœ… Defaults to exclude stack trace with configurable inclusion options
Gracefully handles custom application errors βœ… Handles status codes, headers etc.
Accepts and integrates custom error handlers ❌ No integration options with custom error handlers
Catch-all error handler - no unhandled errors βœ… Yes

express-error-toolkit

Another great option for error handling middleware, this package offers custom error classes, express middleware, an async error handler, a http error factory function for creating custom errors and a type guard for safe error type checks. Although this package has great documentation, it's still a steep learning curve and doesn't meet our requirements to specifically format our core expected MERN API errors, work with custom loggers, or accept additional custom error handlers.

Requirement Is it Addressed?
Specific formatting for core MERN API library errors ❌ Does not specifically format these errors
Specific formatting for jsonwebtoken & zod errors ❌ Does not specifically format these errors
Formats errors into consistent, client friendly response βœ… Consistent and configurable responses
Easy integration with third party logging tools ❌ No specific integration options with logging tools
Configurable, env based error & stack trace logging 🟑 Does not log errors in production but default logs stack trace in development
Gracefully handles custom application errors βœ… Exceptional custom error creation and handling
Accepts and integrates custom error handlers ❌ No integration options with custom error handlers
Catch-all error handler - no unhandled errors βœ… Yes

error-cure

This package comes closest to meeting all our requirements, and upon initial review made me question if I needed to create my own package at all. It's a great, lightweight error handling middleware package with consistent formatting, custom app errors, catch all safety and environment based security.

However, it still falls short in handling our specific database and third party library errors, requiring wrapping of those errors for specific formatting. There is no obvious integration option for additional custom error handling, or integration with logging tools. Perhaps most importantly, although it uses environment variables to configure error and stack trace logging, these options are only configurable through setting your environment variables, and development still logs stack traces by default.

Requirement Is it Addressed?
Specific formatting for core MERN API library errors ❌ Requires manual mapping of database errors
Specific formatting for jsonwebtoken & zod errors ❌ Does not specifically format these errors
Formats errors into consistent, client friendly response βœ… Consistent and client friendly responses
Easy integration with third party logging tools 🟑 Not specifically, but does output error file depending on configuration
Configurable, env based error & stack trace logging 🟑 Does not log errors in production but default logs stack trace in development
Gracefully handles custom application errors βœ… Exceptional custom error creation and handling
Accepts and integrates custom error handlers ❌ No integration options with custom error handlers
Catch-all error handler - no unhandled errors βœ… Yes

My Solution

Enter express-mongo-error-handler, my NPM error handling middleware package19, created by me to have a package that addressed every need previously mentioned (GitHub repository here20). This lightweight, easy to use package is compatible with ESM, CommonJs and TypeScript, contains robust and easy to understand documentation, and is ready to use straight out of the box as your error handling middleware in a MERN stack API.

Lets rehash our previously described problems and desired solutions, and show exactly how this package addresses each of them.

1. Gracefully handling errors from the core MERN backend libraries (Express, Mongoose/MongoDb)

This middleware handles core MERN API errors by default, with extremely simple setup.
Setup with default options (logs errors to console.error in development/test only, excludes stack traces):

import express from "express";
import createErrorHandler from "express-mongo-error-handler"; // ESM
// OR
const createErrorHandler = require("express-mongo-error-handler"); // CJS

const app = express();

// Your routes here
app.get("/api/example", (req, res) => {
  /* Route logic */
});

// ADD LAST! After all existing middleware and routes.
const errorHandler = createErrorHandler();
app.use(errorHandler);
Enter fullscreen mode Exit fullscreen mode

And lets have a brief look under the hood of the errorHandler middleware:

// Accepts options parameter for configuration
const createErrorHandler = (options = {}) => {
  // Check for development or test environment (false without environment variables for safety)
  const notProduction = process.env.NODE_ENV === "development" || process.env.NODE_ENV === "test";

  // Set default options with destructuring for configuration
  const {
    logErrors = notProduction,
    exposeStack = false,
    logger = console.error,
    customHandlers = [],
  } = options;

  // Return configured middleware function
  return (err, req, res, next) => {
    /*
    Logic to handle error logging (shown later)
    */

    /*
    Logic to handle custom error handlers (shown later)
    */

    /* Catch SyntaxError from invalid JSON caught by JSON parsing middleware. Check for 400 and 'body'
    in error so we don't catch other SyntaxErrors by mistake */
    if (err instanceof SyntaxError && err.status === 400 && "body" in err) {
      return res.status(400).json({
        success: false,
        message: "Invalid JSON payload in request",
        errors: ["The request body JSON is invalid and could not be parsed"],
      });
    }

    // Request body data is too large (default limit is 100kb)
    if (err.type === "entity.too.large") {
      return res.status(413).json({
        success: false,
        message: "JSON payload too large",
        errors: ["The request body data exceeds the maximum size limit"],
      });
    }

    /*
    Additional `if` blocks catch Express, Mongoose/MongoDb, JWT, Zod, Custom App
    errors and unhandled/unexpected errors
    */
};
Enter fullscreen mode Exit fullscreen mode

Refer to the index.js file in the repository, or 'Handled Error Types' section in the package readme file for a file picture of every error handled by this middleware. As you can see, the middleware handles the full expected range of errors we would want to explicitly catch and format from our core MERN stack API libraries.

2. Gracefully handling errors from the most popular libraries used alongside this stack (jsonwebtoken, zod)

In the above code block, I provided an example of if blocks for express error types. Here is an example of an if block to handle a jsonwebtoken error:

if (err.name === "TokenExpiredError") {
  return res.status(401).json({
    success: false,
    message: "Expired token",
    errors: ["Your session has expired. Please log in again to refresh."],
  });
}
Enter fullscreen mode Exit fullscreen mode

This is just one example to demonstrate specific handling of core third party library errors within this middleware, refer to the documentation to see the remaining.
As seen from just three examples, each if block has had to deal with a different error shape, requiring different checks for err instanceOf, err.status, err.type and err.name depending on the error being handled, demonstrating the importance of having specific error handling capabilities to deal with the multitude of error types returned from different libraries.

3. Formatting of errors into consistent responses for the client regardless of error type

Regardless of the shape of the error being caught, the middleware will always return a consistent response structure, allowing for simple and easy integration with a front end.
Lets use the above example to break down the response structure:

{
  // Appropriate http error status code always included
  res.status(401).json({
    success: false, // Success always set to false for errors, allowing for easy client boolean check
    message: "Expired token", // Message contains simple summary of error
    // Errors array contains one or more verbose error messages from original error object
    errors: ["Your session has expired. Please log in again to refresh."],
  });
}
Enter fullscreen mode Exit fullscreen mode

Certain mongoose errors will return an array of objects in their errors array. This allows easy mapping for the client of the specific field that triggered the error (e.g. an email field causing a validation error):

{
  "success": false,
  "message": "Duplicate key violation",
  "errors": [
    { "field": "email", "message": "Record with field 'email' already exists" },
    { "field": "username", "message": "Record with field 'username' already exists" }
  ]
}
Enter fullscreen mode Exit fullscreen mode

4. Integration with third party logging tools

By default, when logging is enabled the middleware will log an object to console.error that includes the error name, code, message, and optional stack trace. For those wishing to use external logging libraries, integration is extremely simple.
Here is an example of configuring the middleware using the winston logging package:

import winston from "winston";

const logger = winston.createLogger({
  /* config */
});

// Assign your logger to the `logger` argument
const errorHandler = createErrorHandler({ logger: logger.error.bind(logger) });
app.use(errorHandler);
Enter fullscreen mode Exit fullscreen mode

It's that easy! Refer to the Configuration Options section of the documentation to see additional examples for pino and bunyan.

5. Configuration options to decide whether to include stack traces in development for debugging, defaulted to not include for safety

Let's have a look at the createErrorHandler configuration options:

// Check for development or test environment (false without environment variables for safety)
const notProduction = process.env.NODE_ENV === "development" || process.env.NODE_ENV === "test";

// Destructured createErrorHandler configuration options with defaults
const {
  logErrors = notProduction,
  exposeStack = false,
  logger = console.error,
  customHandlers = [],
} = options;
Enter fullscreen mode Exit fullscreen mode

As seen in the above, logErrors is set to notProduction, meaning by default this middleware will only log errors when NODE_ENV environment variable exists and is set to "development" or "test".
More importantly, exposeStack is set to false by default, an option seen missing in most other error handling middleware packages previously discussed. Of course each of these options can be configured as desired when initializing the middleware. For example, if a user wished to always log errors, and exclude stack traces only in production:

const errorHandler = createErrorHandler({
  logErrors: true,
  exposeStack: process.env.NODE_ENV !== "production", // Equates to false in production only
});
app.use(errorHandler);
Enter fullscreen mode Exit fullscreen mode

6. Ability to gracefully catch and respond to custom application errors

Many users will want to create their own custom error classes, or throw specific custom errors in certain routes. Since normal error objects don't have a statusCode property, so long as any custom application error is thrown with a statusCode, it can be caught with the following:

if (err.statusCode) {
  return res.status(err.statusCode).json({
    success: false,
    message: err.message,
    errors: err.errors || [err.message],
  });
}
Enter fullscreen mode Exit fullscreen mode

Here's an example of a custom error class being created and used throw an error in a route:

// Creation of custom error class. Must contain statusCode property to be handled appropriately
class CustomError extends Error {
  constructor(message, statusCode, errors = []) {
    super(message);
    this.statusCode = statusCode;
    this.errors = errors.length ? errors : [message];
  }
}

// Example usage in routes
app.get("/api/users/:id", async (req, res, next) => {
  try {
    const user = await User.findById(req.params.id);
    if (!user) {
      throw new CustomError("User not found", 404);
    }
    res.json(user);
  } catch (error) {
    next(error);
  }
});
Enter fullscreen mode Exit fullscreen mode

Since it contains a statusCode, this error will be handled by our middleware.

7. Ability for user to pass additional custom error handlers to handle other library errors

Our middleware package responds to the core MERN backend errors, and some of the most popular libraries, but how about handling of other third party library errors that aren't accounted for? For example, a user with the stripe or multer package installed will need to account for the unique type of errors thrown by those libraries if they wish to return a more meaningful response than the generic 500 error.

Thankfully our middleware makes this incredibly easy by accepting an array of custom error handlers as a configuration option. Take a look at the following code:

// Array of custom error handler functions for adding additional package compatibility
const customHandlers = [
  (err, req, res) => {
    // Example 'stripe' package error handler
    if (err.type === "StripeCardError") {
      return res.status(402).json({
        success: false,
        message: "Payment failed",
        errors: [err.message],
      });
    }
  },
  // Each 'if' block can be a separate function or wrapped in a single function as desired
  (err, req, res) => {
    // Example 'multer' package error handler
    if (err.name === "MulterError") {
      return res.status(400).json({
        success: false,
        message: "File upload error",
        errors: [err.message],
      });
    }
  },
  /* Additional error handlers as required */
];
const errorHandler = createErrorHandler({ customHandlers }); // Pass handlers to config
app.use(errorHandler);
Enter fullscreen mode Exit fullscreen mode

In this example, the user has created the customHandlers array, containing an error handling function for a stripe error and multer error respectively. This array is then passed to createErrorHandler as the shorthand customHandlers parameter (we can use shorthand as the name of the array matches the name of the config parameter). Custom error handlers are checked against the error object before the default error handlers, using the following code:

// Run error through any custom error handlers first
for (const handler of customHandlers) {
  const result = handler(err, req, res);
  if (result) return result; // Will return response and exit if custom handler catches error
}
Enter fullscreen mode Exit fullscreen mode

The customHandlers functionality will work regardless of if it's passed an array of separate functions like the above example, an array with a single function containing multiple if blocks, or a combination of either. Since the errors are handled within the createErrorHandler middleware function, the same configuration options are applied to errors handled by your customHandlers array.

8. Catch-all error handler to ensure any unexpected errors are handled gracefully

A must have for any error handling middleware, the following code catches any unhandled error to ensure we never crash the application with uncaught errors:

return res.status(500).json({
  success: false,
  message: "Unexpected error.",
  errors: ["An unexpected error occurred. Please try again later."],
});
Enter fullscreen mode Exit fullscreen mode

This is defined last within the function, and will only trigger if none of the preceding if blocks are triggered.

9. JSDoc tooltips for ease of use

JSDoc tooltips where added to the package to ensure the user could quickly identify key characteristics of the middleware function without having to refer to the documentation every time. Here is what the tooltips look like in the file:

/**
 * express-mongo-error-handler
 * ==============================
 * Create a configurable error-handling middleware for use with Express.js, designed for the
 * backend of MERN applications.
 *
 * Why use this middleware:
 * - Provides default error handling for common express API setups (Express, Mongoose, JWT, Zod)
 * - Prevents leaking of stack traces and sensitive data to clients in production
 * - Consistent response format structure ensures easy front end integration
 * - Works with custom errors, http-errors package, Zod, etc.
 * - Configurable logging and stack trace exposure with environment based defaults
 * - Uses console.error by default but allows custom logger function
 * - Accepts array of custom error handlers for additional flexibility
 *
 * Installation: npm install express-mongo-error-handler
 *
 * @param {Object} options - Configuration options object
 * @param {boolean} [options.logErrors=notProduction] - Option to log error details to logger
 * @param {boolean} [options.exposeStack=false] - Option to expose stack traces in error logging
 * @param {Function} [options.logger=console.error] - Accepts custom logger function for error logging
 * @param {Array<Function>} [options.customHandlers=[]] - Array of custom error handler functions
 * @returns {Function} Express error-handling middleware (err, req, res, next)
 */
Enter fullscreen mode Exit fullscreen mode

And here is how that displays to a user when hovering on the function:

Image of JSDoc on hover tooltip while hovering 'createErrorHandler' function
Scrollable JSDoc tooltip appears when hovering 'createErrorHandler' function


Project Planning

As most experienced developers will tell you, planning for a project is key to it's success. This is particularly true when tackling something new, which for me was creating and publishing an NPM project.
Let's walk through the planning steps I undertook to ensure this project would be successful, and the time estimates for each step.

1. Identify Project Requirements

Estimate - 2.5hrs

We've already covered in detail the project requirements in the 'The Problem' section, however it is important to note that this article is referring to version 1.2.0 (the latest version as of writing this blog). This version is the fourth release of my package, and has additional features added compared to the initial release. The reason for this, is that project planning should ideally identify two sets of requirements - one set for the ideal 'full-feature' version, and another for the 'MVP' (Minimum Viable Product) version.

There is no definitive 'correct' way to plan and deploy an application, but for my project I chose to use 'Iterative Development', i.e create and deploy my MVP product and then add additional desired features afterwards. Planning and identifying my MVP requirements was centred around the following:

  • What are the ideal features of the project? Since the current version of my package has integration of all of the initial ideal features, refer to the features section of the readme to view the initially planned ideal features.
  • What is the core functionality of the project?: The core functionality for this package is to handle Express, Mongoose/MongoDb, zod, jsonwebtoken and unexpected errors in a centralized middleware, with stack traces not included in logging by default, with the option to include stack traces in logs for debugging.
  • What is required for the project to function?: Rather than listing each feature kept for the MVP, let's list the features that were deemed unnecessary for our MVP version -
    • CommonJS, MJS and TypeScript support. Creating a bundled version of the package with support for all three required a decent amount of extra configuration and research, so initially the package was rolled out with MJS support only.
    • Integration with custom error handlers. Since this was technically doable for the user without the addition of a specific feature to make it simple, this was removed from MVP requirements
    • JSdoc tooltips. Not required for user functionality but a great addition to improve user experience, this was left out of MVP requirements.

2. Research Existing Resources

Estimate - 3hrs

Now that I had identified the key features required, before potentially wasting further time on planning this project the most vital next step was to research if any packages had already addressed my needs. There are few things more disappointing to a developer than investing time and energy on developing a feature only to discover there was a package that already did it for you. After identifying which keywords corresponded to the core package functionality, I searched through the NPM repository and other online resources to find the packages that most closely aligned with my core desired features, listed in the 'How Has This Problem Already Been Addressed?' section.

3. Choose Language/Tools/Frameworks etc

Estimate - 2hrs

This will be expanded in greater detail in the 'Skills, Technologies, Languages & Frameworks' section, but to summarize:

  • Vanilla Javascript for the MVP build with TypeScript files added in a successive feature
  • Node.js for runtime environment, middleware execution and NPM ecosystem
  • Express for the error middleware pattern (error, request, response, next)
  • MongoDB/Mongoose, jsonwebtoken, and zod, not used directly but researched to be able to handle their error objects
  • Git & GitHub for version control
  • NPM for package management and uploading/updating of package upon completion
  • Tsup for bundling later version of package
  • Jest for testing

4. Plan Project Structure

Estimate - 30m

Planning the project structure was broken into two phases, the MVP structure and the structure for successive iterations. After identifying the core features required for the MVP version and the languages/frameworks etc. listed above, the next step was to identify what specific files where required and organize them as desired:

β”œβ”€β”€ express-mongo-error-handler (MVP Version)
β”‚   β”œβ”€β”€ .git  # Folder containing git version control files
β”‚   β”‚   └── ...
β”‚   β”œβ”€β”€ .gitignore  # Define files to be ignored by git
β”‚   β”œβ”€β”€ .npmignore  # Define files to be ignored when uploading to NPM
β”‚   β”œβ”€β”€ CONTRIBUTING.md  # Guidelines on open source contribution
β”‚   β”œβ”€β”€ LICENSE  # Open source MIT licence
β”‚   β”œβ”€β”€ README.md  # Main project documentation
β”‚   β”œβ”€β”€ SECURITY.md  # Security policy
β”‚   β”œβ”€β”€ index.js  # Entry point of application with core package functionality
β”‚   β”œβ”€β”€ node_modules  # Folder containing NPM packages
|   |  └── ...
β”‚   β”œβ”€β”€ package-lock.json  # Detailed record of dependency tree
β”‚   β”œβ”€β”€ package.json  # Metadata and configuration settings for project
β”‚   └──test  # Test folder
β”‚       └── createErrorHandler.test.js  # Test file for core package method
Enter fullscreen mode Exit fullscreen mode

After defining the required structure for the MVP version, the structure for the full feature version was planned:

β”œβ”€β”€ express-mongo-error-handler (Current Version)
|   β”œβ”€β”€ <previously shown folders/files>
β”‚   β”œβ”€β”€ dist  # Folder containing compiled versions of project assets
β”‚   β”‚   β”œβ”€β”€ index.cjs  # CommonJS compatible compiled main application file
β”‚   β”‚   └── index.js  # Module (MJS) compatible compiled main application file
β”‚   β”œβ”€β”€ index.d.cts  # TypeScript declaration file for CommonJS
β”‚   β”œβ”€β”€ index.d.ts  # TypeScript declaration file for ESM
β”‚   └── tsup.config.js  # Configuration file for tsup bundler
Enter fullscreen mode Exit fullscreen mode

5. Plan Tests

Estimate - 1.5hrs

In line with principles of TDD, planning and writing tests before writing code gives a clear picture of what specific functionality the code needs to provide to pass the tests. For the MVP version of the project, the following tests where planned:

  • Configuration Tests: Unit tests to assert that the configuration options object to be passed to the middleware function changes its functionality as desired
  • Express Error Handling Tests: Unit tests to assert express errors are caught and responded to as desired
  • Mongoose/MongoDB Error Handling Tests: Unit tests to assert Mongoose/MongoDB errors are caught and responded to as desired
  • Jsonwebtoken Error Handling Tests: Unit tests to assert jsonwebtoken errors are caught and responded to as desired
  • Zod Error Handling Tests: Unit tests to assert zod errors are caught and responded to as desired
  • Custom Application Error Handling Tests: Unit tests to assert custom application errors are caught and responded to as desired
  • Catch-All Error Handling Tests: Unit tests to assert unexpected/unhandled errors are caught and responded to as desired

After completion of the MVP version, unit tests to assert custom error handlers array configuration caught and handled errors correctly was planned and added.


Skills, Technologies, Languages & Frameworks

We mentioned in the 'Project Planning' section which tools we decided to use in the project, but what went into that decision making process? Lets review each tool used, why it was required, the skills needed to use it, what the alternatives where (if any) and why it was chosen:

1. JavaScript

  • Why It Was Required: The package was built to target a specific issue MERN issue, which is built on JavaScript
  • Skills Needed: Basic JS competency as well as understanding asynchronous programming, object destructuring, stack trace debugging & error propagation
  • Alternative Options: TypeScript or any language that compiles to JavaScript
  • Why This Was Chosen: JS is the most popular programming language, and although TS is growing in popularity and offers many distinct advantages, as a junior developer I am not yet at a stage of competency to use TS for this project.

2. Node.js

  • Why It Was Required: Core ecosystem used by MERN which this package targets, enables usage of NPM ecosystem
  • Skills Needed: asynchronous programming, object destructuring, stack trace debugging & error propagation, NPM package management and creation
  • Alternative Options: None if targetting MERN users (the 'N' is for Node)
  • Why This Was Chosen: No alternatives

3. Express

  • Why It Was Required: Core framework used by MERN which this package targets. Not used as a package but as a target of the project
  • Skills Needed: Middleware usage, request and response structure, industry preferences for responses, express error structure
  • Alternative Options: None if targetting MERN users (the 'E' is for Express)
  • Why This Was Chosen: No alternatives

4. MongoDB/Mongoose

  • Why It Was Required: Core database and db manager used by MERN which this package targets. Not used as a package but as a target of the project
  • Skills Needed: Mongo model and schema usage, MongoDB and Mongoose error structure, async db queries
  • Alternative Options: None for MongoDb if targetting MERN users (the 'M' is for MongoDB), Camo, Prisma or direct MongoDB interaction as an alternative to Mongoose
  • Why This Was Chosen: No alternative for MongoDB, Mongoose is by far the most popular ODM in MERN packages making it the most suitable to target.

5. Jsonwebtoken

  • Why It Was Required: To meet one of the key purposes of this package, which was to target core MERN libraries and the most used third party packages. Not used as a package but as a target of the project
  • Skills Needed: Understanding JWT authentication usage, jsonwebtoken error structure
  • Alternative Options: jose, passport, paseto
  • Why This Was Chosen: This package remains the most popular JWT authentication package, making it the most suitable to target.

6. Zod

  • Why It Was Required: To meet one of the key purposes of this package, which was to target core MERN libraries and the most used third party packages. Not used as a package but as a target of the project
  • Skills Needed: Understanding zod schema usage and validation, zod error structure
  • Alternative Options: ajv, joi, yup
  • Why This Was Chosen: Although ajv is currently more popular than zod, adoption of zod has seen a dramatic explosion of growth, leading me to choose this as a more forward thinking package to target. Additionally, ajv has a much steeper learning curve than zod and targets more advanced application users. This project targets more junior/beginner developers, who are more likely to be using zod by comparison.

7. Jest

  • Why It Was Required: Unit testing the application
  • Skills Needed: Writing test suites, mocking API objects, async testing
  • Alternative Options: Mocha, Postman, AVA
  • Why This Was Chosen: As only relatively basic unit tests where required for this project, and I was already familiar and comfortable with this package, there was no comparative advantage to having to learn a new testing package.

8. NPM

  • Why It Was Required: Hosting platform for this package, dependency manager for Node.js
  • Skills Needed: Dependency installations commands, configuring package.json for publishing, package versioning, package publishing commands
  • Alternative Options: yarn, pnpm
  • Why This Was Chosen: Most popular package manager for backends, familiar usage

9. Tsup

  • Why It Was Required: To build/bundle the package for distribution on NPM, allowing for compatibility with CJS and ESM usage
  • Skills Needed: Setting up tsup configuration files, understanding outputs required for different compatibility options
  • Alternative Options: rollup, tsdown, esbuild
  • Why This Was Chosen: tsup is a wrapper of esbuild with easy config and good default config options, ideal for a small package like this. Tsdown was a suitable alternative but is yet to reach stable release.

10. TypeScript

  • Why It Was Required: Adding TS support for type checking and intelliSense, and to enable JSdoc function comments for both import styles
  • Skills Needed: tsconfig usage with TS, JSdoc syntax, basic TS requirements for type checking
  • Alternative Options: Using tsup to automatically generate TS files from index.js
  • Why This Was Chosen: Using tsup automatic configuration was not getting the JSdoc function comments to display correctly when using CJS imports

11. Git/Github

  • Why It Was Required: Version control for hosting and managing code, centralized platform to allow open source contribution
  • Skills Needed: Basic git usage (branching, commits, etc.), conventional commit comments, requirements for open source documentation, tags for NPM packaging
  • Alternative Options: SVN, GitLab, BitBucket
  • Why This Was Chosen: The most popular combination of version control and management, and the one with which I am most familiar

How This Project Improved My Skills

This project was a great learning experience for me, one of many 'firsts'. It was my first NPM package, my first time working with TypeScript, first time compiling a library and my first time creating a solo open source project. Let's have a look at the different skills I upgraded while working on this project:

Skill Knowledge Before Knowledge After
Creating NPM package ❌ None βœ… Competent
Package Versioning ❌ None βœ… Competent
Compiling a Package ❌ None βœ… Competent
Configuring package.json βœ… Competent πŸ’Ž Advanced
TypeScript Declaration Files ❌ None βœ… Competent
JSDoc Implementation 🟑 Some βœ… Competent
Open Source Setup 🟑 Some βœ… Competent
MERN Error Handling βœ… Competent πŸ’Ž Advanced
Middleware Usage βœ… Competent πŸ’Ž Advanced
API Security βœ… Competent πŸ’Ž Advanced

After completing this package, I am now confident in my ability to do the following:

  • Create, publish and update an NPM package, with semantic versioning
  • Generate tarballs and test packages locally
  • Configure package.json correctly for a package, including description, exports, files, keywords etc.
  • Write JSDoc code function comments, detailing arguments, return values and options
  • Create TypeScript declaration files to give TS users type definitions and improve compatibility of JSDoc comments across import styles
  • Compile a package using tsup, setting up configuration options and providing compatibility for ESM ,CJS and TS usage
  • Create open source projects with robust documentation

What Could I Do Differently In The Future?

Although I'm pleased with my project, there are of course areas of competency identified during the course of creating this package that I would wish to improve further:

Improve Open Source Documentation

  • Priority: High
  • Why: The repository is currently missing an issue's template, a code of conduct, and a formal pull request template (although an example is provided within CONTRIBUTING.md). These files will provide additional guidance and structure to any open source contributors wishing to collaborate on the project.
  • Why it wasn't implemented: As my first lightweight package, my priority was to bring the open source documentation to a level that provided enough guidance for collaborators, while allowing myself time to research and implement additional open source documentation at a later time

Convert Package To Typescript

  • Priority: Medium
  • Why: Since this project uses vanilla JS as its logic file and separate TS files to provide type definitions, complexity is added to the updating process, increasing the risk of forgetting to update one when updating the other. Additionally, this will improve ease of configuration using the tsup bundler, and reduce complexity requirements for testing.
  • Why it wasn't implemented: This project was my first time using TS, and I was not comfortable using it to write my packages logic but rather only to add type definitions. After learning TS usage in the future this can be implemented.

Setup CI/CD on GitHub

  • Priority: Medium
  • Why: This will greatly improve the experience of open source collaboration, allowing for automated environment creation and testing. When a collaborator commits or opens a pull request, GitHub Actions spins up a VM, installs project dependencies, and checks the new code passes existing tests. This standardizes staging and testing environments, reduces manual checking of collaborator contributions required, and ensures every addition passes tests.
  • Why it wasn't implemented: Learning the processes and tools behind CI/CD is something I am doing concurrently to creating this package. Once I am more comfortable with the topic, I can look to implement CI/CD through GitHub Actions.

Create Integration Tests

  • Priority: High
  • Why: Current unit tests mock request and response objects only. Setting up a real Express app with an in memory DB for testing would dramatically improve the realism of the tests, closely mimicking real world usage.
  • Why it wasn't implemented: My prioritization when building the application was automated unit testing with 100% coverage, while I manually tested integration. Although setting up automated integration would definitely improve application reliability, it wasn't required for the initial release of the package.

Customizable Response Structure

  • Priority: Low
  • Why: The current package is quite opinionated, meaning that users are essentially forced into using the response structure I deemed suitable unless they choose to rewrap their responses. An idealized version of this package would include a configuration to choose between response structure presets that reflect the most common API industry standards. Additionally, they should have the option to pass their own custom response structure to the handler that overwrites defaults.
  • Why it wasn't implemented: As you can imagine, this approach would add significant complexity to the package, and stray slightly from the core values of being a lightweight and extremely easy to implement package that is beginner friendly. After implementing the other proposed changes to allow for a more consistent CI/CD environment, this is something I would still be interested to explore and implement.

Conclusion

Modern API creation is an ever increasingly relevant field for developers. Throughout this text we've explored:

  • A common setup/security issue facing MERN stack API's
  • The ethical and technical issues relevant to the problem
  • How current industry trends relate to the problem
  • How other existing resources have attempted to solve it
  • The custom package I created as my own solution
  • How I planned the project
  • The skills and technologies required for the project and how I chose them
  • How this project improved my skills as a developer
  • Things that could be done differently in the future

The result of this has been a drastic improvement in my own ability to understand and respond to errors within a MERN API safely, and in line with industry security expectations. I hope that you as the reader have had a similar experience! Remember, this is an open source project, so if you have desired improvements of your own, please feel free to contribute to my GitHub repository. Thank you for exploring this topic with me, and hopefully this package and my exploration of this topic have been valuable to you!

References


  1. Stop Printing Stack Traces in Prod - Medium ↩

  2. Logging Cheat Sheet - OWASP ↩

  3. Logging: The Silent Security Guard and Its Pitfalls - HackerOne ↩

  4. Code of Ethics - ACM ↩

  5. Code of Ethics - IEEE-CM ↩

  6. State of API Security Report 2025 - Salt ↩

  7. API Security Top Ten: #5 Security Misconfiguration - OWASP ↩

  8. 2024 Developer Survey - Stack Overflow ↩

  9. 2025 Developer Survey - Stack Overflow ↩

  10. 2025 State of the Developer Ecosystem (Tools and Trends) - Jetbrain ↩

  11. Why Mern Stack Dominates in 2025 - Saroj Dangol ↩

  12. jsonwebtoken - NPM ↩

  13. zod - NPM ↩

  14. pino - NPM ↩

  15. errorhandler - NPM ↩

  16. strong-error-handler - NPM ↩

  17. express-error-toolkit - NPM ↩

  18. error-cure - NPM ↩

  19. express-mongo-error-handler - NPM ↩

  20. express-mongo-error-handler - GitHub ↩

Top comments (0)