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 --initUpdate 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