Follow AiTechWorlds on LinkedIn for professional AI content!Follow Now →
18 minLesson 29 of 40
Backend with Node.js

Node.js & Express Setup

Node.js and Express — Building Backend Servers

Express is the most widely used Node.js web framework. It gives you just enough structure to build HTTP servers without getting in your way. Understanding Express teaches you how web servers work at a fundamental level — knowledge that transfers directly to Fastify, Koa, NestJS, and backend development in any language.

What Express Does

Express takes an incoming HTTP request and routes it to the right handler function. That's the core job:

GET /api/users → handler function → response
POST /api/users → different handler → response
GET /api/users/42 → another handler → response

Everything else — authentication, validation, database access — you add as middleware or build in handlers.

Setup

mkdir my-api && cd my-api
npm init -y
npm install express
npm install -D @types/express typescript ts-node nodemon

Configure TypeScript:

// tsconfig.json
{
    "compilerOptions": {
        "target": "ES2022",
        "module": "commonjs",
        "rootDir": "src",
        "outDir": "dist",
        "strict": true,
        "esModuleInterop": true
    }
}
// package.json scripts
{
    "scripts": {
        "dev": "nodemon --exec ts-node src/index.ts",
        "build": "tsc",
        "start": "node dist/index.js"
    }
}

Your First Server

// src/index.ts
import express from "express";

const app = express();
const PORT = process.env.PORT ?? 3000;

// Parse JSON request bodies
app.use(express.json());

// Basic route
app.get("/", (req, res) => {
    res.json({ message: "API is running" });
});

app.listen(PORT, () => {
    console.log(`Server running on http://localhost:${PORT}`);
});

Run with npm run dev — the server restarts automatically when you save changes.

Routing

// GET /api/courses
app.get("/api/courses", (req, res) => {
    res.json({ courses: [] });
});

// GET /api/courses/:id — route parameter
app.get("/api/courses/:id", (req, res) => {
    const { id } = req.params;
    res.json({ id, title: `Course ${id}` });
});

// POST /api/courses
app.post("/api/courses", (req, res) => {
    const { title, description } = req.body;
    // Create course in database...
    res.status(201).json({ id: "new-id", title, description });
});

// PATCH /api/courses/:id
app.patch("/api/courses/:id", (req, res) => {
    const { id } = req.params;
    const updates = req.body;
    res.json({ id, ...updates });
});

// DELETE /api/courses/:id
app.delete("/api/courses/:id", (req, res) => {
    const { id } = req.params;
    // Delete from database...
    res.status(204).send();
});

Query Parameters

// GET /api/courses?page=2&limit=10&search=react
app.get("/api/courses", (req, res) => {
    const page = parseInt(req.query.page as string) || 1;
    const limit = parseInt(req.query.limit as string) || 10;
    const search = (req.query.search as string) ?? "";
    
    res.json({ page, limit, search });
});

Router — Organizing Routes

As your API grows, keep routes organized in separate files:

// src/routes/courses.ts
import { Router } from "express";

const router = Router();

router.get("/", async (req, res) => {
    const courses = await getCourses();
    res.json(courses);
});

router.get("/:id", async (req, res) => {
    const course = await getCourse(req.params.id);
    if (!course) return res.status(404).json({ error: "Not found" });
    res.json(course);
});

router.post("/", async (req, res) => {
    const course = await createCourse(req.body);
    res.status(201).json(course);
});

export default router;
// src/routes/users.ts
import { Router } from "express";
const router = Router();
// ... user routes
export default router;
// src/index.ts — mount routers
import courseRouter from "./routes/courses";
import userRouter from "./routes/users";

app.use("/api/courses", courseRouter);
app.use("/api/users", userRouter);

Clean, modular, and easy to find things.

Middleware

Middleware functions run between the request arriving and the handler responding. They have access to req, res, and next:

// Logging middleware — runs on every request
app.use((req, res, next) => {
    console.log(`${req.method} ${req.path} — ${new Date().toISOString()}`);
    next();   // pass to next middleware or handler
});

// Body parsing (built into Express 4.16+)
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// CORS middleware
app.use((req, res, next) => {
    res.header("Access-Control-Allow-Origin", "*");
    res.header("Access-Control-Allow-Headers", "Content-Type, Authorization");
    res.header("Access-Control-Allow-Methods", "GET, POST, PATCH, DELETE, OPTIONS");
    if (req.method === "OPTIONS") return res.sendStatus(200);
    next();
});

Or use the cors package:

npm install cors && npm install -D @types/cors
import cors from "cors";

app.use(cors({
    origin: ["http://localhost:3000", "https://myapp.com"],
    credentials: true,
}));

Authentication Middleware

import jwt from "jsonwebtoken";

function requireAuth(req: any, res: any, next: any) {
    const auth = req.headers.authorization;
    
    if (!auth?.startsWith("Bearer ")) {
        return res.status(401).json({ error: "No token provided" });
    }
    
    const token = auth.slice(7);
    
    try {
        const payload = jwt.verify(token, process.env.JWT_SECRET!);
        req.user = payload;
        next();
    } catch {
        res.status(401).json({ error: "Invalid token" });
    }
}

// Apply to specific routes
app.get("/api/dashboard", requireAuth, (req: any, res) => {
    res.json({ userId: req.user.id });
});

// Apply to all routes in a router
router.use(requireAuth);

Error Handling

Express has a special error-handling middleware with four parameters:

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

export class AppError extends Error {
    constructor(public message: string, public statusCode: number) {
        super(message);
    }
}

export function errorHandler(err: Error, req: Request, res: Response, next: NextFunction) {
    if (err instanceof AppError) {
        return res.status(err.statusCode).json({ error: err.message });
    }
    
    console.error("Unhandled error:", err);
    res.status(500).json({ error: "Internal server error" });
}
// In your route handler, throw to trigger the error handler
app.get("/api/courses/:id", async (req, res, next) => {
    try {
        const course = await getCourse(req.params.id);
        if (!course) throw new AppError("Course not found", 404);
        res.json(course);
    } catch (error) {
        next(error);   // pass to error handler
    }
});

// Register error handler LAST, after all routes
app.use(errorHandler);

Environment Variables

# .env
DATABASE_URL=postgresql://localhost:5432/mydb
JWT_SECRET=your-super-secret-key-change-this
PORT=3000
npm install dotenv
// src/index.ts — load at the very top
import "dotenv/config";

// Now process.env.JWT_SECRET etc. are available

A Complete Project Structure

src/
├── index.ts              → App entry point
├── routes/
│   ├── courses.ts        → Course routes
│   ├── users.ts          → User routes
│   └── auth.ts           → Auth routes (login, register)
├── middleware/
│   ├── auth.ts           → JWT verification
│   └── errorHandler.ts   → Global error handler
├── controllers/
│   └── courses.ts        → Business logic separated from routes
├── models/
│   └── Course.ts         → Database model
└── lib/
    ├── db.ts             → Database connection
    └── jwt.ts            → JWT utilities

Separating routes from controllers keeps your route files as thin wiring — they receive the request, call a controller function, and send the response. The controller handles the business logic.

Next lesson: REST API design principles — building APIs that developers love to use.

📱

Get this course's notes on Telegram!

Free cheat sheets, summaries & practice exercises

Get Notes Free →
!