Trading Boilerplate for Sanity: Routing and Requests in Express.js
Why we don't write raw Node servers, how Express handles the plumbing, and the anatomy of a route.
If you want to build a web server in Node.js, you don't actually need any external libraries. Node has a built-in http module that is perfectly capable of listening to a network port and responding to requests.
But if you try to build a real, production-ready API using only that built-in module, you will quickly discover a painful truth: Node gives you a raw network socket, a stream of incoming bytes, and absolutely nothing else. It wishes you the best of luck.
Let’s look at what it takes to handle two simple endpoints—fetching users and creating a user—using raw Node.js:
const http = require('http');
const server = http.createServer((req, res) => {
if (req.method === 'GET' && req.url === '/users') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify([{ id: 1, name: "Alice" }]));
} else if (req.method === 'POST' && req.url === '/users') {
// Prepare yourself to manually parse a stream of data buffers...
let body = '';
req.on('data', chunk => { body += chunk.toString(); });
req.on('end', () => {
const user = JSON.parse(body);
res.writeHead(201, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ message: "User created", user }));
});
} else {
res.writeHead(404);
res.end("Not Found");
}
});
server.listen(3000);
This is exhausting. To figure out what the user wants, we are writing a giant if/else statement. To read the data they sent, we are manually stitching together memory buffers. If we added fifty more routes, this file would become an unreadable, unmaintainable nightmare.
This is exactly the problem Express.js was built to solve.
What is Express?
Express is a minimal, unopinionated web framework built on top of Node.js. It doesn't fundamentally change how Node works under the hood; it just wraps Node's raw HTTP APIs in a layer of developer-friendly abstraction.
Instead of writing massive if/else blocks to figure out where a request should go, Express introduces the concept of declarative routing. Instead of manually parsing byte streams, Express provides tools to parse data automatically. It handles the plumbing, so you can focus on building the house.
Here is the exact same logic from above, rewritten in Express:
const express = require('express');
const app = express();
// Tell Express to automatically parse incoming JSON data
app.use(express.json());
app.get('/users', (req, res) => {
res.json([{ id: 1, name: "Alice" }]);
});
app.post('/users', (req, res) => {
res.status(201).json({ message: "User created", user: req.body });
});
app.listen(3000, () => console.log("Server running on port 3000"));
It is instantly readable. The intent is obvious. Let's break down exactly how this works.
The Anatomy of a Route
In Express, a route is a rule. It tells the server: "When you receive a specific type of request, at this specific URL, run this specific function."
Every route is composed of three parts: app.METHOD(PATH, HANDLER)
The Method (
app.get,app.post, etc.): This corresponds to the HTTP verb the client used. It dictates the action (e.g., GET for reading, POST for creating).The Path (
'/users'): The specific URL endpoint the client is asking for.The Handler (
(req, res) => {}): The JavaScript function that actually does the work.
Handling GET Requests (Reading Data)
The most common operation in any API is serving data. When a client makes a GET request, they are asking for a resource.
Usually, they don't just want all the users; they want a specific one. Express makes it incredibly easy to capture dynamic parts of the URL using route parameters. You define a parameter by prefixing a path segment with a colon (:).
// The ":id" tells Express that this part of the URL is dynamic
app.get('/users/:id', (req, res) => {
// Express extracts the value and puts it in req.params
const userId = req.params.id;
// In a real app, you'd query your database here
const user = { id: userId, name: "Alice" };
res.json(user);
});
If a client requests /users/42, Express intercepts the request, grabs the 42, attaches it to req.params.id, and executes your function.
Handling POST Requests (Receiving Data)
When a client wants to send data to your server (like submitting a signup form), they use a POST request. The data they send is held in the request body.
By default, Express is incredibly lightweight. To save memory and CPU, it does not automatically parse request bodies. If you try to access req.body out of the box, it will be undefined.
To fix this, we have to use middleware—a function that runs before your route handler and modifies the incoming request.
// 1. The Middleware: "If the request has JSON, parse it."
app.use(express.json());
// 2. The Route Handler
app.post('/users', (req, res) => {
// Thanks to the middleware, req.body is now a native JS object
const newUser = req.body;
if (!newUser.name || !newUser.email) {
// We can chain methods to set the status and send the response
return res.status(400).json({ error: "Name and email are required" });
}
// Save to database logic goes here...
res.status(201).json({
message: "User successfully created",
user: newUser
});
});
Because Express is unopinionated, it forces you to explicitly opt-in to parsing JSON via app.use(express.json()). If your server only ever handles image uploads, you wouldn't use the JSON parser, and Express wouldn't waste cycles trying to parse one.
Sending Responses
Notice how we are sending data back to the client in these examples. In raw Node, we had to manually set the Content-Type header and use JSON.stringify() on our objects.
Express provides the .json() method on the response (res) object. When you call res.json({ key: "value" }), Express does three things automatically:
It converts your JavaScript object into a JSON string.
It sets the
Content-Type: application/jsonHTTP header so the client's browser knows how to read it.It ends the network response.
Similarly, it provides .status() to explicitly set the HTTP status code (like 404 for Not Found, or 201 for Created).
The Takeaway
Express isn't doing magic. It is still just Node.js under the hood.
But by giving us a clean, declarative way to define routes (app.get, app.post), extracting the pain of parsing incoming data, and providing helper methods for sending responses, it allows us to stop writing boilerplate network code.
When you use Express, your code stops looking like a network manual and starts looking like the actual business logic of your application.

