What is a backend?
The backend is the central server that stores data for everyone, keeps secrets like the database password, and applies the same rules to every user. The frontend runs on the user's own device and forgets everything when the tab closes.
Frontend only
With a backend
Press ▶ Play or “Next step”. Arrow keys work too.
Debug view
Step through the diagram to see requests and responses.
Step through the diagram to see requests and responses.
Glossary
JavaScript is just the language. V8 is the engine that runs it. The browser and Node.js are runtimes: V8 plus extra tools (DOM and fetch in the browser; files and networking in Node). Express is a framework on top of Node for routes and middleware.
| Term | Meaning |
|---|---|
| HTTP / HTTPS | Client sends a request, server sends a response. HTTPS is the same protocol, encrypted in transit. |
| WebSockets | One connection stays open, and both sides can send messages at any time. Chat, live dashboards, games. |
| Runtime environment | What gives JavaScript memory, an event loop, and APIs to talk to the outside world. |
| Browser | Frontend runtime: V8 + browser APIs (DOM, window, fetch). |
| Node.js | Backend runtime: V8 + Node APIs (file system, networking, processes). |
| V8 / libuv | V8 runs your JavaScript. libuv handles async I/O with the OS and tells Node when work is done. |
| Event loop | Keeps asking "is something ready to run?" and runs the matching callback. |
| Callback | A function you pass to another function, to be called later when something is ready. |
| Express | Node web framework: routing, middleware, request/response handling. |
| Ports | 80 = HTTP, 443 = HTTPS (used silently). Any other port must be typed: localhost:3000. |
How a request travels
When I type google.com: DNS gives the IP → the browser connects on port 443 → Nginx decrypts HTTPS and forwards plain HTTP to Node on port 3000 → Node runs my code and queries the database → the response goes back the same way, Nginx encrypts it, and the browser shows the page.
Press ▶ Play or “Next step”. Arrow keys work too.
Debug view
Step through the diagram to see requests and responses.
DNS records: A and CNAME
DNS turns a name into an IP. An A record points a domain straight to an IP. A CNAME points a name to another name. I point www, api and blog at the main domain with CNAMEs, so if the server IP changes, I update just one A record.
Press ▶ Play or “Next step”. Arrow keys work too.
Debug view
Step through the diagram to see requests and responses.
| Record | Maps | Example |
|---|---|---|
| A | domain → IP | yourapp.com → 65.21.100.200 |
| CNAME | name → another name | www.yourapp.com → yourapp.com |
Reverse proxy (Nginx)
Nginx sits in front of my Node app. Users only talk to Nginx on port 443. Nginx decrypts HTTPS, forwards the request as plain HTTP to my app on localhost:3000, and sends the answer back. The same Nginx can spread requests across several app servers (load balancing).
Press ▶ Play or “Next step”. Arrow keys work too.
Debug view
Step through the diagram to see requests and responses.
Why not let users hit Node directly?
- Nginx handles the SSL certificate, so Node never deals with HTTPS.
- Only ports 80 and 443 are open to the internet. Port 3000 stays private.
- One Nginx can send traffic to many app servers, and skip one that's down.
upstream app {
server 10.0.0.11:3000;
server 10.0.0.12:3000;
server 10.0.0.13:3000;
}
server {
listen 443 ssl;
server_name yourapp.com;
ssl_certificate /etc/letsencrypt/live/yourapp.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/yourapp.com/privkey.pem;
location / {
proxy_pass http://app; # plain HTTP to Node
}
}SSH vs SSL
SSL (today really TLS) encrypts the traffic between the browser and my server, which turns HTTP into HTTPS; Nginx handles it. SSH is a completely different tool: it lets me log into my server's terminal from my laptop.
| SSL / TLS | SSH | |
|---|---|---|
| What | Certificate that encrypts browser ↔ server traffic | Remote terminal access to your server |
| Who handles it | Nginx (your Node app never touches it) | You, the developer |
| Command | sudo certbot --nginx -d yourapp.com | ssh ubuntu@yourserverIP |
SSL/TLS is the envelope; HTTPS is HTTP delivered inside that sealed envelope.
Why can't the frontend talk to the database?
Because frontend code is public. To connect, the database password would have to be in the JavaScript, and anyone could read it in DevTools and run any query. The backend keeps the password secret, checks who you are, returns only your data, and runs rules like "check the balance first" the same way for everyone.
Frontend → database
Frontend → backend → database
Press ▶ Play or “Next step”. Arrow keys work too.
Debug view
Step through the diagram to see requests and responses.
Step through the diagram to see requests and responses.
- Security: the database holds everyone's data. The backend returns only what a given user may see.
- Secrets: frontend code is readable by everyone, so the DB password must stay on the server.
- Business logic: a withdrawal is check balance → deduct → record transaction → email → update ledger. In the browser, a user could skip the balance check. The backend runs it once, correctly, for everyone.
CORS and the preflight request
CORS is a browser rule: JavaScript on one site can't read a response from another site unless that server allows it with Access-Control-Allow-Origin. The browser enforces it; the backend decides. For requests like DELETE, or ones with an Authorization header, the browser first sends an OPTIONS preflight to ask permission.
Press ▶ Play or “Next step”. Arrow keys work too.
Debug view
Step through the diagram to see requests and responses.
- Who blocks? The browser.
- Who decides? The backend, with
Access-Control-Allow-*headers. - Backend → database? No browser involved, so there's no CORS there.
A preflight (OPTIONS) happens for PUT/DELETE, custom headers, an Authorization header, or a Content-Type: application/json body.
const cors = require('cors')
app.use(cors()) // development: allow any origin
app.use(cors({ origin: 'https://yourapp.com' })) // production: only your frontendCore principles: client-server and stateless
Every HTTP request is independent and carries everything the server needs, like the Authorization header. The server doesn't remember earlier requests. That's why any server behind a load balancer can handle any request, and I can scale by just adding servers.
Press ▶ Play or “Next step”. Arrow keys work too.
Debug view
Step through the diagram to see requests and responses.
If you do keep something in one server's memory, the next request may land on a different server that doesn't have it. The fix is to keep it in shared Redis or the database.
HTTP versions
HTTP/1.0 opened a new TCP connection per request. 1.1 reuses one connection. HTTP/2 sends many requests at once over one connection. HTTP/3 uses QUIC over UDP, so one lost packet only delays its own request, not all of them.
Press ▶ Play or “Next step”. Arrow keys work too.
Debug view
Step through the diagram to see requests and responses.
Anatomy of an HTTP message
Every HTTP message has four parts: the first line (method + URL, or status code), headers, a blank line, and an optional body.
| Part | What it's for |
|---|---|
| Start line | Request: method + URL + version. Response: version + status code + text. |
| Headers | Extra information about the request or response. |
| Blank line | Separates headers from the body. |
| Body (optional) | The data: sent in POST/PUT/PATCH, returned in responses. A GET normally has no body. |
GET /api/users HTTP/1.1
Host: api.shop.com
User-Agent: Mozilla/5.0 (...)
Accept: application/json
Authorization: Bearer abc123token
Connection: keep-aliveHTTP/1.1 200 OK
Date: Wed, 27 Aug 2025 10:30:00 GMT
Content-Type: application/json
Content-Length: 187
Cache-Control: no-cache
{ "status": "success", "data": [ ... ] }Important headers
Headers are extra information about the request or response: the body format (Content-Type), who I am (Authorization), cookies, caching rules, compression, and security rules like HSTS and CSP.
| Header | Sent in | What it does | Example |
|---|---|---|---|
| Content-Type | Both | Media format of the body | application/json |
| Authorization | Request | Auth credentials | Bearer eyJhbGci… |
| Accept | Request | Response format the client wants | application/json |
| Cookie | Request | Cookies stored in the browser | sessionId=xyz789 |
| Set-Cookie | Response | Create/update a cookie | sessionId=xyz789; HttpOnly |
| Cache-Control | Both | How the response may be cached | max-age=3600, public |
| Content-Encoding | Response | Compression used on the body | gzip |
| Content-Length | Both | Body size in bytes | 1024 |
| User-Agent | Request | Info about the client | Mozilla/5.0 (…) |
| Strict-Transport-Security | Response | Always use HTTPS | max-age=31536000 |
| Content-Security-Policy | Response | Which resources may load | default-src 'self' |
| X-Content-Type-Options | Response | Don't guess the content type | nosniff |
| Referrer-Policy | Response | How much referrer info is sent | strict-origin-when-cross-origin |
Methods and idempotency
Idempotent means sending the same request twice leaves the same final result as sending it once. GET, PUT and DELETE are idempotent; POST is not, so retrying it can create a duplicate order. It matters because networks fail and clients retry.
Idempotency simulator
Send each request, then press “Retry” as if the connection dropped before the response arrived. Watch what the server ends up with.
PUT /users/123idempotent{ "name": "Ali Khan" }Requests received by server: 0
Server state
users id | name 123 | Ali
Nothing sent yet.
POST /ordersnot idempotent{ "item": "Laptop" }Requests received by server: 0
Server state
orders id | item (empty)
Nothing sent yet.
PATCH /accounts/1not idempotent{ "add": 10 }Requests received by server: 0
Server state
accounts id | balance 1 | $100
Nothing sent yet.
PUT /users/123
POST /orders
Press ▶ Play or “Next step”. Arrow keys work too.
Debug view
Step through the diagram to see requests and responses.
Step through the diagram to see requests and responses.
| Method | Intent | Idempotent? |
|---|---|---|
| GET | Read, change nothing | Yes |
| POST | Create / submit | No |
| PUT | Replace the whole resource | Yes |
| PATCH | Partial update | Depends: "add $10" is not; "set price to 900" is |
| DELETE | Delete | Yes: already gone is still gone |
Status codes
2xx success, 3xx go elsewhere or use your cache, 4xx the client made a mistake, 5xx the server failed. The common ones: 200, 201, 204, 301, 304, 400, 401, 403, 404, 409, 429, 500, 502, 503, 504.
| Range | Meaning | Codes |
|---|---|---|
| 1xx | Information | 100 Continue (keep sending the body) |
| 2xx | Success | 200 OK · 201 Created · 204 No Content |
| 3xx | Redirect / cache | 301 Moved permanently · 302 Temporarily elsewhere · 304 Not Modified (use your cache) |
| 4xx | Client problem | 400 Bad request · 401 Not authenticated · 403 Not allowed · 404 Not found · 405 Wrong method · 409 Conflict · 429 Too many requests |
| 5xx | Server problem | 500 Unexpected error · 501 Not implemented · 502 Bad upstream response · 503 Unavailable · 504 Upstream timeout |
HTTP caching
The first response includes an ETag, a fingerprint of the content. Next time the browser sends it back in If-None-Match. If nothing changed, the server replies 304 Not Modified with an empty body and the browser uses its saved copy, which saves bandwidth.
Press ▶ Play or “Next step”. Arrow keys work too.
Debug view
Step through the diagram to see requests and responses.
Express does this for you: res.send() / res.json() add an ETag and answer 304 automatically when If-None-Match matches.
Content negotiation
The client says what it wants with headers: Accept (JSON or XML), Accept-Language, Accept-Encoding (gzip). The server sends the best match from the same URL, or 406 if it can't.
Accept: application/json
Accept: application/xml
Press ▶ Play or “Next step”. Arrow keys work too.
Debug view
Step through the diagram to see requests and responses.
Step through the diagram to see requests and responses.
Compression: with Accept-Encoding: gzip, a 26 MB JSON payload can shrink to about 3.8 MB. Almost every production API enables gzip/br.
app.get('/report', (req, res) => {
res.format({
'application/json': () => res.json({ total: 4820 }),
'application/xml': () => res.type('xml').send('<report><total>4820</total></report>'),
default: () => res.status(406).send('Not Acceptable'),
})
})Large data transfers
For big uploads I use multipart/form-data, which sends the file as raw bytes in parts. For big downloads I stream the file in chunks instead of loading it into memory, set Content-Disposition, and support Range requests so a broken download resumes with 206 Partial Content.
Press ▶ Play or “Next step”. Arrow keys work too.
Debug view
Step through the diagram to see requests and responses.
Content-Type: text/event-stream is for Server-Sent Events (live updates like notifications or progress), not for file downloads.
// download: stream instead of loading the whole file into memory
app.get('/files/:name', (req, res) => {
const file = path.join(FILES_DIR, path.basename(req.params.name))
res.attachment(req.params.name) // Content-Disposition: attachment; filename="..."
fs.createReadStream(file).pipe(res) // chunk by chunk
})
// res.sendFile(file) also handles Range requests (206) for youTLS and HTTPS
TLS encrypts the traffic so nobody can read or change it on the way, and the certificate proves the server is really who it says it is. The browser checks the certificate against its list of trusted Certificate Authorities. HTTPS is just HTTP inside TLS.
Press ▶ Play or “Next step”. Arrow keys work too.
Debug view
Step through the diagram to see requests and responses.
People still say "SSL certificate" out of habit; it's TLS today. Certbot gets and installs a CA-signed certificate for Nginx and renews it automatically:
sudo certbot --nginx -d yourapp.comQuiz
Think of the answer first, then reveal it.
- Q1 · concept
Walk through what happens when you type google.com and press Enter.
- Q2 · concept
What's the difference between an A record and a CNAME, and why is a CNAME useful?
- Q3 · concept
Why can't the frontend connect to the database directly?
- Q4 · concept
Who enforces CORS and who controls it? Does CORS protect your API from curl?
- Q5 · scenario
Your SPA's DELETE request fails, and DevTools shows an OPTIONS request with no Access-Control-Allow-Methods. What's happening?
- Q6 · concept
Why does statelessness make HTTP easy to scale?
- Q7 · concept
HTTP/1.1 vs HTTP/2 vs HTTP/3 in one line each?
- Q8 · concept
What does idempotent mean? Which methods are idempotent?
- Q9 · scenario
A payment request times out and the app retries it. The customer is charged twice. Why, and which methods are safe to retry blindly?
- Q10 · concept
Explain ETag and 304 Not Modified.
- Q11 · scenario
You need to let users download 2 GB videos, and resume if the connection drops. How?
- Q12 · concept
What does TLS protect against, and how does the browser trust a server?