Welcome, developers! ๐ Whether youโre just starting out or looking to level up your backend development skills, youโre in the right place. In this in-depth guide, weโll walk you through everything you need to know to build and deploy a real-world, production-ready Backend APIโstep by step.
This isnโt just another basic tutorial. Weโre going beyond โHello Worldโ and diving into core concepts, real-world tools, and deployment so you can build applications that are secure, scalable, and performant.
So grab your โ coffee (or ๐ต tea), fire up your code editor, and letโs get started on your journey to becoming a backend pro!
๐Understanding the Web โ The Backbone of Every App
Before you start building powerful backend systems, itโs crucial to understand how the web works. Think of this as laying the foundation before constructing a skyscraper.
๐งฑ The Two Sides of the Web: Frontend vs Backend
The web is divided into two main parts:
- Frontend: What users see and interact with. Built using HTML, CSS, JavaScript, and modern frameworks like React and Next.js.
- Backend: What users donโt see but is equally (if not more) important. Handles data processing, security, authentication, and server logic.
Even if the frontend is sleek and beautiful, the app will fall apart without a robust backend.
๐ฅ๏ธ Client-Server Architecture
Hereโs how a typical web interaction works:
- A client (your browser or app) sends a request to a server.
- The server, a powerful machine somewhere in the world, processes this request.
- It sends back a response (HTML, JSON, or a file) over the internet.
This fundamental architecture is what powers everything from Google searches to Instagram posts.
๐ก Web Communication: Protocols, IPs, and DNS
๐ Protocols: The Internetโs Language
- The internet uses protocolsโrules for communication.
- The most common? HTTP (Hypertext Transfer Protocol).
- In secure environments, itโs HTTPS (the โSโ stands for Secure).
These protocols allow clients and servers to exchange information reliably.
๐ DNS: The Internetโs Phone Book
Imagine trying to remember 142.250.190.78
instead of google.com
. Yikes!
- DNS (Domain Name System) translates human-readable names to IP addresses.
- Computers need these IPs to locate each other.
- There are two formats:
- IPv4: Most common, but limited.
- IPv6: The future, with more addresses.
This system keeps the internet user-friendly and organized.
๐ What Are APIs? Your Backendโs Interface to the World
๐ฝ๏ธ API = Waiter at a Restaurant
Think of an API as a waiter:
- You (the client) tell the waiter (API) what you want.
- The waiter communicates with the kitchen (backend).
- The waiter brings your food (data) back to you.
An API (Application Programming Interface) is a way for software systems to talk to each other.
๐ฌ Anatomy of an API Request
Letโs break it down:
Part | Description |
---|---|
HTTP Method | Action to be performed (GET, POST, PUT, DELETE) |
Endpoint | The URL of the resource (e.g., /api/users ) |
Headers | Metadata (e.g., Content-Type , Authorization ) |
Body | Data sent to the server (for POST/PUT) |
A typical request might look like this:
POST /api/users HTTP/1.1
Content-Type: application/json
Authorization: Bearer your_token_here
{
"name": "Jane Doe",
"email": "jane@example.com"
}
The server then processes this and sends back a responseโusually in JSON format.
๐ง Status Codes: HTTPโs Feedback System
When you send a request, the server responds with a status code. Here are some key ones to know:
- 200 OK: Everything went well.
- 201 Created: New resource successfully created.
- 400 Bad Request: Somethingโs wrong with your request.
- 401 Unauthorized: You need to log in or provide a token.
- 404 Not Found: The endpoint or resource doesnโt exist.
- 500 Internal Server Error: Something went wrong on the server.
These codes are essential for debugging and improving your APIโs UX.
๐ REST vs GraphQL: API Paradigms
๐ฑ RESTful APIs
- Use standard HTTP methods (GET, POST, PUT, DELETE).
- Resources are represented by URLs.
- Stateless: Each request is independent.
๐ Example:
GET /api/subscriptions
Great for simplicity and scalability. Weโll focus on building a REST API in this guide.
๐งฌ GraphQL APIs
- Flexible querying (fetch exactly what you need).
- Uses one endpoint for all data interactions.
- Ideal for complex data relationships.
But: It comes with a steeper learning curve and setup.
๐งฐ Choosing the Right Tech Stack for the Backend
Now that we understand the web and APIs, itโs time to talk tech. Hereโs what most modern backend systems use.
๐งโ๐ป Languages
Popular backend languages include:
- JavaScript (Node.js, Bun, Deno): Fast, widely used.
- Python (Django, Flask): Beginner-friendly, great for rapid development.
- Ruby (Rails): Convention-over-configuration.
- Java (Spring Boot): Enterprise-grade, secure.
In this guide, weโll use Node.js and Express.jsโperfect for beginners and powerful enough for production.
๐๏ธ Frameworks
Frameworks simplify your life by:
- Handling routing (which URL does what).
- Managing middleware (pre-processing requests).
- Enabling error handling, security, and templating.
Some popular ones:
Language | Framework |
---|---|
JavaScript | Express.js, Fastify |
Python | Django, Flask |
Ruby | Rails |
Java | Spring Boot |
๐ Weโll use Express.jsโlightweight, flexible, and widely supported.
๐๏ธ Databases, Data Modeling, and Your First API Endpoint
So far, youโve learned how the web works, what APIs are, and how they connect the frontend to the backend. Now itโs time to dive deeper into one of the most critical parts of any backend system:
The Database โ your appโs memory. ๐ง
Letโs explore what databases are, how they work, and how to build and interact with one using Node.js, Express, and MongoDB.
๐ง Why You Need a Database
Imagine a social media app with no memory. You log in, post something, and itโs gone the moment you refresh the page.
Thatโs what an app without a database is like. ๐ซ
Databases allow your app to:
- ๐ฅ Store user data (like names, emails, passwords)
- ๐ค Retrieve saved information on request
- ๐ Update records (like changing an email)
- โ Delete data when needed
Whether youโre building a subscription tracker, an e-commerce site, or a chat app, databases are non-negotiable.
๐งพ Types of Databases
There are two major types of databases. Letโs compare them:
1. Relational Databases (SQL)
- Structure: Tables with rows and columns
- Query language: SQL
- Data is highly structured and relational
Examples: MySQL, PostgreSQL, SQLite
๐ Use when: Your data has clear relationships (e.g., Orders โ Customers โ Products)
2. Non-relational Databases (NoSQL)
- Structure: Flexible (documents, key-value pairs, etc.)
- Commonly uses JSON-like data
- Schema can evolve over time
Examples: MongoDB, Firebase, Redis
๐ Use when: You want fast, flexible storage (e.g., blogs, IoT data, social media)
๐ Why We Use MongoDB in This Guide
- โ Fast and scalable
- โ Easy to learn (especially for JavaScript devs)
- โ Uses JSON-like syntax
- โ Pairs perfectly with Mongoose (an ODM)
MongoDB stores data as documents in collections instead of rows in tables.
Example document:
{
"_id": "662bb1af0f7d",
"name": "Alice",
"email": "alice@example.com",
"subscriptions": ["Netflix", "Spotify"]
}
๐งฉ Introducing Mongoose: Your Data Translator
Mongoose is an Object Data Modeling (ODM) library for MongoDB and Node.js. It lets you:
- Define data models (schemas)
- Validate data before saving
- Interact with MongoDB using JavaScript syntax
Think of it as the bridge between your code and your database.
๐ง Setting Up the Project
Letโs set up a backend project with Express, MongoDB, and Mongoose.
๐ Folder Structure
backend-api/
โโโ config/
โโโ controllers/
โโโ models/
โโโ routes/
โโโ middlewares/
โโโ .env
โโโ server.js
โ Dependencies to Install
Run this command in your terminal:
npm init -y
npm install express mongoose dotenv nodemon
Also, add a start script in package.json
:
"scripts": {
"start": "nodemon server.js"
}
๐ฑ Connecting to MongoDB
Create a .env
file to store sensitive configs:
PORT=5000
MONGO_URI=mongodb+srv://yourusername:password@cluster.mongodb.net/api?retryWrites=true&w=majority
Now letโs connect to the database in server.js
:
require('dotenv').config();
const express = require('express');
const mongoose = require('mongoose');
const app = express();
app.use(express.json());
mongoose
.connect(process.env.MONGO_URI)
.then(() => {
console.log('โ
Connected to MongoDB');
app.listen(process.env.PORT, () => {
console.log(`๐ Server running on port ${process.env.PORT}`);
});
})
.catch((err) => console.error('โ MongoDB connection error:', err));
๐ Create Your First Data Model (User)
In models/User.js
:
const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
name: {
type: String,
required: [true, 'Name is required'],
},
email: {
type: String,
required: [true, 'Email is required'],
unique: true,
},
});
module.exports = mongoose.model('User', userSchema);
๐งช Create a Basic API Endpoint
In routes/userRoutes.js
:
const express = require('express');
const User = require('../models/User');
const router = express.Router();
// Create a new user
router.post('/', async (req, res) => {
try {
const user = await User.create(req.body);
res.status(201).json(user);
} catch (err) {
res.status(400).json({ error: err.message });
}
});
// Get all users
router.get('/', async (req, res) => {
const users = await User.find();
res.status(200).json(users);
});
module.exports = router;
In server.js
, register the route:
const userRoutes = require('./routes/userRoutes');
app.use('/api/users', userRoutes);
๐ Test Your API
You can use HTTPie, Postman, or curl to test.
โ Create a user
http POST :5000/api/users name="John Doe" email="john@example.com"
๐ฅ Get users
http GET :5000/api/users
If it works โ congrats! ๐ You just built your first database-connected API endpoint!
๐ Authentication, Authorization, and Middleware Magic
By now, youโve got a running API that connects to a database, handles user data, and returns responses. Butโฆ any user can access your routes. Thatโs a big security issue. ๐ฑ
In this part, weโll:
- โ Add user authentication using JWT
- โ Protect sensitive routes with authorization
- โ Use middleware to organize our logic
- โ Handle errors globally with a custom error handler
Letโs secure your API like a pro. ๐ก๏ธ
๐ช What is Authentication?
Authentication is the process of confirming who the user is.
Think of it like this:
๐ง โI am John, hereโs my password.โ
๐ API checks the credentials and replies: โโ Youโre authenticated. Hereโs your token.โ
You authenticate once, get a token, and then use that token to access protected resources.
๐ What is Authorization?
Authorization answers: What is this user allowed to do?
After confirming the userโs identity (auth entication), you check what they can access (auth orization).
Example:
- ๐ค John is logged in โ (authenticated)
- ๐ซ But John is not an admin โ (not authorized to delete another user)
๐งพ Using JWTs for Authentication
๐ค Whatโs a JWT?
JWT (JSON Web Token) is a compact, self-contained token that contains user info and is signed by your server.
A sample JWT looks like this:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Itโs like a digital passport ๐ โ it proves who the user is, and can be verified by your server without needing to store sessions.
๐ง Installing Required Packages
Run this to install auth-related packages:
npm install bcryptjs jsonwebtoken cookie-parser
๐๏ธ Step-by-Step: Building the Auth System
1. ๐ง Hashing Passwords
In your User
model (models/User.js
), hash the password before saving:
const bcrypt = require('bcryptjs');
userSchema.pre('save', async function (next) {
if (!this.isModified('password')) return next();
this.password = await bcrypt.hash(this.password, 12);
next();
});
Add a password
field to your schema:
password: {
type: String,
required: [true, 'Password is required'],
minlength: 6,
select: false, // Donโt return password in queries
}
2. ๐ช Signing a JWT
Create a utility to generate JWTs (utils/token.js
):
const jwt = require('jsonwebtoken');
exports.createToken = (userId) => {
return jwt.sign({ id: userId }, process.env.JWT_SECRET, {
expiresIn: '7d',
});
};
Add this to .env
:
JWT_SECRET=supersecuresecret
3. ๐ซ Register and Login Controllers
In controllers/authController.js
:
const User = require('../models/User');
const bcrypt = require('bcryptjs');
const { createToken } = require('../utils/token');
// REGISTER
exports.register = async (req, res) => {
try {
const { name, email, password } = req.body;
const user = await User.create({ name, email, password });
const token = createToken(user._id);
res.status(201).json({ token, user: { name: user.name, email: user.email } });
} catch (err) {
res.status(400).json({ error: err.message });
}
};
// LOGIN
exports.login = async (req, res) => {
const { email, password } = req.body;
const user = await User.findOne({ email }).select('+password');
if (!user || !(await bcrypt.compare(password, user.password))) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const token = createToken(user._id);
res.status(200).json({ token, user: { name: user.name, email: user.email } });
};
4. ๐ Add Auth Routes
In routes/authRoutes.js
:
const express = require('express');
const { register, login } = require('../controllers/authController');
const router = express.Router();
router.post('/register', register);
router.post('/login', login);
module.exports = router;
Add to server.js
:
const authRoutes = require('./routes/authRoutes');
app.use('/api/auth', authRoutes);
๐ก๏ธ Protect Routes with Middleware
Create a file middlewares/authMiddleware.js
:
const jwt = require('jsonwebtoken');
const User = require('../models/User');
exports.protect = async (req, res, next) => {
let token;
if (req.headers.authorization?.startsWith('Bearer')) {
token = req.headers.authorization.split(' ')[1];
}
if (!token) return res.status(401).json({ error: 'Unauthorized' });
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = await User.findById(decoded.id).select('-password');
next();
} catch {
res.status(401).json({ error: 'Token invalid or expired' });
}
};
Now, protect any route like this:
const { protect } = require('../middlewares/authMiddleware');
router.get('/profile', protect, (req, res) => {
res.json({ user: req.user });
});
โ๏ธ Create Global Error Middleware
In middlewares/errorMiddleware.js
:
module.exports = (err, req, res, next) => {
console.error(err.stack);
res.status(err.statusCode || 500).json({
error: err.message || 'Internal Server Error',
});
};
Use it at the bottom of server.js
:
const errorHandler = require('./middlewares/errorMiddleware');
app.use(errorHandler);
โจ Clean Architecture Bonus Tip
Separate your routes, controllers, and models as weโve done so far. It keeps your app modular, clean, and easier to maintain as it grows.
๐งช Testing Auth
๐ Register a User
http POST :5000/api/auth/register name="Alice" email="alice@mail.com" password="password123"
๐ Login
http POST :5000/api/auth/login email="alice@mail.com" password="password123"
๐ Access Protected Route
http GET :5000/api/users/profile "Authorization: Bearer <TOKEN>"
Replace <TOKEN>
with the token you got from login.
๐ฅ And boom โ you now have secure, authenticated routes!
Youโve just implemented a real-world authentication system with JWTs, protected routes using middleware, and improved your architecture with global error handling.
๐งฐ Input Validation, Security Best Practices, and Rate Limiting
At this point, your API is functional and protected with authentication and authorization. But real-world production systems require hardening โ shielding the app from hackers, bad input, and abuse.
Hereโs what weโll cover:
โ
Input validation with [express-validator]
โ
Security headers with [Helmet]
โ
Cross-Origin Resource Sharing (CORS)
โ
Rate limiting to prevent abuse
โ
HTTP parameter pollution protection
โ
Preventing NoSQL injections and XSS attacks
โ
Sanitizing data
โ
Using environment variables securely
Letโs start with validating what users send to your API ๐
๐ฅ 1. Input Validation with express-validator
Why? Because you shouldnโt trust user input. Even if itโs a simple form, a malicious user could inject harmful code.
๐ฆ Install express-validator
npm install express-validator
๐งช Example: Validate Register Route
Update your authRoutes.js
:
const { body } = require('express-validator');
const { register } = require('../controllers/authController');
router.post(
'/register',
[
body('name').not().isEmpty().withMessage('Name is required'),
body('email').isEmail().withMessage('Please provide a valid email'),
body('password').isLength({ min: 6 }).withMessage('Password must be at least 6 characters'),
],
register
);
โ Handle Errors in Controller
In authController.js
, add:
const { validationResult } = require('express-validator');
exports.register = async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
// Continue registration logic...
};
Repeat similar validation for the login and other routes. You now have a strong gatekeeper against bad input! ๐
๐ก๏ธ 2. Secure Headers with helmet
helmet
is a middleware that sets HTTP headers to protect against known web vulnerabilities.
๐ฆ Install Helmet
npm install helmet
๐ Use It in server.js
const helmet = require('helmet');
app.use(helmet());
Thatโs it! Helmet adds security headers like:
X-Content-Type-Options
X-DNS-Prefetch-Control
X-Frame-Options
Strict-Transport-Security
- And more!
๐ 3. Cross-Origin Resource Sharing (CORS)
CORS allows or blocks requests from different origins. Itโs essential when your frontend and backend are hosted separately (e.g., React frontend on Netlify, API on VPS).
๐ฆ Install cors
npm install cors
๐ Use It in server.js
const cors = require('cors');
app.use(cors({
origin: 'https://yourfrontend.com', // Or '*'
credentials: true,
}));
โ This enables the browser to allow secure API access from other domains.
๐ถ 4. Rate Limiting to Prevent Abuse
Avoid brute force attacks and prevent your API from being spammed.
๐ฆ Install express-rate-limit
npm install express-rate-limit
๐ Configure in server.js
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 mins
max: 100, // limit each IP to 100 requests per windowMs
message: 'Too many requests from this IP, please try again later.'
});
app.use(limiter);
๐ This protects your app from being bombarded by repeated requests (DoS attacks, brute force logins, etc.)
๐งผ 5. Data Sanitization (XSS + NoSQL Injection)
Malicious users may try to inject JavaScript or MongoDB commands.
๐ฆ Install xss-clean
and express-mongo-sanitize
npm install xss-clean express-mongo-sanitize
๐ Use in server.js
const xss = require('xss-clean');
const mongoSanitize = require('express-mongo-sanitize');
app.use(xss());
app.use(mongoSanitize());
xss-clean
removes malicious HTML or JavaScriptmongo-sanitize
prevents query injection like{ "$gt": "" }
๐งช Example attack blocked:
POST /login
{
"email": { "$gt": "" },
"password": "123"
}
๐ช๏ธ 6. Prevent HTTP Parameter Pollution
Sometimes attackers send duplicate query params to override security logic.
๐ฆ Install hpp
npm install hpp
๐ Use in server.js
const hpp = require('hpp');
app.use(hpp());
โ
Ensures query string like ?role=user&role=admin
wonโt trick your API.
๐ ๏ธ 7. Environment Variable Safety
Never hardcode credentials in your code! Always use .env
files.
๐ .env
MONGO_URI=your-db-url
JWT_SECRET=supersecret
๐ Use dotenv
in server.js
require('dotenv').config();
Then access with process.env.JWT_SECRET
.
๐ Never push .env
to GitHub! Add it to .gitignore
.
๐ก Bonus Tip: Use npm audit
Run this occasionally to scan your project for vulnerabilities:
npm audit fix
๐ It checks dependencies and auto-fixes known issues.
โ Recap
Hereโs a quick recap of your APIโs new protection:
Feature | Protection |
---|---|
express-validator | Input validation |
helmet | HTTP headers |
cors | Cross-origin access |
express-rate-limit | API abuse protection |
xss-clean | XSS attacks |
mongo-sanitize | NoSQL injection |
hpp | Parameter pollution |
.env | Secured credentials |
Youโre now on your way to building a production-level, battle-hardened API that can handle real-world traffic. โ๏ธ
๐ง Business Logic, Smart Workflows & Email Notifications
By now, your API can create users, manage logins, and handle security. But what about the actual product logic?
Here, weโll build features for:
โ
Subscription creation and management
โ
Business rules for renewals
โ
Scheduled email reminders
โ
Leveraging Upstash Workflows for automation
โ
Using NodeMailer to send emails
โ
Date/time calculations with Day.js
๐ Subscription Tracker Use Case
Weโre building a Subscription Tracker API that helps users track and manage their subscriptions (e.g., Netflix, Spotify, Gym memberships). Core features include:
- Add/Edit/Delete subscriptions
- View renewal dates
- Automatically get email reminders
Letโs roll! ๐น
๐งฑ 1. Subscription Model (MongoDB + Mongoose)
Create a new file: models/Subscription.js
const mongoose = require('mongoose');
const SubscriptionSchema = new mongoose.Schema({
user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true,
},
serviceName: {
type: String,
required: true,
},
renewalDate: {
type: Date,
required: true,
},
price: Number,
autoRenew: {
type: Boolean,
default: false,
},
notes: String,
}, { timestamps: true });
module.exports = mongoose.model('Subscription', SubscriptionSchema);
๐งช 2. Routes and Controllers
๐ routes/subscriptionRoutes.js
const express = require('express');
const { protect } = require('../middleware/authMiddleware');
const {
createSubscription,
getUserSubscriptions,
updateSubscription,
deleteSubscription
} = require('../controllers/subscriptionController');
const router = express.Router();
router.use(protect);
router.post('/', createSubscription);
router.get('/', getUserSubscriptions);
router.put('/:id', updateSubscription);
router.delete('/:id', deleteSubscription);
module.exports = router;
๐ง Business Logic in controllers/subscriptionController.js
const Subscription = require('../models/Subscription');
exports.createSubscription = async (req, res) => {
const { serviceName, renewalDate, price, autoRenew, notes } = req.body;
const newSub = await Subscription.create({
user: req.user.id,
serviceName,
renewalDate,
price,
autoRenew,
notes,
});
res.status(201).json(newSub);
};
exports.getUserSubscriptions = async (req, res) => {
const subs = await Subscription.find({ user: req.user.id });
res.status(200).json(subs);
};
exports.updateSubscription = async (req, res) => {
const updated = await Subscription.findByIdAndUpdate(
req.params.id,
{ ...req.body },
{ new: true }
);
res.status(200).json(updated);
};
exports.deleteSubscription = async (req, res) => {
await Subscription.findByIdAndDelete(req.params.id);
res.status(204).send();
};
๐ Now users can fully manage their subscription data!
๐ 3. Business Logic: Calculate Reminder Dates
Use Day.js to compare renewal dates to today.
๐ฆ Install Day.js
npm install dayjs
Example logic:
const dayjs = require('dayjs');
const isRenewalSoon = (renewalDate) => {
const today = dayjs();
const date = dayjs(renewalDate);
return date.diff(today, 'day') <= 3; // Within 3 days
};
Weโll use this in our workflow to decide when to send reminders. โฐ
๐ 4. Automate with Upstash Workflows
Upstash Workflows lets you run serverless tasks on a schedule, like:
- Sending emails
- Updating subscriptions
- Processing overdue renewals
โ Use Case: Every Day at Midnight
Weโll build a task that:
- Fetches all subscriptions
- Filters ones renewing in 3 days
- Sends a reminder email
๐ง 5. Sending Emails with NodeMailer
๐ฆ Install NodeMailer
npm install nodemailer
๐ ๏ธ Setup Email Utility: utils/email.js
const nodemailer = require('nodemailer');
const sendEmail = async (to, subject, text) => {
const transporter = nodemailer.createTransport({
service: 'Gmail',
auth: {
user: process.env.EMAIL_USER,
pass: process.env.EMAIL_PASS,
},
});
await transporter.sendMail({
from: process.env.EMAIL_USER,
to,
subject,
text,
});
};
module.exports = sendEmail;
๐ง Example Email Reminder Function
const sendReminder = async (subscription, user) => {
const subject = `๐ Subscription Renewal: ${subscription.serviceName}`;
const text = `Hey ${user.name}, your subscription to ${subscription.serviceName} renews on ${subscription.renewalDate.toDateString()}.`;
await sendEmail(user.email, subject, text);
};
๐ Remember to add your email credentials to .env
:
EMAIL_USER=your@gmail.com
EMAIL_PASS=yourAppPassword
Use Gmail โApp Passwordsโ if you have 2FA enabled.
๐ 6. Putting It All Together: Daily Job
Whether youโre using Upstash, cron jobs, or a script triggered by PM2 โ run this daily:
const User = require('../models/User');
const Subscription = require('../models/Subscription');
const dayjs = require('dayjs');
const sendEmail = require('../utils/email');
const sendReminders = async () => {
const subs = await Subscription.find().populate('user');
for (let sub of subs) {
const daysLeft = dayjs(sub.renewalDate).diff(dayjs(), 'day');
if (daysLeft === 3) {
await sendEmail(
sub.user.email,
`Reminder: ${sub.serviceName} renews soon!`,
`Your ${sub.serviceName} subscription renews on ${sub.renewalDate.toDateString()}`
);
}
}
console.log('โ
Reminder job completed');
};
sendReminders();
This logic can live in a file like jobs/sendReminders.js
and be triggered by:
- A cron job
- A scheduled workflow (e.g., Upstash)
- PM2 with
--cron
module
๐งช Test It Out
- Add some subscriptions with renewal dates 3 days from now.
- Run the script manually to simulate a daily job.
- Check your inbox ๐ฌ
๐ฅ Boom! Youโve got automated reminder emails now!
๐งพ Recap: You Built Real Business Features
Feature | Tech Used |
---|---|
Subscription management | Express + MongoDB |
Date calculations | Day.js |
Email notifications | NodeMailer |
Daily automation | Cron/Upstash |
Smart business logic | Renewal date logic |
Youโre now building real-world product features like a pro. ๐ง ๐ผ
๐ Deploying Your Backend API to a VPS (The Real Way)
Learning to deploy isnโt just a cherry on top โ itโs a vital skill that separates tutorial coders from real-world backend engineers. In this part, weโll walk through every step to take your local Express.js API and get it running 24/7 on a production server using:
โ
A VPS (Hostinger or similar)
โ
Ubuntu (Linux) server environment
โ
SSH for remote access
โ
PM2 for process management
โ
Nginx for reverse proxying
โ
GitHub for deployment
โ
Optional: HTTPS with SSL/TLS ๐
Letโs get your server up and running like a pro. ๐ช
โ๏ธ Step 1: Choose a VPS Provider
For this guide, weโll assume youโre using a provider like:
- Hostinger VPS (great for beginners)
- DigitalOcean, Linode, Hetzner, or AWS EC2
Pick a VPS with at least:
- 1 vCPU
- 1 GB RAM
- 20 GB SSD
- Ubuntu 22.04 LTS (recommended OS)
Once provisioned, youโll get:
- An IP address (e.g.
64.21.101.24
) - Root username (usually
root
) - Password or SSH key access
๐ Step 2: Connect to Your VPS with SSH
From your local terminal:
ssh root@your_server_ip
If prompted, confirm the fingerprint and enter your password or provide your SSH key.
๐ง Pro Tip: You can simplify future connections using .ssh/config
:
Host myapi
HostName your_server_ip
User root
IdentityFile ~/.ssh/your_key
Now you can just run ssh myapi
.
๐ Step 3: Update the Server
Before anything else, update everything:
sudo apt update && sudo apt upgrade -y
Also install essential tools:
sudo apt install git curl ufw -y
๐งฐ Step 4: Install Node.js & npm
Use NodeSource (recommended for latest LTS):
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs
Verify it:
node -v
npm -v
๐ Step 5: Install PM2 (Production Process Manager)
npm install -g pm2
PM2 keeps your app running forever โ auto-restarts it on crash, reboot, or deployment. Super useful.
๐งพ Step 6: Clone Your Project from GitHub
From your VPS home directory:
git clone https://github.com/yourusername/your-api-repo.git
cd your-api-repo
If itโs private, set up deploy keys or authenticate with Git.
๐ฆ Step 7: Install Dependencies
npm install
๐ Step 8: Set Up Environment Variables
On your VPS, create a .env
file:
nano .env
Paste in your environment variables (same ones used locally):
PORT=5000
MONGO_URI=your_production_mongo_uri
JWT_SECRET=your_production_jwt
EMAIL_USER=your_pro_email
EMAIL_PASS=your_email_password
๐จ Never commit this file to GitHub!
๐ Step 9: Start Your App with PM2
pm2 start server.js --name backend-api
Make it restart on reboots:
pm2 startup
pm2 save
Check logs if needed:
pm2 logs
โ Your API is running on port 5000!
๐ Step 10: Set Up Nginx Reverse Proxy
Nginx helps direct public traffic to your backend, which is running privately on port 5000.
๐ง Install Nginx:
sudo apt install nginx
โ๏ธ Create Config
sudo nano /etc/nginx/sites-available/backend
Paste this in:
server {
listen 80;
server_name yourdomain.com;
location / {
proxy_pass http://localhost:5000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}
Enable the site:
sudo ln -s /etc/nginx/sites-available/backend /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl restart nginx
๐ Step 11: Optional โ Set Up HTTPS with Letโs Encrypt
Want that secure ๐ green padlock?
Install Certbot:
sudo apt install certbot python3-certbot-nginx
Run:
sudo certbot --nginx -d yourdomain.com
Let it auto-renew:
sudo systemctl enable certbot.timer
๐ก Bonus: UFW Firewall Setup
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable
๐งช Step 12: Test Your Live API
Visit your domain or IP in the browser:
http://yourdomain.com/api/users
Or test with curl
or Postman.
โ If everythingโs working, youโre live, baby!
๐งพ Final Checklist
โ
Server secured
โ
App deployed with Git + PM2
โ
Environment variables in .env
โ
Nginx reverse proxy working
โ
HTTPS setup with Letโs Encrypt
โ
Testing complete
โ
API running 24/7 ๐
๐ Youโre Officially a Backend Developer!
Youโve gone from:
๐ง Learning the web basics
๐ Understanding client-server architecture
๐ Mastering RESTful APIs
๐ Building with MongoDB + Mongoose
๐ Implementing JWT auth and middleware
๐ง Automating logic and workflows
โ๏ธ Deploying like a DevOps engineer
โฆto having a live, production-grade API running on your own VPS. ๐
Thatโs not just a portfolio project โ thatโs a full backend system you built from scratch. And itโs just the beginning. ๐
๐ Whatโs Next?
Hereโs how to keep leveling up:
- ๐งช Add unit + integration testing (Jest + Supertest)
- ๐งฐ Add CI/CD pipelines (GitHub Actions)
- ๐ Add logging + monitoring (Winston, Logtail, Sentry)
- โ๏ธ Build an admin dashboard or connect your frontend
- ๐ Try GraphQL, WebSockets, or gRPC
๐ฌ Final Words
Backend development is the silent powerhouse of the web. Itโs not flashy, but itโs where the magic happens โ data, logic, security, automation, and performance.
This journey has taken you from zero to full-stack backend warrior. ๐ก๏ธ Whether youโre applying for your first job, freelancing, or building your own startup โ this knowledge is gold. ๐ฐ
Thanks for reading. Now go build something epic. ๐
๐ก Found this guide helpful? Bookmark it, share it, or teach it to others. The best way to learn โ is to build and teach. ๐จโ๐ซ๐ฉโ๐ซ