🚀 The Ultimate Guide to Building and Deploying a Production-Ready Backend API

Table of Contents

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:

  1. A client (your browser or app) sends a request to a server.
  2. The server, a powerful machine somewhere in the world, processes this request.
  3. 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:

PartDescription
HTTP MethodAction to be performed (GET, POST, PUT, DELETE)
EndpointThe URL of the resource (e.g., /api/users)
HeadersMetadata (e.g., Content-Type, Authorization)
BodyData 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": "[email protected]"
}

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:

LanguageFramework
JavaScriptExpress.js, Fastify
PythonDjango, Flask
RubyRails
JavaSpring 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": "[email protected]",
  "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:[email protected]/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="[email protected]"

📥 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="[email protected]" password="password123"

🔑 Login

http POST :5000/api/auth/login email="[email protected]" 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 JavaScript
  • mongo-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:

FeatureProtection
express-validatorInput validation
helmetHTTP headers
corsCross-origin access
express-rate-limitAPI abuse protection
xss-cleanXSS attacks
mongo-sanitizeNoSQL injection
hppParameter pollution
.envSecured 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:

  1. Fetches all subscriptions
  2. Filters ones renewing in 3 days
  3. 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 protected]
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

  1. Add some subscriptions with renewal dates 3 days from now.
  2. Run the script manually to simulate a daily job.
  3. Check your inbox 📬

💥 Boom! You’ve got automated reminder emails now!


🧾 Recap: You Built Real Business Features

FeatureTech Used
Subscription managementExpress + MongoDB
Date calculationsDay.js
Email notificationsNodeMailer
Daily automationCron/Upstash
Smart business logicRenewal 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. 👨‍🏫👩‍🏫

Share the Post:
Picture of Web Codder

Web Codder

Vikas Sankhla is a seasoned Full Stack Developer with over 7 years of experience in web development. He is the founder of Web Codder, a platform dedicated to providing comprehensive web development tutorials and resources. Vikas specializes in the MERN stack (MongoDB, Express.js, React.js, Node.js) and has been instrumental in mentoring aspiring developers through his online courses and content. His commitment to simplifying complex web technologies has made him a respected figure in the developer community.

Related Posts