Skip to main content

Express Setup

Express is the most widely used Node.js framework for building REST APIs. In this lesson we'll set up a TypeScript project from scratch, define routes, and handle requests and responses.

Project Initialization

Start by creating a new project and installing dependencies:

mkdir my-api && cd my-api
npm init -y
npm install express
npm install -D typescript @types/express @types/node tsx
npx tsc --init

Update your tsconfig.json with these key settings:

// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  },
  "include": ["src"]
}

Add scripts to your package.json:

// package.json (partial)
{
  "type": "module",
  "scripts": {
    "dev": "tsx watch src/index.ts",
    "build": "tsc",
    "start": "node dist/index.js"
  }
}

Creating the Server

Create src/index.ts as your entry point:

import express from "express";

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

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

// Health check
app.get("/health", (_req, res) => {
  res.json({ status: "ok", timestamp: new Date().toISOString() });
});

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

export default app;

Run it with npm run dev and visit http://localhost:3000/health.

Defining Routes

Organize routes into separate files using Express Router:

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

const router = Router();

interface User {
  id: number;
  name: string;
  email: string;
}

// In-memory store (replace with a database later)
let users: User[] = [
  { id: 1, name: "Alice", email: "alice@example.com" },
  { id: 2, name: "Bob", email: "bob@example.com" },
];
let nextId = 3;

// GET /api/users
router.get("/", (_req, res) => {
  res.json({ data: users });
});

// GET /api/users/:id
router.get("/:id", (req, res) => {
  const user = users.find((u) => u.id === Number(req.params.id));
  if (!user) {
    res.status(404).json({ error: "User not found" });
    return;
  }
  res.json({ data: user });
});

// POST /api/users
router.post("/", (req, res) => {
  const { name, email } = req.body;
  if (!name || !email) {
    res.status(400).json({ error: "Name and email are required" });
    return;
  }
  const user: User = { id: nextId++, name, email };
  users.push(user);
  res.status(201).json({ data: user });
});

// PUT /api/users/:id
router.put("/:id", (req, res) => {
  const index = users.findIndex((u) => u.id === Number(req.params.id));
  if (index === -1) {
    res.status(404).json({ error: "User not found" });
    return;
  }
  const { name, email } = req.body;
  users[index] = { id: users[index].id, name, email };
  res.json({ data: users[index] });
});

// DELETE /api/users/:id
router.delete("/:id", (req, res) => {
  const index = users.findIndex((u) => u.id === Number(req.params.id));
  if (index === -1) {
    res.status(404).json({ error: "User not found" });
    return;
  }
  users.splice(index, 1);
  res.status(204).send();
});

export default router;

Mount the router in your main server file:

// src/index.ts
import express from "express";
import usersRouter from "./routes/users.js";

const app = express();
app.use(express.json());

app.get("/health", (_req, res) => {
  res.json({ status: "ok" });
});

// Mount routes
app.use("/api/users", usersRouter);

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

Request and Response Objects

Express extends Node's req and res with useful properties:

router.get("/example", (req, res) => {
  // Request properties
  req.params;     // URL parameters — /users/:id -> { id: "42" }
  req.query;      // Query string — /users?role=admin -> { role: "admin" }
  req.body;       // Parsed JSON body (requires express.json() middleware)
  req.headers;    // HTTP headers
  req.method;     // "GET", "POST", etc.
  req.path;       // "/example"

  // Response methods
  res.status(200);                   // Set status code
  res.json({ message: "hello" });    // Send JSON (sets Content-Type automatically)
  res.send("plain text");            // Send a string response
  res.set("X-Custom", "value");      // Set a response header
  res.redirect("/other");            // Redirect to another URL
});

Basic Middleware

Middleware functions run before your route handler. They have access to req, res, and a next function:

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

export function logger(req: Request, res: Response, next: NextFunction) {
  const start = Date.now();
  res.on("finish", () => {
    const duration = Date.now() - start;
    console.log(`${req.method} ${req.path} ${res.statusCode} ${duration}ms`);
  });
  next();
}

Apply it globally or to specific routes:

import { logger } from "./middleware/logger.js";

// Global — applies to all routes
app.use(logger);

// Route-specific
app.get("/api/users", logger, (_req, res) => {
  res.json({ data: [] });
});

Project Structure

A clean folder layout for a REST API project:

src/
  index.ts           # Server entry point
  routes/
    users.ts         # User routes
    posts.ts         # Post routes
  middleware/
    logger.ts        # Request logging
    auth.ts          # Authentication
  types/
    index.ts         # Shared TypeScript types