Learn Infotech
February 4, 2025
Nodejs

0 likes


Bulletproof your backend with Express, TypeScript, Zod, Helmet, Cors and bcrypt

Hey dear readers! Are you already a master of JavaScript and backend with express? If so now you can take your skill even to next level bulletproofing your backend with typescript, Validation with Zod and secure it with helmet and much more. I hope you guys are excited to learn something new.

Let's just dive in creating a simple blogs API.

1. Firstly Project Setup

Here are tool and technologies we are going to use and the dependencies we are going to install.

Tools & Libraries:

  • Express: Backend framework.

  • TypeScript: Static typing for safety.

  • Zod: Schema validation.

  • Passport: Authentication middleware.

  • Mongoose: MongoDB ODM (Object Data Modeling).

  • Helmet: Security headers.

  • Rate Limiter: Prevent brute-force attacks.

npm init -y
npm install express typescript zod passport passport-local passport-jwt mongoose bcrypt jsonwebtoken cors helmet express-rate-limit dotenv
npm install -D @types/express @types/node ts-node nodemon @types/passport @types/bcrypt @types/jsonwebtoken

Ok now we have installed all the dependencies we need. Let's just start by creating index.ts file inside of src folder. We are going make couple of folders called config for all the configurations files ,modules for all of our code in a modular format. We are also going to use MVCS(Model,View,Controller and Service) pattern inside of module which will look something like below.

Now before writing a single line of code I would like to discuss .prettierrc, nodemon.json and .env for this application

In .prettierrc we put a small configuration to format our code. Of course you can add your own configuration as per your wish

In nodemon.json we added configuration of development environment how the application should run.

In .env we put necessary environment variable for the application to run.

2. Input Validation with Zod

Validate incoming data (body, params, queries) to prevent malformed or malicious requests.

Example:

// schemas/user.schema.ts
import { z } from "zod";

// User registration schema
export const registerSchema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
  role: z.enum(["user", "admin"]).default("user"),
});

// Middleware to validate requests
import { Request, Response, NextFunction } from "express";
export const validate = (schema: z.ZodSchema) => {
  return (req: Request, res: Response, next: NextFunction) => {
    try {
      schema.parse(req.body); // Validate request body
      next();
    } catch (err) {
      res.status(400).json({ error: err.errors });
    }
  };
};

Usage:

app.post("/register", validate(registerSchema), (req, res) => {
  // Proceed if validation passes
});

3. Authentication with Passport

Why: Secure endpoints with JWT (stateless) or session-based auth.

Step 1: JWT Strategy

// strategies/jwt.strategy.ts
import { Strategy as JwtStrategy, ExtractJwt } from "passport-jwt";
import { User } from "../models/user.model";
import dotenv from "dotenv";
dotenv.config();

const jwtOptions = {
  jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
  secretOrKey: process.env.JWT_SECRET!,
};

export const jwtStrategy = new JwtStrategy(jwtOptions, async (payload, done) => {
  try {
    const user = await User.findById(payload.sub);
    if (user) return done(null, user);
    else return done(null, false);
  } catch (err) {
    return done(err, false);
  }
});

Step 2: Local Strategy (Email/Password)

// strategies/local.strategy.ts
import { Strategy as LocalStrategy } from "passport-local";
import { User } from "../models/user.model";
import bcrypt from "bcrypt";

export const localStrategy = new LocalStrategy(
  { usernameField: "email" },
  async (email, password, done) => {
    try {
      const user = await User.findOne({ email });
      if (!user) return done(null, false);
      
      const isValid = await bcrypt.compare(password, user.password);
      if (!isValid) return done(null, false);
      
      return done(null, user);
    } catch (err) {
      return done(err, false);
    }
  }
);

Step 3: Initialize Passport

// app.ts
import passport from "passport";
import { jwtStrategy, localStrategy } from "./strategies";

passport.use(jwtStrategy);
passport.use(localStrategy);

app.use(passport.initialize());

4. Data Modeling with Mongoose

Why: Enforce data integrity and prevent NoSQL injection.

// models/user.model.ts
import { Schema, model } from "mongoose";
import bcrypt from "bcrypt";

interface User {
  email: string;
  password: string;
  role: "user" | "admin";
}

const userSchema = new Schema<User>({
  email: { type: String, required: true, unique: true },
  password: { type: String, required: true },
  role: { type: String, enum: ["user", "admin"], default: "user" },
});

// Hash password before saving
userSchema.pre("save", async function (next) {
  if (this.isModified("password")) {
    this.password = await bcrypt.hash(this.password, 10);
  }
  next();
});

export const User = model<User>("User", userSchema);

5. Error Handling

Why: Gracefully handle errors and avoid leaking sensitive details.

// middleware/errorHandler.ts
import { Request, Response, NextFunction } from "express";

export const errorHandler = (
  err: Error,
  req: Request,
  res: Response,
  next: NextFunction
) => {
  console.error(err.stack);

  // Handle Zod validation errors
  if (err.name === "ZodError") {
    return res.status(400).json({ error: "Validation failed" });
  }

  // Handle Mongoose errors (e.g., duplicate key)
  if (err.name === "MongoError") {
    return res.status(400).json({ error: "Database error" });
  }

  res.status(500).json({ error: "Internal server error" });
};

6. Security Best Practices

a. HTTPS & Headers

import helmet from "helmet";
import cors from "cors";

app.use(helmet()); // Set secure headers (XSS, CSP, HSTS)
app.use(cors({ origin: ["https://your-frontend.com"] })); // Restrict CORS

b. Rate Limiting

import rateLimit from "express-rate-limit";

const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // Limit each IP to 100 requests per window
});
app.use(limiter);

c. Environment Variables

# .env
JWT_SECRET=your_super_secret_key
MONGO_URI=mongodb://localhost:27017/your_db

7. Putting It All Together

Example: Protected Route

// routes/protected.route.ts
import { Router } from "express";
import passport from "passport";

const router = Router();

router.get(
  "/profile",
  passport.authenticate("jwt", { session: false }),
  (req, res) => {
    res.json({ user: req.user });
  }
);

export default router;

Example: User Registration

// routes/auth.route.ts
import { Router } from "express";
import { User } from "../models/user.model";
import { registerSchema, validate } from "../schemas/user.schema";
import bcrypt from "bcrypt";

const router = Router();

router.post("/register", validate(registerSchema), async (req, res) => {
  try {
    const { email, password } = req.body;
    const existingUser = await User.findOne({ email });
    if (existingUser) return res.status(400).json({ error: "User exists" });

    const user = new User({ email, password });
    await user.save();
    res.status(201).json({ message: "User created" });
  } catch (err) {
    res.status(500).json({ error: "Internal server error" });
  }
});

8. Testing & Monitoring

  • Unit/Integration Tests: Use Jest/Supertest to test endpoints.

  • Logging: Use Winston/Morgan for request logging.

  • Monitoring: Track API health with tools like Prometheus or New Relic.

Common Pitfalls to Avoid

  1. No Input Validation: Always validate incoming data with Zod.

  2. Plaintext Passwords: Use bcrypt to hash passwords.

  3. Hardcoded Secrets: Store secrets in .env files.

  4. Exposed Error Details: Avoid sending stack traces to clients.

  5. Missing Rate Limiting: Prevent brute-force attacks.

Checklist for Bulletproof APIs

  • Input validation with Zod.

  • Authentication with Passport (JWT/local).

  • Password hashing with bcrypt.

  • Secure headers (Helmet).

  • Rate limiting.

  • Environment variables.

  • Error handling middleware.

  • CORS restrictions.

  • Mongoose schema validation.

By combining TypeScript for type safety, Zod for validation, Passport for authentication, Mongoose for data integrity, and Express middleware for security, you’ll create a robust backend that’s resilient to common attacks. 🛡️

Learn Infotech
Managed by Suprince Shakya