I stopped thinking APIs break because of “complex code”.
They break because of boring things you didn’t take seriously.
Here are 5 lessons from production:
1. Validate early or suffer later
I used to validate inside the logic.
Then weird bugs started appearing far from the source.
Now I just kill bad requests immediately:
if (!req.body.email || typeof req.body.email !== "string") {
return res.status(400).json({ error: "Valid email is required" });
}
No validation inside business logic. Ever.
2. Your errors are part of your API contract
A generic 500 is useless in production.
Be explicit:
return res.status(401).json({ error: "Invalid API key" });
return res.status(402).json({ error: "Insufficient credits" });
If your error needs explanation in Slack, your API message failed.
3. Middleware order can break everything silently
I once debugged “broken auth” for hours.
It was just middleware order.
app.use(cors()); // must go first
app.use(express.json());
app.use(authMiddleware);
app.use("/api", routes);
Move one line and everything changes.
4. Logging should be boring, not noisy
I’ve tried both extremes.
Both were wrong.
What actually helps in production:
console.log(`${req.method} ${req.path} -> ${res.statusCode}`);
And for real debugging:
console.error({
requestId,
error: err.message,
stack: err.stack
});
Everything else becomes noise at 3AM.
5. Rate limiting is not “later”
I learned this after watching an endpoint get hammered and cost real money.
import rateLimit from "express-rate-limit";
const limiter = rateLimit({
windowMs: 60 * 1000,
max: 60,
message: { error: "Too many requests" }
});
app.use(limiter);
If your API has no limits, it doesn’t have protection. It has hope.
Final thought
Most API failures don’t come from complex engineering.
They come from ignoring the basics because “it’ll be fine”.
It won’t.
Production doesn’t care.
Top comments (0)