Node.js API Development: From Zero to Production
Building RESTful APIs with Node.js and Express has been a core part of my work as a MERN stack developer. After building APIs for e-commerce platforms, appointment systems, and crypto trackers, I've developed a solid workflow that I'm sharing today.
Why Node.js for APIs?
When I started backend development, I chose Node.js because:
- JavaScript everywhere - Same language for frontend and backend
- Fast and scalable - Non-blocking I/O handles thousands of requests
- Rich ecosystem - npm has packages for everything
- Easy deployment - Vercel, Heroku, AWS all support it
Project Setup
Here's how I start every API project:
mkdir my-api && cd my-api
npm init -y
npm install express mongoose dotenv cors
npm install -D nodemon
Folder Structure
my-api/
├── config/
│ └── db.js # Database connection
├── models/ # Mongoose models
├── routes/ # API routes
├── controllers/ # Business logic
├── middleware/ # Custom middleware
├── utils/ # Helper functions
├── .env # Environment variables
└── server.js # Entry point
1. Setting Up Express Server
// server.js
const express = require('express');
const cors = require('cors');
const dotenv = require('dotenv');
const connectDB = require('./config/db');
dotenv.config();
connectDB();
const app = express();
// Middleware
app.use(cors());
app.use(express.json());
// Routes
app.use('/api/users', require('./routes/userRoutes'));
app.use('/api/products', require('./routes/productRoutes'));
const PORT = process.env.PORT || 5000;
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
2. Database Connection
// config/db.js
const mongoose = require('mongoose');
const connectDB = async () => {
try {
await mongoose.connect(process.env.MONGO_URI);
console.log('MongoDB Connected');
} catch (error) {
console.error('MongoDB connection failed:', error.message);
process.exit(1);
}
};
module.exports = connectDB;
3. Creating Models
// models/User.js
const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
name: {
type: String,
required: [true, 'Name is required'],
trim: true
},
email: {
type: String,
required: [true, 'Email is required'],
unique: true,
lowercase: true
},
password: {
type: String,
required: [true, 'Password is required'],
minlength: 6
}
}, { timestamps: true });
module.exports = mongoose.model('User', userSchema);
4. Controllers Pattern
I separate business logic from routes:
// controllers/userController.js
const User = require('../models/User');
exports.getUsers = async (req, res) => {
try {
const users = await User.find().select('-password');
res.json({ success: true, data: users });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
};
exports.createUser = async (req, res) => {
try {
const { name, email, password } = req.body;
const user = await User.create({ name, email, password });
res.status(201).json({ success: true, data: user });
} catch (error) {
res.status(400).json({ success: false, message: error.message });
}
};
5. Routes
// routes/userRoutes.js
const express = require('express');
const router = express.Router();
const { getUsers, createUser } = require('../controllers/userController');
router.route('/')
.get(getUsers)
.post(createUser);
module.exports = router;
Best Practices I Follow
- Use async/await - Cleaner than callbacks
- Validate input - Never trust user data
- Hash passwords - Use bcrypt
- Rate limiting - Prevent abuse
- CORS properly - Whitelist domains
- Log errors - Use Winston or Morgan
- API versioning - /api/v1/users
Real-World Example: E-Commerce API
In my e-commerce project, I built:
GET /api/products # Get all products
GET /api/products/:id # Get single product
POST /api/products # Create product (admin)
PUT /api/products/:id # Update product (admin)
DELETE /api/products/:id # Delete product (admin)
Deployment
I deploy my APIs on:
- Vercel - Serverless functions
- Heroku - Traditional hosting
- Railway - Modern alternative
Check out my API projects on GitHub.