You've seen the diagram. Boxes labelled "nginx," "load balancer," "reverse proxy," "service mesh," arrows everywhere, and a feeling that real engineers were born understanding it. They weren't. That diagram wasn't designed in one go; it grew, one box at a time, and every box was added to solve a specific, understandable problem.
So we'll grow it ourselves. We start with the smallest possible system, push it until it breaks, and add exactly one piece to fix each break. By the end, the scary diagram will just look like a list of sensible decisions. No prior backend knowledge needed.
First, what even is "calling the backend"?
Your UI runs in a browser on someone's phone. The backend is a program running on a computer somewhere else (a "server"). When your UI does fetch('https://api.shop.com/products'), here's the plain-English version of what happens:
Find the address
api.shop.comis a name, like "the pizza place." The browser asks DNS (the internet's phone book) "what's the actual number for this name?" and gets back an IP address, like93.184.216.34.Knock on the door
The browser opens a connection to that address on a specific port (a numbered door on the machine; web traffic usually uses port 443 for HTTPS).
Send the request
Over that connection it sends an HTTP request: "GET /products, please." Just text, really.
Get the response
The backend program reads the request, does its work, and sends back an HTTP response: a status code (200 = OK) and some data (your products as JSON).
That's it. A "call to the backend" is your code sending a little text message to a program on another computer and getting a text message back. Everything else in this article is about what's on the other side of that connection, and how it grows.
System 1: one cook, one stall
The simplest backend is a single program listening on a port. In Node, the whole thing fits on screen:
import { createServer } from 'node:http';
const server = createServer((req, res) => {
if (req.url === '/products') {
res.writeHead(200, { 'content-type': 'application/json' });
res.end(JSON.stringify([{ id: 1, name: 'Keyboard' }]));
} else {
res.writeHead(404);
res.end('Not found');
}
});
server.listen(3000); // listening on port 3000Analogy: a tiny food stall
This is a one-person food stall. You (the browser) walk up and talk directly to the cook (the server). The cook takes your order, makes the food, hands it back. For a handful of customers, this is perfect. There's no host, no waiter, no kitchen stations, because there's no need yet. Most apps should start exactly this simple.
System 2: the cook needs a pantry (the database)
Our cook is keeping the menu in their head. But real data (users, orders, products) can't live "in the program's memory" — if the program restarts, it's all gone, and a second copy of the program wouldn't share it. The data needs a permanent home: a database.
The server doesn't store the data itself anymore. It asks the database for it.
// The server now reads from a database instead of hardcoded data
const products = await db.query('SELECT id, name FROM products');
res.end(JSON.stringify(products.rows));Analogy: the storeroom out back
The database is the pantry/storeroom. The cook doesn't keep ingredients in their pockets; they keep them in a proper store and fetch what they need per order. The store stays stocked even if the cook goes home (restarts).
The "connection" and why we pool them
Talking to a database means opening a connection to it (just like the browser opened a connection to the server). But opening a fresh connection is slow and the database can only handle a limited number at once. So the server keeps a small set of connections open and reuses them. That set is a connection pool.
// Don't open a new connection per request — reuse a small pool.
const pool = new Pool({ max: 20 }); // keep up to 20 ready
const result = await pool.query('SELECT * FROM products WHERE id = $1', [id]);Analogy: a few waiters, not one per customer
Imagine if every customer who wanted something from the pantry hired their own runner, then fired them after one trip. Chaos, and the pantry door can only fit so many people. Instead you keep a small team of runners (the pool) who shuttle back and forth and are reused all day. That's a connection pool: a handful of reusable doorways to the database instead of a new one each time.
System 3: one cook can't keep up (load balancer)
The stall got popular. One server program can only handle so many requests at once before everyone waits. The fix isn't a magic faster cook; it's more cooks: run several copies of the exact same server program (on more machines, or more containers).
But now there's a new question. The browser knows one address. Which copy of the server should handle each request? Something has to stand in front and hand out requests. That's a load balancer.
Analogy: the host at a busy restaurant
The load balancer is the host standing at the door of a busy restaurant. You don't pick your own table; the host sees which waiter is free and sends you there, keeping the load even so no single waiter is swamped while another stands idle. If a waiter goes home sick (a server crashes), the host simply stops seating people there.
How does it pick? Two common strategies, and you can feel them in code. Run this:
Round robin just takes turns. Least connections sends the next request to the least-busy server. The load balancer also does health checks: it quietly pings each server, and if one stops answering, it's taken out of rotation so no requests get sent into the void.
This is why servers should be 'stateless'
For the host to send you to any free waiter, every waiter must be able to serve any customer. So your server copies shouldn't keep important things "in their own memory" (like who's logged in). That belongs in the shared database or a cache, so request #2 works no matter which copy handles it. "Keep state out of the servers" is the rule that makes adding more servers easy.
System 4: a proper front desk (reverse proxy / nginx)
We have several servers behind a load balancer. But a few jobs don't belong inside your app code, and you don't want to do them in every server copy:
- Handling HTTPS/TLS (the encryption padlock) so your app code deals in plain HTTP internally.
- Serving static files (images, the built JavaScript) without bothering the app.
- Routing by URL: send
/api/*to the app servers,/images/*to a file store. - Hiding the messy internals behind one clean public address.
The thing that sits at the very front and does this is a reverse proxy, and the most famous one is nginx. (A load balancer is often built into the reverse proxy — nginx can do both, which is why these boxes blur together in real diagrams.)
# A tiny nginx reverse-proxy config
server {
listen 443 ssl; # handle HTTPS here
server_name api.shop.com;
location /images/ {
root /var/www; # serve files directly
}
location /api/ {
proxy_pass http://app_servers; # forward everything else to the app
}
}
upstream app_servers { # the pool it load-balances across
server 10.0.0.1:3000;
server 10.0.0.2:3000;
}Analogy: the building's reception desk
nginx is the receptionist in a company lobby. Visitors (browsers) never wander the building looking for the right person. They go to one desk. Reception checks their ID (TLS), answers simple questions on the spot (serves a static file), and routes everyone else to the right department (forwards to an app server). The staff work safely behind the desk, and the public only ever sees reception.
Wait — "proxy," "reverse proxy," what's the difference?
The word proxy just means "something that acts on behalf of someone else." The direction is the whole distinction:
Forward proxy — for the client
Sits in front of you and makes requests to the outside world on your behalf. The website you visit sees the proxy, not you. Think a company travel agent: you tell them where you want to go, they deal with airlines for you. Used for company web filters, privacy, and caching outbound requests.
Reverse proxy — for the server
Sits in front of the servers and receives requests from the outside world on their behalf. You (the visitor) see the proxy, not the real servers. Think the company receptionist: outsiders talk to reception, never directly to staff. nginx in front of your app is a reverse proxy.
Same idea (a middleman), opposite sides. A forward proxy hides the clients from the internet; a reverse proxy hides the servers from the internet. In backend architecture you'll almost always mean the reverse kind.
System 5: the kitchen gets stations (microservices)
So far, all our app servers run one program that does everything: products, orders, payments, emails, search. That single do-everything program is called a monolith, and for most teams it's the right choice for a long time.
But imagine the app gets huge and so does the team. Everyone edits the same codebase and trips over each other. The payment code and the search code have totally different needs, but they're stuck deploying together. At some point you might split the one big program into several smaller ones, each owning one job and talking to the others over the network. Those are microservices.
Monolith — one program does it all
One codebase, one deploy, one database. Simple to build, run, and debug. The right default. It hurts only when the team gets large or one part needs to scale very differently from the rest.
Microservices — many small programs
Each service (orders, payments, search) is its own program with its own database, deployed independently. Teams move without colliding and each part scales on its own. The cost: now it's a distributed system — network calls between services, more moving parts, much harder to debug.
Analogy: one cook vs a kitchen with stations
The monolith is one brilliant cook who does grill, salad, and dessert alone. Fast and simple, until the orders pile up and the menu explodes. Microservices are a big kitchen with specialist stations: a grill station, a salad station, a pastry station, each an expert, each able to get busier without slowing the others. But now they have to coordinate (the grill tells pastry when the main is nearly done), and coordination between stations is exactly where things get complicated.
Don't start here
This is the single most over-applied idea in backend engineering. Microservices solve a team-and-scale problem, not a "this is how real apps look" problem. A small team on a monolith ships faster than the same team juggling fifteen services and the network between them. Split only when a real pain (teams colliding, one part needing very different scaling) actually shows up. Most "we need microservices" really means "we haven't outgrown the monolith yet."
The whole journey, end to end
Now put every layer together and follow a single tap of "Buy" through the grown-up system. None of it is mysterious anymore, because you watched each box arrive.
DNS lookup
The browser turns
api.shop.cominto an IP address.CDN check (often first)
For static things (images, the app's JS), a CDN near the user may answer instantly without the request ever reaching your servers.
Reverse proxy / load balancer
The request hits nginx at the front door. It terminates HTTPS and picks a healthy app server to forward to.
App server
Your backend code runs: checks the user is allowed, validates the order.
Database (via the pool)
It borrows a connection from the pool and reads/writes the data.
Other services / queue
For slow work (sending the receipt email), it drops a job on a queue and moves on, instead of making the user wait.
Response travels back
App server → nginx → browser. The UI updates. All of this, ideally, in well under a second.
The one idea to take away
Every layer exists to solve one problem the simpler setup couldn't: the database remembers data past a restart, the connection pool reuses expensive database doorways, the load balancer spreads work across many server copies, the reverse proxy (nginx) gives one clean front door that handles HTTPS, routing, and static files, and microservices let big teams and uneven scaling parts move independently (at real cost). You never need all of it at once. Start with one cook at a stall, and add a box only when you can feel the problem it fixes.
Want to go deeper on any single box? The full-stack track walks through the runtime, databases, and APIs, and the system design deep dives show these layers handling real scale, like 60 million people watching one cricket match.
Test yourself
Questions· say the answer out loud before you open it. If you can't, the chapter isn't done.
QIn plain terms, what happens when the UI 'calls the backend'?+
The browser turns the API's name into an IP address via DNS, opens a connection to that address on a port, sends an HTTP request (just text like 'GET /products'), and the backend program reads it, does its work, and sends back an HTTP response with a status code and data. A call is one text message to a program on another computer and a text message back.
QWhy does an app need a database instead of just keeping data in the server's memory?+
Because memory is lost when the program restarts, and a second copy of the server wouldn't share it. A database is the permanent, shared home for data, so it survives restarts and every server copy sees the same data. The server becomes a middleman that asks the database rather than remembering things itself.
QWhat is a connection pool and why use one?+
Opening a database connection is slow and the database accepts only a limited number at once. A connection pool keeps a small set of connections open and reuses them across requests instead of opening a fresh one each time. Like a small team of runners shuttling to the storeroom rather than hiring a new runner per trip.
QWhat problem does a load balancer solve, and how does it choose a server?+
One server can only handle so much, so you run several identical copies; the load balancer sits in front and decides which copy handles each request, keeping load even and skipping any that fail its health checks. It chooses by strategies like round robin (take turns) or least connections (send to the least busy server).
QWhat's the difference between a forward proxy and a reverse proxy?+
Both are middlemen; the difference is which side they represent. A forward proxy sits in front of the client and makes requests to the internet on its behalf (like a travel agent), hiding the client. A reverse proxy sits in front of the servers and receives requests on their behalf (like a receptionist), hiding the servers. nginx in front of your app is a reverse proxy.
QWhat does nginx (a reverse proxy) actually do at the front of a system?+
It's the single public front door: it handles HTTPS/TLS so app code stays plain HTTP, serves static files directly, routes by URL (/api to app servers, /images to a file store), load-balances across server copies, and hides the internal servers from the public. One clean address in front of the messy internals.
QWhen should you use microservices instead of a monolith?+
Only when a real team-or-scale pain appears: many engineers colliding in one codebase, or one part needing wildly different scaling from the rest. A monolith (one program, one deploy, one database) is simpler to build, run, and debug and is the right default. Microservices add a distributed system's complexity, so you split reluctantly, not by default.
Comments
Loading comments…