DEV Community

Cover image for Backend Red Flags - What NOT to do
fatih
fatih

Posted on

Backend Red Flags - What NOT to do

Standarts are there for us to maintain. By doing so, you stand a high chance of getting a GOOD software artifact in the end. However it's not guaranteed; the things that you commit may be ruining the quality of your artifact.

In this article, we will go through, step-by-step, some of the most well-known yet still not fully understood red flags in backend development, anti-patterns.


1. Never write BUSINESS LOGIC in a Controller

I'm going to say this one time, only one time, do your best to understand:

CONTROLLERS ARE ONLY THERE TO HANDLE REQUESTS AND RESPONSES.

Business Logic must be placed either in a separate contextService.file or in a module that is apart from the controller. This is MOST PROBABLY a must for you to agree upon no matter what. There may be, I'm saying "may", some cases that you think require such action; most of the time, they're avoidable. In other words, you're doing something WRONG.

Let's look at to it with an example, to make things clearer:

Bad Example

import { Request, Response } from 'express';
import bcrypt from 'bcrypt';
import { UserModel } from './models/User';

export class UserController {
  public async createUser(req: Request, res: Response): Promise<Response> {
    try {
      const { username, password } = req.body;

      // business logic put inside the controller
      const hashedPassword = await bcrypt.hash(password, 10);
      const newUser = await UserModel.create({ username, password: hashedPassword });

      return res.status(201).json(newUser);
    } catch (error) {
      return res.status(500).json({ message: 'Error creating user' });
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Why this is bad?

  • The controller is too bloated because it mixes request handling with business logic.
  • Reusing the hashing or user creation logic elsewhere becomes difficult.
  • Testing the controller becomes harder, as you'd need to mock business logic in your tests.

Good Example

// UserService.ts
import bcrypt from 'bcrypt';
import { UserModel } from './models/User';

export class UserService {
  public async createUser(username: string, password: string): Promise<any> {
    // business logic is encapsulated here
    const hashedPassword = await bcrypt.hash(password, 10);
    return UserModel.create({ username, password: hashedPassword });
  }
}
Enter fullscreen mode Exit fullscreen mode
// UserController.ts
import { Request, Response } from 'express';
import { UserService } from './services/UserService';

export class UserController {
  private userService = new UserService();

  public async createUser(req: Request, res: Response): Promise<Response> {
    try {
      const { username, password } = req.body;

      // delegate logic to the service
      const newUser = await this.userService.createUser(username, password);

      return res.status(201).json(newUser);
    } catch (error) {
      return res.status(500).json({ message: 'Error creating user' });
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Why this is good?

  • Separation of Concerns

The controller focuses only on handling requests and responses. Business logic is isolated in a dedicated service class, making it easier to manage.

  • Reusability

The service logic can now be reused in other parts of your artifact (e.g., another controller, a background job).

  • Testability

You can now write separate unit tests for the UserService.ts and UserController. Mocking and testing become much simpler.

2. Neglecting DTOs for Data Exposure (Sensitive Data Leak)

DTOs ARE ESSENTIAL FOR SECURING THE DATA YOU EXPOSE.

Without DTOs, you risk EXPOSING sensitive data you shouldn’t. You may think it's okay to return raw database objects or use models directly, but most of the time, you're just ASKING FOR TROUBLE.

Sensitive fields like passwords, tokens, and personal information should never be exposed in a response unless absolutely necessary. It’s your job to prevent that, using DTOs to clean and filter the data.

If you skip this, YOU'RE ASKING FOR A DATA LEAK.

Let's look at an example to make it crystal clear:

Bad Example

import { Request, Response } from 'express';
import { UserModel } from './models/User';

export class UserController {
  public async getUser(req: Request, res: Response): Promise<Response> {
    try {
      const userId = req.params.id;

      // fetch user directly from the database also business logic inside the controller
      const user = await UserModel.findById(userId);

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

      // Returning raw data (contains sensitive fields like password)
      return res.json(user);
    } catch (error) {
      return res.status(500).json({ message: 'Error fetching user' });
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Why this is bad?

  • Sensitive Data Exposure

The raw user object might include sensitive fields like password, isAdmin, or createdAt, which should not be exposed to the frontend.

  • Lack of Control

There’s no control over which fields are exposed in the response. The entire object is sent, which could lead to security issues or unnecessary data being leaked.

Good Example

// UserDTO.ts
export class UserDTO {
  constructor(public id: string, public username: string, public email: string) {}

  // static method to map the user entity to a DTO
  public static fromEntity(user: any): UserDTO {
    return new UserDTO(user._id, user.username, user.email);
  }
}
Enter fullscreen mode Exit fullscreen mode
// UserController.ts
import { Request, Response } from 'express';
import { UserModel } from './models/User';
import { UserDTO } from './dtos/UserDTO';

export class UserController {
  public async getUser(req: Request, res: Response): Promise<Response> {
    try {
      const userId = req.params.id;

      // fetch user directly from the database
      const user = await UserModel.findById(userId);

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

      // map user entity to DTO
      const userDTO = UserDTO.fromEntity(user);

      // return the controlled user DTO, not raw data
      return res.json(userDTO);
    } catch (error) {
      return res.status(500).json({ message: 'Error fetching user' });
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Why this is good?

  • Only the necessary fields are included in the DTO, preventing sensitive data exposure.

3. Never Use Synchronous Code in Async Operations

This one’s simple: NEVER mix synchronous code in asynchronous operations. It’s like trying to run a marathon with a broken leg—it's going to SLOW YOU DOWN.

When you write asynchronous code, you’re telling the system, “Hey, go ahead and do this task while I get other stuff done.” If you suddenly throw synchronous code into the mix, you're blocking the entire process. It’s like a traffic jam on a highway—everything else has to wait for that one slow-moving car to get out of the way.

Why does this matter?
When you use synchronous operations (like fs.readFileSync or JSON.parse()), you're forcing the system to halt while that operation completes, preventing other tasks from being processed. This becomes especially dangerous in a highly concurrent environment like web servers, where latency can directly impact the user experience.

NEVER do this if you want your system to scale and respond quickly. Stick to asynchronous methods, and you'll keep your app smooth, fast, and efficient.

Let’s see this with a code example:

Bad Example

// UserProfileService.ts
import fs from 'fs';
import { UserProfile } from '../models/UserProfile';

export class UserProfileService {
  public async getUserProfile(userId: string): Promise<UserProfile> {
    try {
      // synchronous operation: this blocks the event loop until the file is fully read
      const data = fs.readFileSync(`./data/users/${userId}.json`, 'utf-8');

      // parse JSON data and return the user profile
      const userProfile: UserProfile = JSON.parse(data);
      return userProfile;
    } catch (error) {
      throw new Error('User profile not found');
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Why this is bad?

  • Blocking the Event Loop

fs.readFileSync() is synchronous, so when the server reads a file, it blocks the event loop. While reading one user's profile, other incoming requests have to wait. This is a major performance bottleneck.

  • Scalability Problems

If multiple users are trying to fetch their profile simultaneously, each request would block the server, causing a queue of requests and increasing response time. The server can handle fewer requests per second, leading to poor user experience.

Good Example

// UserProfileService.ts
import fs from 'fs/promises';
import { UserProfile } from '../models/UserProfile';

export class UserProfileService {
  public async getUserProfile(userId: string): Promise<UserProfile> {
    try {
      // asynchronous operation: non-blocking, doesn't hold up the event loop
      const data = await fs.readFile(`./data/users/${userId}.json`, 'utf-8');

      // parse JSON data and return the user profile
      const userProfile: UserProfile = JSON.parse(data);
      return userProfile;
    } catch (error) {
      throw new Error('User profile not found');
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Why this is good?

  • Non-Blocking

Using fs.readFile() from the fs/promises module makes the file reading operation asynchronous. The await ensures that while waiting for the file to be read, other tasks (such as responding to different HTTP requests) are processed without any delay.

  • Scalable

Since the event loop isn't blocked, the server can handle many concurrent requests efficiently. If multiple users request their profiles at the same time, the server can continue serving other requests while it waits for each file to be read.

  • Better User Experience

Users won't experience delays due to blocking, leading to faster response times and a better overall user experience.

4. Too Generic Error Handling- A Debugging Nightmare

Let’s talk about one of the most frustrating mistakes you can make when dealing with errors in your application: Too Generic Error Handling. You’ve probably encountered this before—errors that are too vague or too repetitive to be useful. When you catch errors and just throw or log a generic message like "Something went wrong" or "An error occurred", you’re essentially making debugging harder for yourself and your team. Context is everything, and a generic error message will only leave you guessing what actually went wrong. Debugging should be about tracing the root cause, not staring at the same meaningless message over and over again.

Let’s explore why this happens and how you can avoid it with better error handling:

Bad Example

// PaymentService.ts
import axios from 'axios';

export class PaymentService {
  private paymentAPI = 'https://paymentgateway.com/api/payment';

  // method to process payment
  public async processPayment(userId: string, amount: number): Promise<boolean> {
    try {
      const response = await axios.post(this.paymentAPI, { userId, amount });

      if (response.data.success) {
        return true;
      } else {
        throw new Error('Payment failed');
      }
    } catch (error) {
      // catch all errors and print a generic message
      console.error('An error occurred during payment processing');
      return false;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Why this is bad?

  • No Error Context

The error handling is generic and does not provide any useful information about the nature of the failure. It doesn't account for different types of errors that may occur (e.g., network errors, API-specific errors, or user-related errors).

  • Repetitive Message

The same message ("An error occurred during payment processing") is logged regardless of the actual issue. This leads to repetitive and uninformative error logs that make debugging much harder.

Good Example

// PaymentService.ts
import axios from 'axios';

export class PaymentService {
  private paymentAPI = 'https://paymentgateway.com/api/payment';

  // method to process payment
  public async processPayment(userId: string, amount: number): Promise<boolean> {
    try {
      const response = await axios.post(this.paymentAPI, { userId, amount });

      if (response.data.success) {
        return true;
      } else {
        // handle API-specific failure scenarios
        if (response.data.errorCode === 'INSUFFICIENT_FUNDS') {
          throw new Error('Payment failed due to insufficient funds');
        } else if (response.data.errorCode === 'INVALID_CARD') {
          throw new Error('Payment failed due to invalid card');
        } else {
          throw new Error('Payment failed due to an unknown error');
        }
      }
    } catch (error: any) {
      // check if it's a network or server error
      if (error.response) {
        console.error(`Payment failed with status ${error.response.status}: ${error.response.data.message}`);
      } else if (error.request) {
        console.error('Payment request failed: No response from payment gateway');
      } else {
        console.error(`Payment failed: ${error.message}`);
      }
      return false;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Why this is good?

  • Specific Error Handling

The code checks for specific error codes like INSUFFICIENT_FUNDS or INVALID_CARD to provide clear, targeted messages.

  • Actionable Error Messages

The system gives clear, actionable messages that help users and developers understand and fix the issue.

  • Error Categorization

The code distinguishes between different error types (e.g., network, API errors) to handle them appropriately.

  • Transparent Debugging

Detailed logs make it easy to pinpoint exactly where and why the error happened.


Conclusion

Well, these are some of the most common red flags I've encountered over the years in software development. Don’t feel bad if you've fallen into these traps—there's always room to improve. Just take the time, learn from it, which will help you to become best version of yourself over the time.

thank_you

I sincerely thank you for reading through, consider connecting with
me on;

youtube
x
linkedin
my personal website - fatihguzel.dev

Top comments (0)