HomeBlogProjects
⌘+K
Click here to switch the theme or press d

click to read any blog 

navigate
HomeBlogProjects
explore
BooksGameTools
social
InstagramX
work
PeerlistLinkedInGithub
creator
MediumYouTube
© 2026 ALOK. All rights reserved.
Back to blog

Must-Take Precautions While Designing Your Backend

March 14, 2026
Read on Medium
you can also support on medium

Most backend systems don’t fail because of bad logic. They fail because of bad structure, blind trust, and decisions made in a hurry that nobody wants to touch six months later. Whether you’re starting fresh or scaling something existing, there are a handful of things that will save you enormous pain down the road. This is not a beginner’s tutorial. These are lessons learned from real projects that either held up or fell apart.

Backend checklist

1. Structure Your Folders by Feature, Not by Function

No doubt you learn this structure while learning from online toturial or making any project following this classing service, controller, routes

The classic mistake looks like this:

/controllers
  authController.js
  userController.js
/services
  authService.js
  userService.js
/routes
  authRoute.js
  userRoute.js

At first glance it feels organized. But every time you work on the auth feature, you're jumping across three folders. Your mental model of "auth" is scattered across your entire codebase.

Instead, group by feature:

/auth
  auth.controller.js
  auth.service.js
  auth.route.js
/user
  user.controller.js
  user.service.js
  user.route.js
/order
  order.controller.js
  order.service.js
  order.route.js

Now when a new developer joins and needs to touch auth, they go to /auth and everything they need is right there. Deleting a feature becomes a single folder deletion. Testing becomes easier. Ownership is clear.

This pattern becomes non-negotiable once your codebase grows past a certain size.

2. Never Trust the Client

Your frontend is not your security layer. It never was. Anyone with a browser devtools window or a curl command can send whatever they want to your API. Input that looks clean in your UI can arrive at your server completely malformed, malicious, or just plain wrong.

Add validation on the server side. Always. Use Zod or a similar schema library:

import { z } from "zod";
const registerSchema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
  username: z.string().min(3).max(20),
});
// In your route handler
const result = registerSchema.safeParse(req.body);
if (!result.success) {
  return res.status(400).json({ errors: result.error.flatten() });
}

The rule is simple: validate the shape, type, and constraints of every incoming request before it touches your business logic or your database. No exceptions.

3. Add a Rate Limiter

Without rate limiting, a single user can hammer your API thousands of times per minute. That might be someone trying to brute force a login, scrape your data, or just a bug in their client making infinite retry loops.

Rate limiting is a one-time setup with a large payoff. Using Express? express-rate-limit is simple and effective:

import rateLimit from "express-rate-limit";
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100,
  message: "Too many requests, please try again later.",
});
app.use("/api", limiter);

For auth routes specifically, make it stricter. A login endpoint should not allow 100 attempts in 15 minutes from the same IP. Drop that to 5 or 10 and lock it down.

how rate limiter protect you

4. Cache Frequently Accessed Data with Redis

If your app is reading the same data from the database on every request, you’re leaving a lot of performance on the table. User profiles, product catalogs, configuration settings, dashboard summaries. These don’t change on every request, yet many backends query the database every single time.

Redis fixes this:

import redis from "ioredis";
const client = new redis();
async function getUserProfile(userId) {
  const cached = await client.get(`user:${userId}`);
  if (cached) return JSON.parse(cached);
  const user = await db.users.findById(userId);
  await client.setex(`user:${userId}`, 3600, JSON.stringify(user));
  return user;
}

The pattern is straightforward: check the cache first, hit the database only on a miss, and write the result back to cache with an expiry. Your database will thank you when traffic spikes.

5. Stop Reaching for External Packages for Small Tasks

the blackhole theory of node modules

This one is a slow leak. You add a package to check if a string is a palindrome. Another one to capitalize a word. Another for deep equality checks. Before long, your node_modules is 400MB and half of it is solving problems that are two lines of JavaScript.

Every external dependency is a maintenance burden, a potential security vulnerability, and a source of supply chain risk. The npm ecosystem has had multiple incidents where a small, widely used package was compromised or simply deleted, breaking thousands of projects overnight. Before installing anything, ask yourself: can I write this in under 20 lines?

That said, there are cases where the logic is genuinely complex. Temporary email detection is one of them. If you’re building anything with user accounts, people will sign up with throwaway emails from services like Mailinator or Guerrilla Mail to skip your onboarding flow, bypass paywalls, or just avoid getting any follow-up.

Here is the thing though: you don’t need a package for this either. There is a GitHub repo called disposable-email-domains that maintains a list of over 3000 known throwaway email domains. Just copy that list into your own project as a local file:

import { validate } from "deep-email-validator";
const result = await validate("user@mailinator.com");
if (!result.valid) {
  return res.status(400).json({ error: "Disposable emails are not allowed." });
}

The rule is: be picky. Add dependencies that solve genuinely hard problems. Don’t add them because you don’t want to write a for loop.

6. Design Before You Code

This one gets skipped the most and causes the most regret.

Before you write a single route, sit down with a tool like Excalidraw and sketch out how your system actually works. Draw the entities. Draw the data flow. Map out which service calls which. Decide where authentication sits. Figure out where caching makes sense. Think about what happens when things fail.

A design-first approach forces you to catch bad ideas before they become bad code. Changing a box in a diagram takes ten seconds. Refactoring a tightly coupled service takes three days. The investment pays for itself before you’ve written a single line.

You don’t need a fancy tool. Excalidraw is free, fast, and runs in the browser. The habit matters more than the tool.

Wrapping Up

None of these things are exotic. They’re all well-known practices, but the gap between knowing them and actually doing them is where most backend problems are born. Structure your folders so a new developer can navigate them. Validate everything that comes from the outside. Protect your endpoints with rate limiting. Cache data that doesn’t need to be fetched fresh every time. Be deliberate about your dependencies. And before you write any of it, think it through on paper first.

Get these six things right and you’ll spend a lot less time firefighting and a lot more time building.

#backend#security#nodejs#backend-development#software-development
Back to blog
you may like that also

Read related blogs

08.03.2026

The Button That Sells Your Identity: What Really Happens When You 'Login with Google'

Read article →

#google#database#security#privacy#oauth

15.02.2026

Do Progressive Blur Matters in your website

Read article →

#web-development#software-engineering#programming#software-development#full-stack-developer