Best Way To Structure Your API Response

As a backend developer, it's your responsibility to ensure the API responses sent to the client are well-structured and consistent. A good API response not only helps consumers of your API but also makes debugging and future development far more manageable.

You may recall we've touched on this topic in a previous article. If not, feel free to refer to it [here].

What Do We Mean by “Well-Structured API Response”?

Let’s be honest—many developers, myself included early on, simply fetch data from a database like MongoDB, PostgreSQL, or MySQL and return it directly to the client. While this approach works, it often lacks structure, clarity, and consistency. You might be wondering—if it’s not wrong, why does this article exist?

That’s a fair question. It’s not inherently wrong to return raw data. However, it can be improved significantly with proper structure.

Real-World Example

Let’s consider a scenario where a user logs in and their profile data is stored in MongoDB. The backend exposes an endpoint like /api/v1/user/profile/:id to fetch this data. Here's a typical implementation:

const mongoose = require("mongoose");
const userModel = require("../models/user.model.js");

export const profileController = async (req, res) => {
    try {
        const { id } = req.params;

        // Validate ID
        if (!id.trim() || !mongoose.Types.ObjectId.isValid(id)) {
            return res.status(400).json({ message: "Invalid user ID" });
        }

        const user = await userModel.findById(id).select("-password");

        if (!user) {
            return res.status(404).json({ message: "User not found" });
        }

        return res.status(200).json(user);
    } catch (error) {
        // Ideally, log the error
        res.status(500).json({ message: "Internal Server Error" });
    }
}

Sample Output:

{
    "_id": "5c0a7922c9d89830f4911426",
    "username": "johndoe",
    "email": "johndoe@example.com"
}

Looks fine, right? But now consider this enhanced version:

res.status(200).json({
    status: 200,
    success: true,
    message: "User data fetched successfully",
    data: user,
    errors: []
});

Updated Output:

{
    "status": 200,
    "success": true,
    "message": "User data fetched successfully",
    "data": {
        "_id": "5c0a7922c9d89830f4911426",
        "username": "johndoe",
        "email": "johndoe@example.com"
    },
    "errors": []
}

This version is much more descriptive, professional, and ready for scaling.

But... Is It Too Verbose?

Yes, a structured response like this is slightly larger and harder to maintain. But does that mean we should avoid structuring APIs?

Absolutely not.

A well-structured API is easier to debug, document, consume, and maintain. It's an investment that pays off. Handling this structure can be more demanding—especially in production-grade architectures.

Series Update: Production-Ready Telegram Bots

On a related note, I’m launching a new series on “Building Production-Ready Telegram Bots using the Telegraf Framework” at Beyond The Stack.
To get early updates, subscribe to my newsletter and follow me on X (Twitter) and LinkedIn.

Back to Business: Let’s Solve This Problem Properly

The goal is to make a utility class ApiResponse that handles all of this formatting internally. This will make your controllers clean, readable, and maintainable.

How It Works

We’ll create a class ApiResponse that accepts:

  • status

  • message

  • data

  • errors

We’ll place it in a centralized location—typically /utils/ApiResponse.js. Here's how it looks:

// /utils/ApiResponse.js

export class ApiResponse {
    constructor(status, message = "OK", data = [], errors = []) {
        this.status = status;
        this.success = status < 400;
        this.message = message;
        this.data = data;
        this.errors = errors;
    }
}

Updated Controller Using ApiResponse

const mongoose = require("mongoose");
const userModel = require("../models/user.model.js");
const { ApiResponse } = require("../utils/ApiResponse.js");

export const profileController = async (req, res) => {
    try {
        const { id } = req.params;

        if (!id.trim() || !mongoose.Types.ObjectId.isValid(id)) {
            return res.status(400).json(new ApiResponse(400, "Invalid user ID", [], ["Invalid parameter"]));
        }

        const user = await userModel.findById(id).select("-password");

        if (!user) {
            return res.status(404).json(new ApiResponse(404, "User not found", [], ["No user with given ID"]));
        }

        return res.status(200).json(new ApiResponse(200, "User data fetched successfully", user));
    } catch (error) {
        console.error(error); // Log the error properly in production
        return res.status(500).json(new ApiResponse(500, "Internal Server Error", [], [error.message]));
    }
}

Final Response Output

{
    "status": 200,
    "success": true,
    "message": "User data fetched successfully",
    "data": {
        "_id": "5c0a7922c9d89830f4911426",
        "username": "johndoe",
        "email": "johndoe@example.com"
    },
    "errors": []
}

Conclusion

Structuring your API response isn't just a “nice-to-have”—it's a best practice. It helps ensure clarity, consistency, and professionalism across your codebase and integrations.

In this article, we introduced a simple yet powerful ApiResponse utility class to help standardize your API responses. If you're still confused, feel free to like, comment with your questions, and—most importantly—go try this in your code editor right now.

What’s Next?

We’ve laid the foundation for clean API responses. But we haven’t yet discussed how to handle errors deeply and consistently.

In the next part, we’ll build an ApiError class that mirrors the structure of ApiResponse—allowing you to build production-grade, robust error handling logic.

Subscribe to the newsletter to stay notified, and follow me on X and LinkedIn for real-time updates.

10
Subscribe to my newsletter

Read articles from Ismail Bin Mujeeb directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Ismail Bin Mujeeb
Ismail Bin Mujeeb