How to Secure Your APIs - The Complete Guide

Episode - How to Secure Your APIs - The Complete Guide

Hey everyone! Welcome back to the tutorial series. Today we are going to learn about one of the MOST important topics in backend development - How to Secure Your APIs!

I am super excited about this topic because security is something every developer MUST understand. Trust me, interviewers love asking about API security!

If your API is not secure, hackers can steal data, crash your server, and do some really bad things. So let's make sure YOUR APIs are bulletproof!

What we will cover:

  • Authentication (JWT, OAuth 2.0, Sessions)
  • Authorization (Role-Based & Attribute-Based Access)
  • Rate Limiting (Protect from Abuse)
  • Hiding Sensitive Data
  • Logging & Monitoring
  • Interview Questions
  • Key Points to Remember
Why API Security Matters:
=========================

Without Security:
┌──────────┐                    ┌──────────┐
│  Hacker  │ ──── GET /users ──→│  SERVER  │
│          │ ←── All User Data ─│          │
└──────────┘                    └──────────┘
Anyone can access anything! 😱

With Security:
┌──────────┐                          ┌──────────┐
│  Hacker  │ ──── GET /users ────────→│  SERVER  │
│          │ ←── 401 Unauthorized ────│          │
└──────────┘                          └──────────┘

┌──────────┐                          ┌──────────┐
│  Valid   │ ──── GET /users ────────→│  SERVER  │
│  User    │     + Auth Token         │          │
│          │ ←── User's Own Data ─────│          │
└──────────┘                          └──────────┘
Only authorized users get their own data! ✅

1. Authentication - "Who Are You?"

Authentication is the process of verifying the identity of a user. The server needs to know - are you really who you claim to be?

Think of it like this:

Real World Analogy:
===================

Authentication = Showing your ID card at the office entrance

Guard: "Show me your ID"
You: *shows employee ID card*
Guard: "OK, you are Akshay. You can enter."

The guard VERIFIED your identity!
Same thing happens in APIs.

There are three major ways to implement authentication:

Three Authentication Methods:
==============================

1. JWT (JSON Web Token)    → Stateless, scalable
2. OAuth 2.0               → Third-party login (Google, GitHub)
3. Sessions                → Server-side, traditional

Let's understand each one!

1A) JWT Authentication (JSON Web Token)

JWT is the most popular method for securing APIs today. Let me explain why!

Q: What is JWT?

A: JWT is a compact, self-contained token that securely transmits information between parties as a JSON object. It is digitally signed, so the server can verify it without storing anything in a database!

How JWT Works:
==============

Step 1: User logs in
┌──────────┐                              ┌──────────┐
│  Client  │ ── POST /login ─────────────→│  Server  │
│          │    { email, password }        │          │
└──────────┘                              └──────────┘

Step 2: Server validates credentials & creates JWT
┌──────────┐                              ┌──────────┐
│  Client  │ ←── JWT Token ──────────────│  Server  │
│          │    "eyJhbGciOi..."           │          │
└──────────┘                              └──────────┘

Step 3: Client sends JWT with EVERY request
┌──────────┐                              ┌──────────┐
│  Client  │ ── GET /profile ────────────→│  Server  │
│          │    Header: Bearer eyJhbG...  │          │
│          │ ←── { name: "Akshay" } ─────│          │
└──────────┘                              └──────────┘

Server verifies the token and responds!
No database lookup needed! That's the power of JWT!

Structure of JWT:

A JWT token has three parts separated by dots:

JWT Structure:
==============

eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOiIxMjMifQ.SflKxwRJSMeKKF2QT4fwpM

Part 1: HEADER          (Algorithm + Token Type)
Part 2: PAYLOAD         (User Data / Claims)
Part 3: SIGNATURE       (Verification)

┌─────────────────────────────────────────────────┐
│                    JWT TOKEN                     │
├──────────┬──────────────┬───────────────────────┤
│  HEADER  │   PAYLOAD    │      SIGNATURE        │
│          │              │                       │
│ {        │ {            │ HMACSHA256(           │
│  "alg":  │  "userId":  │   base64(header) +    │
│  "HS256",│  "123",     │   "." +               │
│  "typ":  │  "name":    │   base64(payload),    │
│  "JWT"   │  "Akshay",  │   secret_key          │
│ }        │  "exp": ... │ )                     │
│          │ }            │                       │
├──────────┼──────────────┼───────────────────────┤
│ base64   │   base64     │     base64            │
│ encoded  │   encoded    │     encoded           │
└──────────┴──────────────┴───────────────────────┘

Let's implement JWT in Node.js:

// Install: npm install jsonwebtoken

const jwt = require("jsonwebtoken");

const SECRET_KEY = "myTopSecretKey123"; // Keep this in .env file!

// ===========================
// STEP 1: Generate JWT Token
// ===========================

app.post("/login", async (req, res) => {
    const { email, password } = req.body;

    // Validate user from database
    const user = await User.findOne({ email });

    if (!user) {
        return res.status(401).json({ error: "Invalid credentials" });
    }

    // Compare password (use bcrypt!)
    const isMatch = await bcrypt.compare(password, user.password);

    if (!isMatch) {
        return res.status(401).json({ error: "Invalid credentials" });
    }

    // Create JWT token
    const token = jwt.sign(
        { userId: user._id, email: user.email },  // Payload
        SECRET_KEY,                                 // Secret key
        { expiresIn: "1h" }                        // Expires in 1 hour
    );

    // Send token to client (via cookie or response body)
    res.cookie("token", token, { httpOnly: true });
    res.json({ message: "Login successful!", token });
});
// ===========================
// STEP 2: Auth Middleware
// ===========================

const authMiddleware = (req, res, next) => {
    // Get token from cookie or header
    const token = req.cookies.token
        || req.headers.authorization?.split(" ")[1];

    if (!token) {
        return res.status(401).json({ error: "No token provided!" });
    }

    try {
        // Verify the token
        const decoded = jwt.verify(token, SECRET_KEY);

        // Attach user info to request
        req.user = decoded;

        next(); // Move to next middleware/route
    } catch (err) {
        return res.status(401).json({ error: "Invalid or expired token!" });
    }
};
// ===========================
// STEP 3: Protect Routes
// ===========================

// This route is PROTECTED!
app.get("/profile", authMiddleware, (req, res) => {
    res.json({
        message: "Welcome!",
        userId: req.user.userId,
        email: req.user.email
    });
});

// Without valid token:
// → 401 Unauthorized

// With valid token:
// → { message: "Welcome!", userId: "123", email: "akshay@gmail.com" }

1B) OAuth 2.0 - "Login with Google/GitHub"

Ever clicked "Sign in with Google"? That's OAuth 2.0!

Q: What is OAuth 2.0?

A: OAuth 2.0 is an authorization framework that allows a user to grant a third-party application limited access to their resources on another service, without sharing their password!

OAuth 2.0 Flow:
===============

You want to login to "myapp.com" using Google:

Step 1: User clicks "Login with Google"
┌──────────┐                              ┌──────────┐
│  myapp   │ ── Redirect to Google ──────→│  Google  │
│  .com    │                              │  Auth    │
└──────────┘                              └──────────┘

Step 2: User logs in with Google & grants permission
┌──────────┐                              ┌──────────┐
│  User    │ ── Enter Google credentials ─→│  Google  │
│          │ ── "Allow myapp access?" ────→│  Auth    │
│          │    ✅ Yes!                    │          │
└──────────┘                              └──────────┘

Step 3: Google sends Authorization Code to myapp
┌──────────┐                              ┌──────────┐
│  myapp   │ ←── Authorization Code ──────│  Google  │
│  .com    │                              │  Auth    │
└──────────┘                              └──────────┘

Step 4: myapp exchanges code for Access Token
┌──────────┐                              ┌──────────┐
│  myapp   │ ── Code + Client Secret ────→│  Google  │
│  .com    │ ←── Access Token ────────────│  API     │
└──────────┘                              └──────────┘

Step 5: myapp uses token to get user info
┌──────────┐                              ┌──────────┐
│  myapp   │ ── GET /userinfo ───────────→│  Google  │
│  .com    │    + Access Token            │  API     │
│          │ ←── { name, email, photo } ──│          │
└──────────┘                              └──────────┘

User is logged in without sharing Google password!

Implementation with Passport.js:

// Install: npm install passport passport-google-oauth20

const passport = require("passport");
const GoogleStrategy = require("passport-google-oauth20").Strategy;

// Configure Google Strategy
passport.use(new GoogleStrategy({
    clientID: process.env.GOOGLE_CLIENT_ID,
    clientSecret: process.env.GOOGLE_CLIENT_SECRET,
    callbackURL: "/auth/google/callback"
},
    async (accessToken, refreshToken, profile, done) => {
        // Find or create user in your database
        let user = await User.findOne({ googleId: profile.id });

        if (!user) {
            user = await User.create({
                googleId: profile.id,
                name: profile.displayName,
                email: profile.emails[0].value
            });
        }

        done(null, user);
    }
));

// Routes
app.get("/auth/google",
    passport.authenticate("google", { scope: ["profile", "email"] })
);

app.get("/auth/google/callback",
    passport.authenticate("google"),
    (req, res) => {
        // Generate JWT for the authenticated user
        const token = jwt.sign({ userId: req.user._id }, SECRET_KEY);
        res.cookie("token", token, { httpOnly: true });
        res.redirect("/dashboard");
    }
);

1C) Session-Based Authentication

Sessions are the traditional way of handling authentication. The server stores session data on its side!

Session-Based Auth Flow:
========================

Step 1: User logs in
┌──────────┐                              ┌──────────┐
│  Client  │ ── POST /login ─────────────→│  Server  │
│          │    { email, password }        │          │
└──────────┘                              └──────────┘

Step 2: Server creates a session & sends Session ID
┌──────────┐                              ┌──────────────┐
│  Client  │ ←── Set-Cookie: sid=abc123 ──│  Server      │
│          │                              │              │
│  Stores  │                              │  Session     │
│  cookie  │                              │  Store:      │
│          │                              │  abc123 → {  │
│          │                              │   userId: 1, │
│          │                              │   name: "AK" │
│          │                              │  }           │
└──────────┘                              └──────────────┘

Step 3: Client sends Session ID with every request
┌──────────┐                              ┌──────────────┐
│  Client  │ ── GET /profile ────────────→│  Server      │
│          │    Cookie: sid=abc123        │              │
│          │                              │  Looks up    │
│          │ ←── { name: "AK" } ─────────│  abc123 in   │
│          │                              │  session     │
│          │                              │  store       │
└──────────┘                              └──────────────┘

JWT vs Sessions - When to use what?

Feature JWT Sessions
Storage Client-side (token) Server-side (session store)
Stateless? Yes - server stores nothing No - server stores session data
Scalability Excellent (no shared state) Harder (need shared session store)
Revocation Difficult (token valid until expiry) Easy (delete session from store)
Best For APIs, microservices, mobile apps Traditional web apps, monoliths
Used By Most modern APIs Banks, government portals
Quick Summary - Authentication Methods:
=======================================

┌───────────────┬──────────────────────────────────────────┐
│  JWT          │  Self-contained token, stateless,        │
│               │  best for APIs and microservices          │
├───────────────┼──────────────────────────────────────────┤
│  OAuth 2.0    │  Third-party login (Google, GitHub),     │
│               │  user never shares password with you      │
├───────────────┼──────────────────────────────────────────┤
│  Sessions     │  Server stores state, easy revocation,   │
│               │  best for traditional web apps            │
└───────────────┴──────────────────────────────────────────┘

2. Authorization - "What Can You Do?"

Wait! Authentication and Authorization are NOT the same thing! This is a very common interview question!

Authentication vs Authorization:
================================

Authentication = "WHO are you?"     → Verifying identity
Authorization  = "WHAT can you do?" → Verifying permissions

Real World Example:
===================

Authentication: You show your employee ID at the office gate
               → Guard knows you are "Akshay, Software Engineer"

Authorization:  You try to enter the server room
               → Guard says "Sorry, only Admins can enter!"
               → You are AUTHENTICATED but NOT AUTHORIZED!

MIND BLOWN, right? You can be authenticated (identity verified) but still NOT authorized (no permission)!

Role-Based Access Control (RBAC)

RBAC is the most common authorization pattern. Users are assigned roles, and each role has specific permissions!

RBAC Example:
=============

┌───────────────────────────────────────────────────────┐
│                    ROLES & PERMISSIONS                  │
├──────────┬─────────┬───────────┬──────────┬───────────┤
│ Endpoint │  Admin  │  Manager  │  User    │  Guest    │
├──────────┼─────────┼───────────┼──────────┼───────────┤
│ GET      │   ✅    │    ✅     │   ✅     │    ✅     │
│ /posts   │         │           │          │           │
├──────────┼─────────┼───────────┼──────────┼───────────┤
│ POST     │   ✅    │    ✅     │   ✅     │    ❌     │
│ /posts   │         │           │          │           │
├──────────┼─────────┼───────────┼──────────┼───────────┤
│ DELETE   │   ✅    │    ✅     │   ❌     │    ❌     │
│ /posts   │         │           │          │           │
├──────────┼─────────┼───────────┼──────────┼───────────┤
│ GET      │   ✅    │    ❌     │   ❌     │    ❌     │
│ /admin   │         │           │          │           │
└──────────┴─────────┴───────────┴──────────┴───────────┘

Implementation:

// ===========================
// Authorization Middleware
// ===========================

const authorize = (...allowedRoles) => {
    return (req, res, next) => {
        // req.user is set by authMiddleware (from JWT)
        const userRole = req.user.role;

        if (!allowedRoles.includes(userRole)) {
            return res.status(403).json({
                error: "Forbidden! You don't have permission."
            });
        }

        next();
    };
};
// ===========================
// Using Authorization
// ===========================

// Anyone authenticated can view posts
app.get("/api/posts",
    authMiddleware,
    (req, res) => {
        res.json({ posts: [...] });
    }
);

// Only Admin and Manager can delete posts
app.delete("/api/posts/:id",
    authMiddleware,
    authorize("admin", "manager"),
    (req, res) => {
        // Delete post logic
        res.json({ message: "Post deleted!" });
    }
);

// Only Admin can access admin panel
app.get("/api/admin/dashboard",
    authMiddleware,
    authorize("admin"),
    (req, res) => {
        res.json({ stats: {...} });
    }
);
OUTPUT:
=======

User (role: "user") → DELETE /api/posts/1
→ 403 Forbidden! You don't have permission.

Admin (role: "admin") → DELETE /api/posts/1
→ 200 { message: "Post deleted!" }

Attribute-Based Access Control (ABAC)

ABAC is more flexible than RBAC. It uses attributes (properties of the user, resource, or environment) to make access decisions!

ABAC Example:
=============

"A user can EDIT a post ONLY IF they are the OWNER of that post"

const authorizeOwner = async (req, res, next) => {
    const post = await Post.findById(req.params.id);

    if (!post) {
        return res.status(404).json({ error: "Post not found" });
    }

    // Check if the requesting user is the owner
    if (post.authorId.toString() !== req.user.userId) {
        return res.status(403).json({
            error: "You can only edit your own posts!"
        });
    }

    next();
};

// Usage
app.put("/api/posts/:id",
    authMiddleware,
    authorizeOwner,
    updatePostHandler
);
RBAC vs ABAC:
=============

RBAC:
  "Only admins can delete posts"
  → Simple, role-based rules
  → Good for most applications

ABAC:
  "Users can edit their OWN posts,
   Managers can edit posts in THEIR department,
   Admins can edit ANY post"
  → Fine-grained, attribute-based rules
  → Good for complex permission systems

3. Rate Limiting - "Slow Down!"

Rate limiting is about controlling how many requests a user can make in a given time period. This protects your API from abuse, brute force attacks, and DDoS!

Q: Why do we need rate limiting?

Without Rate Limiting:
======================

Attacker:
┌──────────┐
│  Hacker  │ ── 1000 requests/second ──→ ┌──────────┐
│          │ ── POST /login              │  SERVER  │
│          │ ── { password: "aaa" }      │          │
│          │ ── { password: "aab" }      │  💀 Dead │
│          │ ── { password: "aac" }      │  Server  │
│          │ ── ... (brute force!)       │  crashes │
└──────────┘                             └──────────┘

Problems:
1. Brute force password attacks
2. Server overload (DDoS)
3. Resource exhaustion
4. Huge cloud bills! 💸


With Rate Limiting:
===================

┌──────────┐                              ┌──────────┐
│  Hacker  │ ── Request 1  ──────────────→│  SERVER  │
│          │ ── Request 2  ──────────────→│          │
│          │ ── Request 3  ──────────────→│  ✅ OK   │
│          │ ── ...                       │          │
│          │ ── Request 100 ─────────────→│          │
│          │ ── Request 101 ─────────────→│          │
│          │ ←── 429 Too Many Requests ──│  🛑 Stop │
└──────────┘                              └──────────┘

Server is protected!

Implementation with express-rate-limit:

// Install: npm install express-rate-limit

const rateLimit = require("express-rate-limit");

// ===========================
// Global Rate Limiter
// ===========================

const globalLimiter = rateLimit({
    windowMs: 15 * 60 * 1000,  // 15 minutes
    max: 100,                   // Max 100 requests per window
    message: {
        error: "Too many requests! Please try again after 15 minutes."
    },
    standardHeaders: true,      // Return rate limit info in headers
    legacyHeaders: false,
});

// Apply to ALL routes
app.use(globalLimiter);
// ===========================
// Strict Limiter for Login
// ===========================

const loginLimiter = rateLimit({
    windowMs: 15 * 60 * 1000,  // 15 minutes
    max: 5,                     // Only 5 login attempts!
    message: {
        error: "Too many login attempts! Try again after 15 minutes."
    },
});

// Apply ONLY to login route
app.post("/login", loginLimiter, loginHandler);
// ===========================
// API-Specific Limiter
// ===========================

const apiLimiter = rateLimit({
    windowMs: 1 * 60 * 1000,  // 1 minute
    max: 30,                    // 30 requests per minute
    message: {
        error: "API rate limit exceeded."
    },
});

// Apply to API routes
app.use("/api/", apiLimiter);
OUTPUT (Response Headers):
==========================

HTTP/1.1 200 OK
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1708683600

// After exceeding limit:
HTTP/1.1 429 Too Many Requests
{
    "error": "Too many requests! Please try again after 15 minutes."
}

Rate Limiting Strategies:

Strategy How It Works Best For
Fixed Window Count requests in fixed time window (e.g., 100 req per 15 min) Simple APIs
Sliding Window Rolling time window, smoother distribution Production APIs
Token Bucket Tokens refill at fixed rate, each request consumes a token Burst-friendly APIs
IP-Based Limit per IP address Public APIs
User-Based Limit per authenticated user Authenticated APIs

4. Hiding Sensitive Data - "Don't Leak Secrets!"

This is VERY important! You should NEVER expose sensitive data in your API responses, logs, or code!

What Counts as Sensitive Data:
==============================

1. Passwords (NEVER return in API response!)
2. API Keys / Secret Keys
3. Database connection strings
4. Credit card numbers
5. Social security numbers
6. Internal server errors / stack traces
7. Database IDs in some cases
8. Environment variables

4A) Never Return Passwords in API Response

// ❌ BAD - Returning password in response!
app.get("/api/user/:id", async (req, res) => {
    const user = await User.findById(req.params.id);
    res.json(user);
});

// Response includes password!
// {
//     "_id": "123",
//     "name": "Akshay",
//     "email": "akshay@gmail.com",
//     "password": "$2b$10$hashedPasswordHere..."  ← DANGEROUS!
// }


// ✅ GOOD - Exclude password from response!

// Method 1: Select specific fields
app.get("/api/user/:id", async (req, res) => {
    const user = await User.findById(req.params.id)
        .select("-password");  // Exclude password!
    res.json(user);
});

// Method 2: Use toJSON transform in Schema
const userSchema = new mongoose.Schema({
    name: String,
    email: String,
    password: String
});

userSchema.methods.toJSON = function () {
    const user = this.toObject();
    delete user.password;  // Remove password before sending!
    return user;
};

// Response is clean!
// {
//     "_id": "123",
//     "name": "Akshay",
//     "email": "akshay@gmail.com"
// }

4B) Use Environment Variables for Secrets

// ❌ BAD - Hardcoded secrets!
const SECRET_KEY = "mySuperSecretKey123";
const DB_URL = "mongodb+srv://admin:password123@cluster.mongodb.net";
const API_KEY = "sk-abc123xyz456";


// ✅ GOOD - Use .env file!

// .env file (NEVER commit to Git!)
SECRET_KEY=mySuperSecretKey123
DATABASE_URL=mongodb+srv://admin:password123@cluster.mongodb.net
OPENAI_API_KEY=sk-abc123xyz456

// In your code:
require("dotenv").config();

const SECRET_KEY = process.env.SECRET_KEY;
const DB_URL = process.env.DATABASE_URL;
const API_KEY = process.env.OPENAI_API_KEY;
// .gitignore - ALWAYS add .env!

node_modules/
.env           ← NEVER push secrets to GitHub!
.env.local
.env.production

4C) Don't Expose Internal Errors to Users

// ❌ BAD - Exposing stack trace!
app.get("/api/data", async (req, res) => {
    try {
        const data = await fetchData();
        res.json(data);
    } catch (err) {
        res.status(500).json({
            error: err.message,
            stack: err.stack  // ← Hacker sees your file paths!
        });
    }
});

// Response:
// {
//     "error": "Cannot read property 'id' of undefined",
//     "stack": "TypeError: Cannot read property 'id'
//              at /home/user/myapp/src/controllers/data.js:42:15
//              at /home/user/myapp/node_modules/express/..."
// }
// Hacker now knows your file structure! 😱


// ✅ GOOD - Generic error message to client, detailed log internally!
app.get("/api/data", async (req, res) => {
    try {
        const data = await fetchData();
        res.json(data);
    } catch (err) {
        // Log detailed error internally
        console.error("Error in /api/data:", err);

        // Send generic message to client
        res.status(500).json({
            error: "Something went wrong. Please try again later."
        });
    }
});

4D) Helmet.js - Secure HTTP Headers

// Install: npm install helmet

const helmet = require("helmet");

// Add security headers with ONE line!
app.use(helmet());
What Helmet Does:
=================

Without Helmet:
  X-Powered-By: Express  ← Tells attackers you use Express!

With Helmet:
  X-Powered-By: removed!
  X-Content-Type-Options: nosniff
  X-Frame-Options: DENY
  Strict-Transport-Security: max-age=...
  Content-Security-Policy: ...
  X-XSS-Protection: 1; mode=block

One line of code, multiple layers of protection!

4E) Use HTTPS, Not HTTP

HTTP vs HTTPS:
==============

HTTP (Not Secure):
┌──────────┐                              ┌──────────┐
│  Client  │ ── password: "abc123" ──────→│  Server  │
│          │                              │          │
└──────────┘                              └──────────┘
                    ↑
              ┌──────────┐
              │  Hacker  │  Can read password!
              │  (MITM)  │  Data is in plain text!
              └──────────┘

HTTPS (Secure):
┌──────────┐                              ┌──────────┐
│  Client  │ ── &$#@*&!encrypted!@#$ ───→│  Server  │
│          │                              │          │
└──────────┘                              └──────────┘
                    ↑
              ┌──────────┐
              │  Hacker  │  Can't read anything!
              │  (MITM)  │  Data is encrypted!
              └──────────┘

ALWAYS use HTTPS in production!

5. Logging & Monitoring - "Know What's Happening!"

If your API gets attacked and you have no logs, you won't even know something happened! Logging and monitoring are your eyes and ears!

Why Logging Matters:
====================

Without Logging:
  Hacker: *Steals 10,000 user records*
  You: "Everything seems fine!" 😊
  3 months later: "Wait... where did all the data go?" 😱

With Logging:
  Hacker: *Attempts to steal data*
  Log: "⚠️ 500 failed login attempts from IP 192.168.1.100"
  Log: "⚠️ Unusual GET /api/users bulk request"
  Alert: "🚨 Possible attack detected!"
  You: *Blocks IP immediately* 💪

5A) Request Logging with Morgan

// Install: npm install morgan

const morgan = require("morgan");

// Log every HTTP request
app.use(morgan("combined"));
OUTPUT (Morgan Logs):
=====================

::1 - - [23/Feb/2026:10:15:30 +0000] "GET /api/users HTTP/1.1" 200 1234
::1 - - [23/Feb/2026:10:15:32 +0000] "POST /login HTTP/1.1" 401 52
::1 - - [23/Feb/2026:10:15:33 +0000] "POST /login HTTP/1.1" 401 52
::1 - - [23/Feb/2026:10:15:34 +0000] "POST /login HTTP/1.1" 401 52
192.168.1.100 - [23/Feb/2026:10:15:35 +0000] "POST /login HTTP/1.1" 429 65

← 3 failed logins in 3 seconds? Suspicious! 🚨

5B) Application Logging with Winston

// Install: npm install winston

const winston = require("winston");

// Create logger
const logger = winston.createLogger({
    level: "info",
    format: winston.format.combine(
        winston.format.timestamp(),
        winston.format.json()
    ),
    transports: [
        // Console output (for development)
        new winston.transports.Console(),

        // File output (for production)
        new winston.transports.File({
            filename: "logs/error.log",
            level: "error"
        }),
        new winston.transports.File({
            filename: "logs/combined.log"
        })
    ]
});
// ===========================
// Using Winston in Your App
// ===========================

// Log successful actions
logger.info("User logged in", {
    userId: user._id,
    email: user.email,
    ip: req.ip
});

// Log warnings
logger.warn("Multiple failed login attempts", {
    email: req.body.email,
    ip: req.ip,
    attempts: 5
});

// Log errors
logger.error("Database connection failed", {
    error: err.message,
    stack: err.stack  // OK to log internally!
});
OUTPUT (logs/combined.log):
===========================

{"level":"info","message":"User logged in","userId":"123","email":"akshay@gmail.com","ip":"::1","timestamp":"2026-02-23T10:15:30.000Z"}
{"level":"warn","message":"Multiple failed login attempts","email":"hack@evil.com","ip":"192.168.1.100","attempts":5,"timestamp":"2026-02-23T10:15:35.000Z"}
{"level":"error","message":"Database connection failed","error":"ECONNREFUSED","timestamp":"2026-02-23T10:16:00.000Z"}

5C) What to Log (and What NOT to Log)

✅ WHAT TO LOG:
===============
- Authentication events (login, logout, failed attempts)
- Authorization failures (403 Forbidden)
- Rate limit hits (429 Too Many Requests)
- Server errors (500 Internal Server Error)
- API response times (slow queries)
- Suspicious activity patterns
- Database connection events

❌ WHAT NOT TO LOG:
===================
- Passwords (even hashed ones!)
- Credit card numbers
- API keys / Secret keys
- Personal health information
- Full request bodies with sensitive data
- Session tokens

5D) Monitoring - Real-Time Visibility

Logging saves data. Monitoring makes it actionable!

Popular Monitoring Tools:
=========================

┌───────────────┬──────────────────────────────────────────┐
│  Tool         │  Purpose                                 │
├───────────────┼──────────────────────────────────────────┤
│  Prometheus   │  Metrics collection (request count,      │
│  + Grafana    │  response time, error rate)              │
├───────────────┼──────────────────────────────────────────┤
│  ELK Stack    │  Elasticsearch + Logstash + Kibana       │
│               │  Log aggregation and visualization       │
├───────────────┼──────────────────────────────────────────┤
│  Datadog      │  All-in-one monitoring platform          │
│               │  (logs, metrics, traces)                 │
├───────────────┼──────────────────────────────────────────┤
│  Sentry       │  Error tracking and alerting             │
│               │  Captures exceptions in real-time        │
├───────────────┼──────────────────────────────────────────┤
│  AWS           │  CloudWatch for AWS-hosted apps          │
│  CloudWatch   │  Logs, metrics, alarms                   │
└───────────────┴──────────────────────────────────────────┘

Health Check Endpoint:

// Simple health check - monitors can ping this!

app.get("/health", (req, res) => {
    res.status(200).json({
        status: "healthy",
        uptime: process.uptime(),
        timestamp: new Date().toISOString(),
        memoryUsage: process.memoryUsage().heapUsed,
        version: "1.0.0"
    });
});
OUTPUT:
=======

{
    "status": "healthy",
    "uptime": 86400,
    "timestamp": "2026-02-23T10:00:00.000Z",
    "memoryUsage": 52428800,
    "version": "1.0.0"
}

// Monitoring tool pings /health every 30 seconds
// If it fails → Alert triggered! 🚨

The Complete API Security Checklist

API Security Layers:
====================

                    ┌─────────────────────┐
                    │    INCOMING REQUEST  │
                    └──────────┬──────────┘
                               │
                               ▼
               ┌───────────────────────────────┐
    Layer 1:   │         HTTPS / TLS           │  Encrypt data in transit
               └───────────────┬───────────────┘
                               │
                               ▼
               ┌───────────────────────────────┐
    Layer 2:   │        RATE LIMITING          │  Block excessive requests
               └───────────────┬───────────────┘
                               │
                               ▼
               ┌───────────────────────────────┐
    Layer 3:   │       HELMET (Headers)        │  Secure HTTP headers
               └───────────────┬───────────────┘
                               │
                               ▼
               ┌───────────────────────────────┐
    Layer 4:   │      AUTHENTICATION           │  Verify identity (JWT)
               └───────────────┬───────────────┘
                               │
                               ▼
               ┌───────────────────────────────┐
    Layer 5:   │      AUTHORIZATION            │  Check permissions (RBAC)
               └───────────────┬───────────────┘
                               │
                               ▼
               ┌───────────────────────────────┐
    Layer 6:   │      INPUT VALIDATION         │  Sanitize user input
               └───────────────┬───────────────┘
                               │
                               ▼
               ┌───────────────────────────────┐
    Layer 7:   │    LOGGING & MONITORING       │  Track everything
               └───────────────┬───────────────┘
                               │
                               ▼
                    ┌─────────────────────┐
                    │   YOUR API LOGIC    │  Safe and protected!
                    └─────────────────────┘

Every request passes through ALL these layers before reaching your actual business logic. That's how you build a secure API!

Putting It All Together - Complete Express Setup

// app.js - Complete Secure Express Setup

const express = require("express");
const helmet = require("helmet");
const morgan = require("morgan");
const rateLimit = require("express-rate-limit");
const cookieParser = require("cookie-parser");
require("dotenv").config();

const app = express();

// ===========================
// Layer 1: Security Headers
// ===========================
app.use(helmet());

// ===========================
// Layer 2: Rate Limiting
// ===========================
const limiter = rateLimit({
    windowMs: 15 * 60 * 1000,
    max: 100,
    message: { error: "Too many requests!" }
});
app.use(limiter);

// ===========================
// Layer 3: Request Logging
// ===========================
app.use(morgan("combined"));

// ===========================
// Layer 4: Body Parsing
// ===========================
app.use(express.json({ limit: "10kb" }));  // Limit body size!
app.use(cookieParser());

// ===========================
// Layer 5: Routes
// ===========================

// Public routes (no auth needed)
app.post("/login", loginLimiter, loginHandler);
app.post("/signup", signupHandler);

// Protected routes (auth + authorization)
app.get("/api/profile", authMiddleware, getProfile);
app.delete("/api/users/:id", authMiddleware, authorize("admin"), deleteUser);

// Health check
app.get("/health", (req, res) => {
    res.json({ status: "healthy" });
});

// ===========================
// Layer 6: Error Handler
// ===========================
app.use((err, req, res, next) => {
    logger.error(err.message, { stack: err.stack });
    res.status(500).json({ error: "Something went wrong!" });
});

app.listen(3000, () => {
    console.log("Secure server running on port 3000!");
});
OUTPUT:
=======

Secure server running on port 3000!

// Every request now passes through:
// ✅ Helmet (security headers)
// ✅ Rate Limiter (abuse protection)
// ✅ Morgan (request logging)
// ✅ Body size limit (payload protection)
// ✅ Auth Middleware (identity verification)
// ✅ Authorization (permission check)
// ✅ Error Handler (no info leaks)

Interview Questions - Quick Fire!

Q: What is the difference between Authentication and Authorization?

"Authentication verifies WHO you are (identity), while Authorization verifies WHAT you can do (permissions). Authentication comes first - you must know who someone is before you can decide what they're allowed to do."

Q: What is JWT and how does it work?

"JWT (JSON Web Token) is a compact, self-contained token with three parts - Header, Payload, and Signature. The server creates a JWT on login, the client stores it and sends it with every request. The server verifies the signature without needing a database lookup, making it stateless and scalable."

Q: JWT vs Sessions - when to use which?

"Use JWT for stateless APIs, microservices, and mobile apps where scalability matters. Use Sessions for traditional web apps where you need easy token revocation - like banking apps where you want to force logout a user immediately."

Q: What is OAuth 2.0?

"OAuth 2.0 is an authorization framework that allows users to grant third-party applications limited access to their resources without sharing their password. For example, 'Login with Google' uses OAuth 2.0 - the user authenticates with Google, and Google provides an access token to the third-party app."

Q: What is Rate Limiting and why is it important?

"Rate limiting controls how many requests a client can make in a given time period. It protects against brute force attacks, DDoS, server overload, and excessive API usage. Common strategies include fixed window, sliding window, and token bucket algorithms."

Q: What is RBAC?

"RBAC stands for Role-Based Access Control. Users are assigned roles (like admin, manager, user), and each role has specific permissions. For example, only admins can delete users, while regular users can only view their own profile."

Q: How do you hide sensitive data in APIs?

"Never return passwords in API responses - use select('-password') or toJSON transforms. Store secrets in environment variables, not in code. Use Helmet.js for secure headers. Never expose stack traces to clients. Always use HTTPS in production."

Q: Why is logging important for API security?

"Logging provides visibility into what's happening in your system. It helps detect attacks (like brute force login attempts), troubleshoot issues, and maintain audit trails. Tools like Winston for application logs and Morgan for HTTP request logs are commonly used in Node.js."

Q: What is Helmet.js?

"Helmet.js is a middleware that sets various HTTP security headers automatically. It removes the X-Powered-By header, adds Content-Security-Policy, X-Frame-Options, Strict-Transport-Security, and more - all with a single line of code."

Q: What sensitive data should you NEVER log?

"Never log passwords, API keys, credit card numbers, session tokens, or personal health information. You should log authentication events, authorization failures, rate limit hits, and server errors - but always sanitize sensitive fields first."

Quick Recap

Security Layer What It Does Tools
Authentication Verifies WHO you are JWT, OAuth 2.0, Sessions
Authorization Verifies WHAT you can do RBAC, ABAC, Middleware
Rate Limiting Controls request frequency express-rate-limit
Hide Sensitive Data Protects secrets and PII .env, Helmet.js, HTTPS
Logging & Monitoring Tracks activity and detects attacks Morgan, Winston, Sentry

Key Points to Remember

  • Authentication = "Who are you?" | Authorization = "What can you do?"
  • JWT = Stateless, self-contained token (Header + Payload + Signature)
  • OAuth 2.0 = Third-party login without sharing passwords
  • Sessions = Server-side state, easy revocation, traditional apps
  • RBAC = Role-based permissions (admin, manager, user)
  • ABAC = Attribute-based permissions (owner can edit their own posts)
  • Rate Limiting = Protect against brute force and DDoS attacks
  • Never return passwords in API responses - use select("-password")
  • Use .env files for secrets - never hardcode them!
  • Helmet.js = One line, multiple security headers
  • HTTPS = Always use in production - encrypts data in transit
  • Morgan = HTTP request logging | Winston = Application logging
  • Never log passwords, API keys, or credit card numbers
  • Health check endpoint = /health for monitoring tools to ping
  • Security is layered - no single measure is enough!

What's Next?

Now you understand how to secure your APIs! In the next episode, we can explore:

  • Input Validation and Sanitization (Preventing SQL Injection, XSS)
  • CORS (Cross-Origin Resource Sharing)
  • API Versioning Strategies
  • Deploying Secure APIs to Production

Keep coding, keep learning! See you in the next one!