Full Stack Developer specializing in React, Node.js, and scalable web solutions.

I am Ujjawal Solanki, a dedicated software developer with expertise in full-stack development, React applications, and Node.js backend services. I create innovative, scalable, and user-friendly web applications.

Build Production REST APIs with Node.js and Express

My First Express API Worked. It Was Still a Mess

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.

What made the first version messy

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.

The shift from routes to contracts

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.

The structure I use now

Step 1

Group endpoints by resource and purpose

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);
Step 2

Keep route handlers thin and move logic into controllers or services

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);
  }
}
Step 3

Return predictable success and error shapes

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' });
Step 4

Validate input before it hits your real logic

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();
}
Step 5

Add a central error handler so failures stay readable

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'
  });
});

Troubleshooting patterns I watch for

  1. If React needs route-specific parsing logic, my response shapes are drifting.
  2. If every controller repeats auth or validation code, I need middleware.
  3. If bug fixes feel local but break other routes, I probably lack shared rules.
  4. If logs are noisy but not informative, I am handling errors too late.

The best APIs feel a little boring. Boring is good. It means people can depend on the contract without reading your mind.

What I optimize for now

  • Clear folder structure over clever abstractions.
  • Consistent HTTP status codes.
  • Shared middleware for auth, validation, and errors.
  • Environment-based config instead of hardcoded secrets.

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.