Skip to content

Express.js Backend Guide#

A practical guide to building production-ready Express.js applications with TypeScript.

🎯 What You'll Learn#

  • Server Architecture - Structured Express app with middleware
  • Error Handling - Centralized error handling with proper logging
  • Authentication - Protect routes with Better Auth
  • Validation - Type-safe request validation with Zod
  • Database - Drizzle ORM for type-safe database queries
  • Logging - Winston logger setup for debugging and monitoring

📚 Sections#

1. Server Setup#

Learn how to structure an Express server with CORS, middleware, and routers.

2. Error Handling#

Centralized error handling, status codes, and logging errors.

3. Authentication#

Protect routes, check roles, and handle sessions with Better Auth.

4. Request Validation#

Validate req.body, req.query, and req.params with Zod schemas.

5. Complete Request Flow#

See how a complete request flows through middleware, validation, auth, controller, and database.

🚀 Quick Start#

# Install dependencies
npm install express cors morgan http-status-codes
npm install -D @types/express @types/cors @types/morgan

# TypeScript & validation
npm install typescript zod
npm install -D ts-node

# Logging
npm install winston

# Database (optional)
npm install drizzle-orm postgres

📖 Basic Example#

import express from "express";
import type { Request, Response } from "express";

const app = express();

// Middleware
app.use(express.json());

// Routes
app.get("/api/users", (req: Request, res: Response) => {
  res.json({ users: [] });
});

// Start server
app.listen(3000, () => {
  console.log("Server running on http://localhost:3000");
});

🎓 Core Concepts#

Status Codes (http-status-codes)#

import { StatusCodes } from "http-status-codes";

res.status(StatusCodes.OK).json({ data });              // 200
res.status(StatusCodes.CREATED).json({ user });         // 201
res.status(StatusCodes.BAD_REQUEST).json({ error });    // 400
res.status(StatusCodes.UNAUTHORIZED).json({ error });   // 401
res.status(StatusCodes.FORBIDDEN).json({ error });      // 403
res.status(StatusCodes.NOT_FOUND).json({ error });      // 404
res.status(StatusCodes.CONFLICT).json({ error });       // 409
res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({}); // 500

Type Imports#

import type { Request, Response, NextFunction } from "express";

Environment Variables#

import "dotenv/config"; // Loads .env file automatically

Export Patterns#

Feature export default named export
Number allowed Only 1 Many
Import style import x from import { x } from
Renaming Easy: import whatever from Must rename: import { x as y }
Common use Configs, single classes Utils, services, constants

🛠️ Helper Functions#

// Get client IP (handles proxies)
export function getClientIp(req: Request): string {
  if (req.headers["x-forwarded-for"]) {
    return (req.headers["x-forwarded-for"] as string).split(",")[0]!.trim();
  }
  if (req.headers["x-real-ip"]) {
    return req.headers["x-real-ip"] as string;
  }
  return req.ip!;
}

// Create URL-friendly slug
export function createSlug(title: string): string {
  return title
    .toLowerCase()
    .trim()
    .replace(/[^\w\s-]/g, "")
    .replace(/\s+/g, "-")
    .replace(/-+/g, "-");
}

// Generate random string
export function randString(): string {
  return crypto.randomBytes(20).toString("hex");
}

// Common API responses
export const commonResps = {
  internalError: {
    error: "Failed to complete request due to internal error",
    code: "internal_server_error",
  },
};

TypeScript Patterns#

Type Assertion#

const value as string  // Tells TS this is guaranteed to be a string

Filter Undefined Values#

export function filterUndefined<T>(obj: T): {
  [K in keyof T]: Exclude<T[K], undefined>;
} {
  const result = {} as any;
  for (const key in obj) {
    if (obj[key] !== undefined) {
      result[key] = obj[key];
    }
  }
  return result;
}

Ignore TypeScript Errors (use sparingly!)#

// @ts-ignore
const x: number = "this is actually a string";

📋 Next Steps#

Start with Server Setup to build your Express application structure, then move through each section to add functionality.


Pro Tip: Follow the request flow diagram in the Complete Request Flow section to understand how all pieces work together.