๐Ÿšง Warning: This blog involves serious backend building. No fluff, no TODO apps, and definitely no res.send('Hello from 2015').

๐Ÿ‘‹ Welcome back, backend bestie! In Part 1, we Marie-Kondo'ed your folder structure and kicked index.js chaos out the door. Now it's time to build something real โ€” an actual working API that:

โœ… Talks to MongoDB โœ… Validates user input โœ… Doesn't blow up in Postman โœ… Actually does what APIs are supposed to do

We're skipping the fluff. No "Hello World", no fake To-Do apps. This is the real-world stuff โ€” the kind of backend you'd actually use in a side project, freelance gig, or startup hustle.

Let's go from "where do I even start?" to "yo, it just worked on the first try!"

๐Ÿ› ๏ธ What We're Building Today

A simple (but real) API:

  • POST /users โ†’ Create a user
  • GET /users/:id โ†’ Get a user by ID
  • MongoDB for storage
  • Zod for input validation
  • Express + TypeScript stack
  • Tested in Postman (because cURL gives me trust issues)

๐Ÿ—‚๏ธ File Setup Recap (Quick Refresher)

From Part 1, we're working in this structure:

/src
 โ”ฃ /features
 โ”ƒ โ”ฃ /users
 โ”ƒ โ”ƒ โ”ฃ user.controller.ts
 โ”ƒ โ”ƒ โ”ฃ user.service.ts
 โ”ƒ โ”ƒ โ”ฃ user.routes.ts
 โ”ƒ โ”ƒ โ”ฃ user.model.ts
 โ”ƒ โ”ƒ โ”ฃ user.validators.ts
 โ”ƒ โ”ƒ โ”— index.ts
 โ”ƒ โ”ฃ /auth
 โ”ƒ โ”ƒ โ”ฃ auth.controller.ts
 โ”ƒ โ”ƒ โ”ฃ auth.service.ts
 โ”ƒ โ”ƒ โ”ฃ auth.routes.ts
 โ”ƒ โ”ƒ โ”ฃ auth.strategy.ts
 โ”ƒ โ”ƒ โ”— index.ts
 โ”ƒ โ”— ...more features (posts, payments, unicorns ๐Ÿฆ„)
 โ”ฃ /shared
 โ”ƒ โ”ฃ /middlewares
 โ”ƒ โ”ฃ /utils
 โ”ƒ โ”ฃ /constants
 โ”ƒ โ”— /types
 โ”ฃ /config
 โ”ƒ โ”ฃ env.config.ts
 โ”ƒ โ”— db.config.ts
 โ”ฃ /core
 โ”ƒ โ”ฃ app.ts
 โ”ƒ โ”— server.ts
 โ”ฃ /tests
 โ”ƒ โ”ฃ /unit
 โ”ƒ โ”— /integration
 โ”— index.ts

๐Ÿ”— Step 1: Connect MongoDB Like an Adult

In /config/db.config.ts:

import mongoose from 'mongoose';

export const connectDB = async () => {
  try {
    await mongoose.connect(process.env.MONGO_URI!);
    console.log('โœ… MongoDB connected');
  } catch (err) {
    console.error('โŒ MongoDB connection error:', err);
    process.exit(1);
  }
};

Now plug it in core/server.ts:

import { connectDB } from '../config/db.config';
connectDB();

.env

MONGO_URI=mongodb://localhost:27017/backend_besties

Yes, please use .env. Don't make me find your hardcoded passwords on GitHub.

๐Ÿ‘ค Step 2: Create a User Schema

In /features/users/user.model.ts:

import mongoose from 'mongoose';

const UserSchema = new mongoose.Schema(
  {
    name: { type: String, required: true },
    email: { type: String, required: true, unique: true },
  },
  { timestamps: true }
);
export default mongoose.model('User', UserSchema);

Short. Sweet. Gets the job done.

๐Ÿง  Step 3: Add Zod for Input Validation

Install it:

npm i zod

In /features/users/user.validators.ts:

import { z } from 'zod';

export const createUserSchema = z.object({
  name: z.string().min(1, "Name is required"),
  email: z.string().email("Invalid email"),
});

And then use it in your controller.

๐Ÿ“ฅ Step 4: Controller + Service Logic

In /features/users/user.controller.ts:

import { Request, Response } from 'express';
import * as userService from './user.service';
import { createUserSchema } from './user.validators';

export const createUser = async (req: Request, res: Response) => {
  try {
    const data = createUserSchema.parse(req.body);
    const user = await userService.create(data);
    res.status(201).json(user);
  } catch (err: any) {
    res.status(400).json({ error: err.message });
  }
};

export const getUserById = async (req: Request, res: Response) => {
  try {
    const user = await userService.findById(req.params.id);
    if (!user) return res.status(404).json({ error: 'User not found' });
    res.json(user);
  } catch (err: any) {
    res.status(400).json({ error: err.message });
  }
};

In /features/users/user.service.ts:

import User from '../../models/User';

export const create = async (data: { name: string; email: string }) => {
  return await User.create(data);
};

๐Ÿ›ฃ๏ธ Step 5: Plug It into Routes

In /features/users/user.routes.ts:

import express from 'express';
import * as controller from './user.controller';

const router = express.Router();
router.post('/', controller.createUser);
router.get('/:id', controller.getUserById);

export default router;

Then import this into /core/app.ts:

import userRoutes from '../features/users/user.routes';

app.use('/users', userRoutes);

Boom. You just built an actual backend endpoint.

๐Ÿงช Step 6: Test Like a Pro in Postman

{
  "name": "Yaksh",
  "email": "yaksh@example.com"
}

โœ… Check responses โŒ Break stuff on purpose (send invalid email) ๐ŸŽ‰ Throw party when validation saves your backend from bad data

โš ๏ธ Common Mistakes to Avoid

  • Skipping validation because "frontend handles it" (spoiler: it won't)
  • Not using .env files (and leaking secrets to GitHub)
  • Writing controller + DB logic in one file like it's 2012
  • Not handling errors (aka user goes poof and no one knows why)

๐Ÿ”ฎ What's Next in the Series?

Next up: we'll add authentication (because right now, any random human can hit /users and we're not about that life).

We'll cover:

  • JWT auth
  • Login/Register APIs
  • Securing routes like a backend bouncer ๐Ÿงข๐Ÿ•ถ๏ธ

๐Ÿค Let's Be Backend Besties

If this helped even a little, smash that follow button on Medium, drop a comment, or send me a cookie emoji ๐Ÿช. Want the next post in your inbox? ๐Ÿ‘‰ Subscribe here Let's build better backends, together. ๐Ÿ™Œ

โœ๏ธ This post is part 2 of my series:

From Messy to Mastery: Structuring Your Node.js Backend Like a Pro

More posts coming soon! ๐Ÿ”ฅ

๐Ÿ“š Missed a part? Check out the full series here โ†’ ๐Ÿ› ๏ธ From Messy to Mastery: Structuring Your Node.js Backend Like a Pro

๐Ÿ”” New blogs every Tuesday & Friday! Follow me on Medium or on LinkedIn so you don't miss the good stuff.