๐ง 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 userGET /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_bestiesYes, 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 zodIn /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
.envfiles (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.