© Ujjawal Solanki | All Rights Reserved
The first Express API I built gave me false confidence. Postman returned JSON, routes responded, and I thought that meant I had built something clean.
A week later I opened the code again and could barely follow my own logic. Validation lived in random places. Errors had five different shapes. One route returned `data`, another returned `message`, and another just exploded.
That experience taught me a painful but useful lesson: an API can work and still be badly designed.
I wrote routes as if they were isolated mini scripts. Each endpoint solved its own tiny problem without any agreement with the others.
That freedom felt fast at the start. Later it became expensive. Every bug fix added a new exception to an already inconsistent system.
Frontend teams do not struggle with your API because they dislike your tech stack. They struggle because the contract keeps changing.
What helped me was treating the API as a product for other code. React is the customer. Mobile clients are customers. Even future you is a customer.
Once I saw it that way, consistency mattered more than cleverness.
I keep routes boring and predictable: `/api/users`, `/api/projects`, `/api/orders`. That removes a surprising amount of mental friction when the app grows.
app.use('/api/auth', authRoutes);
app.use('/api/projects', projectRoutes);
app.use('/api/tasks', taskRoutes);When Mongo queries, validation, response shaping, and business rules all live in the route file, each endpoint becomes a wall of noise.
Thin routes make it easier to test logic, reuse logic, and debug logic.
router.post('/', requireAuth, validateProject, createProject);
async function createProject(req, res, next) {
try {
const project = await projectService.create(req.user.sub, req.body);
res.status(201).json({ data: project });
} catch (err) {
next(err);
}
}One of the biggest improvements I made was boring response formatting. The frontend should not need detective skills to parse a simple result.
// success
res.status(200).json({ data: project });
// validation error
res.status(400).json({ message: 'Title is required' });
// not found
res.status(404).json({ message: 'Project not found' });I used to rely on happy-path payloads from my own frontend. Then the first malformed request reminded me that APIs live in a larger world.
Validation protects the app from accidental bad data and saves your controllers from defensive clutter.
function validateProject(req, res, next) {
if (!req.body.title || !req.body.title.trim()) {
return res.status(400).json({ message: 'Title is required' });
}
next();
}Without a shared error handler, every route invents its own panic language. I prefer one place where unexpected failures become consistent responses and useful logs.
app.use((err, req, res, next) => {
console.error(err);
res.status(err.status || 500).json({
message: err.message || 'Internal server error'
});
});The best APIs feel a little boring. Boring is good. It means people can depend on the contract without reading your mind.
A strong REST API does not win because it is flashy. It wins because the rest of the stack stops tripping over it.
If this helped you, save it for later. And if you are stuck on a MERN feature, you can always reach out to me.
© Ujjawal Solanki | All Rights Reserved