How Express turns Node.js from "I can build a server" to "I can build a server in 5 minutes."
In the last article, we built an HTTP server with raw Node.js. It worked. But let me be honest — it was painful. Manually parsing URLs, setting headers, checking req.url with if-else chains, handling different HTTP methods... for a simple 3-page site, it was already getting messy.
Now imagine building an API with 20 routes, request body parsing, error handling, and middleware. With raw Node.js? That's weeks of boilerplate code.
This is why Express.js exists. It takes everything tedious about raw Node.js HTTP handling and gives you a clean, minimal API to do the same thing in a fraction of the code. Express is to Node.js what a power drill is to a screwdriver — same job, dramatically faster.
Let me show you how it works. This was one of those "why didn't I learn this sooner" moments in the ChaiCode Web Dev Cohort 2026.
What Is Express.js?
Express.js is a lightweight web framework for Node.js. It sits on top of Node's built-in http module and adds:
- Routing — map URLs to handler functions cleanly
- Middleware — plug in reusable logic (logging, parsing, auth)
-
Request/Response helpers —
req.params,req.body,res.json(),res.status() - Simpler syntax — less boilerplate, more focus on your logic
Node.js (raw):
const http = require("http");
http.createServer((req, res) => {
if (req.url === "/users" && req.method === "GET") { ... }
if (req.url === "/users" && req.method === "POST") { ... }
// manual parsing, manual headers, manual everything
});
Express:
const app = express();
app.get("/users", handler);
app.post("/users", handler);
// done. Express handles the rest.
Express is the most popular Node.js framework by far — used by companies like Uber, IBM, and Accenture. It's minimal enough to learn quickly but powerful enough for production apps.
Why Express Simplifies Node.js Development
Let me prove it. Here's the same server — raw Node.js vs Express — doing the exact same thing.
Raw Node.js
const http = require("http");
const server = http.createServer((req, res) => {
if (req.url === "/" && req.method === "GET") {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ message: "Welcome to the API" }));
} else if (req.url === "/users" && req.method === "GET") {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify([{ name: "Pratham" }, { name: "Arjun" }]));
} else if (req.url === "/users" && req.method === "POST") {
let body = "";
req.on("data", (chunk) => (body += chunk));
req.on("end", () => {
const user = JSON.parse(body);
res.writeHead(201, { "Content-Type": "application/json" });
res.end(JSON.stringify({ created: user }));
});
} else {
res.writeHead(404, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "Not found" }));
}
});
server.listen(3000);
27 lines. Manual URL checking, manual method checking, manual body parsing, manual headers everywhere.
Express
const express = require("express");
const app = express();
app.use(express.json()); // Parse JSON bodies automatically
app.get("/", (req, res) => {
res.json({ message: "Welcome to the API" });
});
app.get("/users", (req, res) => {
res.json([{ name: "Pratham" }, { name: "Arjun" }]);
});
app.post("/users", (req, res) => {
res.status(201).json({ created: req.body });
});
app.listen(3000, () => {
console.log("Server running on http://localhost:3000");
});
18 lines. Clean routing, automatic JSON parsing, built-in response helpers. Same functionality, half the code, ten times more readable.
Setting Up Express
Initialize a Project
mkdir express-app
cd express-app
npm init -y
Install Express
npm install express
This adds Express to your node_modules folder and records it in package.json under dependencies.
Verify Installation
Your package.json should include:
{
"dependencies": {
"express": "^4.18.2"
}
}
Creating Your First Express Server
// server.js
const express = require("express");
const app = express();
const PORT = 3000;
app.get("/", (req, res) => {
res.send("Hello from Express! 🚀");
});
app.listen(PORT, () => {
console.log(`Server running at http://localhost:${PORT}`);
});
Run it:
node server.js
Visit http://localhost:3000 in your browser. You'll see: Hello from Express! 🚀
What Each Line Does
const express = require("express"); // Import Express
const app = express(); // Create an Express application
const PORT = 3000; // Define the port
app.get("/", (req, res) => { // When someone visits "/"...
res.send("Hello from Express!"); // ...send this response
});
app.listen(PORT, () => { // Start listening for requests
console.log(`Server running...`);
});
Express Routing — The Core Concept
Routing means connecting a URL path + HTTP method to a handler function. When a request matches a route, the handler runs.
Route Structure
app.METHOD(PATH, HANDLER);
-
METHOD —
get,post,put,patch,delete -
PATH — the URL pattern (
/,/users,/products/:id) - HANDLER — the function that runs when the route matches
Express Routing Structure
Incoming Request: GET /users
│
↓
┌──────────────────────────────────────────┐
│ EXPRESS APP │
│ │
│ app.get("/") ← doesn't match │
│ app.get("/users") ← MATCH! ✅ │
│ app.post("/users") ← wrong method │
│ app.get("/products") ← doesn't match │
│ │
└──────────────────┬───────────────────────┘
│
↓
┌──────────────────────────────────────────┐
│ ROUTE HANDLER │
│ │
│ (req, res) => { │
│ res.json([...users]); │
│ } │
│ │
└──────────────────┬───────────────────────┘
│
↓
┌──────────────────────────────────────────┐
│ RESPONSE TO CLIENT │
│ │
│ Status: 200 │
│ Body: [{ "name": "Pratham" }, ...] │
│ │
└──────────────────────────────────────────┘
Express checks routes in the order they're defined and runs the first one that matches.
Handling GET Requests
GET requests are for reading data. They're what happens when you visit a URL in your browser, click a link, or call an API to fetch information.
Basic GET Routes
const express = require("express");
const app = express();
// Home page
app.get("/", (req, res) => {
res.send("Welcome to the Home Page!");
});
// About page
app.get("/about", (req, res) => {
res.send("This is the About Page.");
});
// API endpoint — return JSON
app.get("/api/users", (req, res) => {
const users = [
{ id: 1, name: "Pratham", role: "developer" },
{ id: 2, name: "Arjun", role: "designer" },
{ id: 3, name: "Priya", role: "manager" },
];
res.json(users);
});
app.listen(3000, () => console.log("Server on http://localhost:3000"));
Route Parameters — Dynamic URLs
What if you need /users/1, /users/2, /users/3? You can't create a separate route for each user. Use route parameters:
app.get("/users/:id", (req, res) => {
const userId = req.params.id;
res.json({ message: `You requested user with ID: ${userId}` });
});
// GET /users/5 → { message: "You requested user with ID: 5" }
// GET /users/42 → { message: "You requested user with ID: 42" }
:id is a placeholder. Whatever value is in that position gets stored in req.params.id.
Multiple Parameters
app.get("/users/:userId/orders/:orderId", (req, res) => {
const { userId, orderId } = req.params;
res.json({
message: `Order ${orderId} for user ${userId}`,
});
});
// GET /users/3/orders/101 → { message: "Order 101 for user 3" }
Query Parameters
Query parameters are the ?key=value pairs after the URL:
app.get("/search", (req, res) => {
const { q, page, limit } = req.query;
res.json({
searchTerm: q,
page: page || 1,
limit: limit || 10,
});
});
// GET /search?q=nodejs&page=2&limit=5
// → { searchTerm: "nodejs", page: "2", limit: "5" }
Handling POST Requests
POST requests are for sending data to the server — creating new resources, submitting forms, sending login credentials.
Setup: Parse Request Bodies
Express doesn't parse request bodies by default. Add this middleware:
app.use(express.json()); // Parse JSON bodies
app.use(express.urlencoded({ extended: true })); // Parse form data
Basic POST Route
const express = require("express");
const app = express();
app.use(express.json());
const users = [];
app.post("/api/users", (req, res) => {
const { name, email } = req.body;
if (!name || !email) {
return res.status(400).json({ error: "Name and email are required" });
}
const newUser = {
id: users.length + 1,
name,
email,
createdAt: new Date().toISOString(),
};
users.push(newUser);
res.status(201).json({ message: "User created!", user: newUser });
});
app.listen(3000, () => console.log("Server running"));
Testing POST Requests
You can't test POST requests in a browser address bar (that only sends GET). Use one of these:
Using curl in terminal:
curl -X POST http://localhost:3000/api/users \
-H "Content-Type: application/json" \
-d '{"name": "Pratham", "email": "pratham@prathamdev.in"}'
Using the fetch API in browser console:
fetch("http://localhost:3000/api/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "Pratham", email: "pratham@prathamdev.in" }),
})
.then((res) => res.json())
.then((data) => console.log(data));
Sending Responses
Express gives you several ways to send responses back to the client:
res.send() — Send Any Content
app.get("/text", (req, res) => {
res.send("Plain text response");
});
app.get("/html", (req, res) => {
res.send("<h1>HTML response</h1><p>This is rendered by the browser.</p>");
});
res.json() — Send JSON (Most Common for APIs)
app.get("/api/data", (req, res) => {
res.json({
success: true,
data: { name: "Pratham", role: "developer" },
});
});
res.json() automatically sets the Content-Type header to application/json.
res.status() — Set Status Code
app.post("/api/users", (req, res) => {
// 201 = Created
res.status(201).json({ message: "User created!" });
});
app.get("/api/missing", (req, res) => {
// 404 = Not Found
res.status(404).json({ error: "Resource not found" });
});
app.get("/api/error", (req, res) => {
// 500 = Server Error
res.status(500).json({ error: "Something went wrong" });
});
Common HTTP Status Codes
| Code | Meaning | When to Use |
|---|---|---|
| 200 | OK | Successful GET, PUT, PATCH |
| 201 | Created | Successful POST (new resource) |
| 204 | No Content | Successful DELETE |
| 400 | Bad Request | Invalid input from client |
| 401 | Unauthorized | Missing or invalid authentication |
| 404 | Not Found | Resource doesn't exist |
| 500 | Internal Server Error | Something broke on the server |
res.redirect() — Redirect to Another URL
app.get("/old-page", (req, res) => {
res.redirect("/new-page");
});
Request → Route Handler → Response Flow
Here's the complete picture of how a request flows through Express:
Client sends: POST /api/users { "name": "Pratham" }
│
↓
┌──────────────────────────────────────────────┐
│ EXPRESS APP │
│ │
│ 1. Middleware runs first: │
│ express.json() → parses request body │
│ │
│ 2. Router matches the route: │
│ app.post("/api/users") ← MATCH! ✅ │
│ │
│ 3. Handler runs: │
│ (req, res) => { │
│ const { name } = req.body; │
│ // create user... │
│ res.status(201).json({ user }); │
│ } │
│ │
│ 4. Response sent to client: │
│ Status: 201 │
│ Body: { "user": { "name": "Pratham" } } │
│ │
└──────────────────────────────────────────────┘
Putting It All Together — A Mini CRUD API
Let's build a complete example with GET, POST, and route parameters:
const express = require("express");
const app = express();
app.use(express.json());
// In-memory "database"
let todos = [
{ id: 1, title: "Learn Node.js", completed: false },
{ id: 2, title: "Build Express API", completed: false },
{ id: 3, title: "Deploy to production", completed: false },
];
// GET all todos
app.get("/api/todos", (req, res) => {
res.json(todos);
});
// GET single todo by ID
app.get("/api/todos/:id", (req, res) => {
const todo = todos.find((t) => t.id === parseInt(req.params.id));
if (!todo) {
return res.status(404).json({ error: "Todo not found" });
}
res.json(todo);
});
// POST — create new todo
app.post("/api/todos", (req, res) => {
const { title } = req.body;
if (!title) {
return res.status(400).json({ error: "Title is required" });
}
const newTodo = {
id: todos.length + 1,
title,
completed: false,
};
todos.push(newTodo);
res.status(201).json(newTodo);
});
// PUT — update todo
app.put("/api/todos/:id", (req, res) => {
const todo = todos.find((t) => t.id === parseInt(req.params.id));
if (!todo) {
return res.status(404).json({ error: "Todo not found" });
}
todo.title = req.body.title || todo.title;
todo.completed = req.body.completed ?? todo.completed;
res.json(todo);
});
// DELETE — remove todo
app.delete("/api/todos/:id", (req, res) => {
const index = todos.findIndex((t) => t.id === parseInt(req.params.id));
if (index === -1) {
return res.status(404).json({ error: "Todo not found" });
}
todos.splice(index, 1);
res.status(204).send();
});
app.listen(3000, () => {
console.log("Todo API running at http://localhost:3000/api/todos");
});
A fully functional REST API with create, read, update, and delete — in under 60 lines.
Let's Practice: Hands-On Assignment
Part 1: Create a Basic Express Server
Set up a new project, install Express, and create a server with three routes:
-
GET /— returns"Welcome to my Express app!" -
GET /about— returns"About page" -
GET /api/status— returns JSON{ status: "running", uptime: process.uptime() }
Part 2: Build a User API
Create these endpoints:
-
GET /api/users— returns all users -
GET /api/users/:id— returns a single user by ID (or 404) -
POST /api/users— creates a new user fromreq.body(validate name and email)
Part 3: Add Search Functionality
Add a route that accepts query parameters:
// GET /api/users/search?name=Pratham&role=developer
app.get("/api/users/search", (req, res) => {
const { name, role } = req.query;
// Filter users based on query parameters
// Return matching users
});
Key Takeaways
- Express.js is a minimal web framework that simplifies Node.js development — routing, request parsing, and response handling become clean and readable.
-
Routing maps URL paths + HTTP methods to handler functions:
app.get("/path", handler). -
GET requests read data. Use
req.paramsfor URL parameters (:id) andreq.queryfor query strings (?key=value). -
POST requests send data. Use
express.json()middleware to parse request bodies, then access data viareq.body. -
Response methods —
res.json()for APIs,res.send()for text/HTML,res.status()for HTTP codes,res.redirect()for redirections.
Wrapping Up
Express takes the raw power of Node.js and wraps it in a developer-friendly API. The same server that took 27 lines with raw http takes 18 lines with Express — and it's more readable, more maintainable, and more extensible. That's why Express is the default starting point for nearly every Node.js backend project.
I'm learning all of this through the ChaiCode Web Dev Cohort 2026 under Hitesh Chaudhary and Piyush Garg. Express was the moment backend development went from "I can do this" to "I actually enjoy doing this." Routes, request handling, JSON APIs — it all feels natural with Express.
Connect with me on LinkedIn or visit PrathamDEV.in. More articles on the way as the backend journey continues.
Happy coding! 🚀
Written by Pratham Bhardwaj | Web Dev Cohort 2026, ChaiCode
Top comments (0)