Building an HTTP Server and Designing APIs That Survive Scale
From “Hello World” to Long-Term Thinking

I'm a passionate backend dev
Up to now in this series, we’ve talked a lot about ideas.
Why networks are unreliable
Why TCP exists
Why HTTP is stateless
Why REST is a constraint system
At some point, a fair question comes up: “Okay… but what does this look like in actual code?”
This article is about bridging that gap. First, we’ll build the smallest possible HTTP server, in a few different languages. Then, once we have a server, we’ll talk about something much harder: How to design APIs that still make sense a year from now.
Before Frameworks, What Does an HTTP Server Actually Do?
Strip everything down, and an HTTP server does only a few things:
Listens on a port
Accepts incoming requests
Reads method, path, headers
Returns a response
That’s it. Frameworks add convenience. They don’t change this core loop. Let’s see this loop in action.
The Smallest Possible HTTP Server
The goal here is not mastery of syntax. It’s to show that HTTP is the same everywhere.
JavaScript (Node.js)
// Import Node's built-in HTTP module
// This module gives us low-level access to HTTP
const http = require("http");
// Create an HTTP server
// This function runs every time a request comes in
const server = http.createServer((req, res) => {
// Set the HTTP status code to 200 (OK)
// Tell the client we are sending plain text
res.writeHead(200, { "Content-Type": "text/plain" });
// Send the response body and close the connection
res.end("Hello from Node\n");
});
// Start listening for incoming requests on port 3000
server.listen(3000, () => {
console.log("Server running on port 3000");
});
reqrepresents the incoming HTTP requestresis how we send data backNo memory is stored between requests
Every request is handled independently
Python
# Import the HTTP server and handler utilities
from http.server import HTTPServer, BaseHTTPRequestHandler
# Define a function that will handle incoming requests
def handle_request(self):
# Send HTTP status code 200 (OK)
self.send_response(200)
# Send a response header indicating plain text
self.send_header("Content-Type", "text/plain")
# End the HTTP headers section
self.end_headers()
# Write the response body as bytes
self.wfile.write(b"Hello from Python\n")
# Monkey-patch the GET handler
# This tells the server what to do when a GET request arrives
BaseHTTPRequestHandler.do_GET = handle_request
# Create an HTTP server
# It listens on localhost at port 3000
server = HTTPServer(("localhost", 3000), BaseHTTPRequestHandler)
# Start the server and keep it running forever
server.serve_forever()
The server:
listens on a port
waits for requests
runs a handler
sends a response
Still stateless
Different syntax. Same idea.
Go
package main
import (
"fmt" // Used to write formatted output
"net/http" // Go's standard HTTP library
)
// This function handles incoming HTTP requests
func handler(w http.ResponseWriter, r *http.Request) {
// Write a response back to the client
fmt.Fprintln(w, "Hello from Go")
}
func main() {
// Register the handler function for the root path "/"
http.HandleFunc("/", handler)
// Start the HTTP server on port 3000
// The server will block here and keep running
http.ListenAndServe(":3000", nil)
}
Again: listen, handle, respond.
http.ResponseWriteris how Go sends responses*http.Requestcontains request detailsGo’s standard library already assumes statelessness
Pause Here and Notice Something Important
Three languages. Three standard libraries. But conceptually? It’s the same server.
A request arrives
A request handler runs
A response is sent
No memory of the past
This is stateless HTTP in its purest form. Once you understand this loop, frameworks become optional tools, not crutches.
Servers Are Easy
APIs Are Hard. Writing a server that responds is trivial. Designing an API that: survives change, scales with traffic, doesn’t break clients. That’s the real backend work.
Most APIs don’t fail because of performance. They fail because of design decisions that age badly. Let’s talk about that.
APIs That “Work” vs APIs That Last
Many APIs work fine at the beginning. Then:
clients grow
features pile up
assumptions leak
changes hurt
The difference between fragile APIs and durable ones is how they are designed, not what framework they use. From here on, we’ll use JavaScript-style examples, just to keep things consistent.
Design Around Resources, Not Actions
This is where many APIs quietly go wrong.
❌ Action-based endpoints:
POST /createUser
POST /getUserDetails
POST /updateUser
These tie your API to specific behaviors.
✅ Resource-based thinking:
POST /users
GET /users/123
PATCH /users/123
DELETE /users/123
Now the API describes what exists, not what the server does. This makes evolution easier.
Respect HTTP Semantics (They Exist for a Reason)
Using HTTP methods correctly isn’t overly detailed. It’s defensive design.
Example:
GET /users/123
This tells everyone:
no state change
safe to retry
safe to cache
If this endpoint mutates data, everything above it breaks: caches, retries, proxies, expectations. Good APIs cooperate with HTTP instead of fighting it.
Make Inputs and Outputs Explicit
Hidden behavior is the enemy of scale. Bad pattern:
behavior changes based on hidden headers
implicit defaults
undocumented side effects
Better pattern:
explicit request bodies
explicit query parameters
explicit responses
Example:
POST /users
{
"email": "user@example.com",
"name": "Suman"
}
Explicit Outputs
A good API is also clear about responses.
Success response:
HTTP/1.1 201 Created
Content-Type: application/json
{
"id": "123",
"email": "user@example.com",
"name": "Suman"
}
Error response:
HTTP/1.1 400 Bad Request
Content-Type: application/json
{
"error": "email is required"
}
Now the client knows:
What happened
Why it happened
What to do next
Clear input. Clear outcome.
APIs are contracts. Vagueness is technical debt.
Treat APIs as Promises, Not Functions
This mindset shift matters a lot. Functions can change freely. APIs cannot. Once clients depend on your API:
removing fields is dangerous
changing meanings is breaking
assumptions spread quickly
Design APIs as if you’ll regret changing them later. Because you will.
Versioning Is a Last Resort
Versioning feels like a solution. It’s usually a symptom. Good API design tries to:
add fields instead of changing them
keep old behavior working
evolve without forcing migrations
Versioning should be intentional, not habitual.
Why This Matters at Scale
When APIs are designed well: services can be replaced, teams can move independently clients don’t break constantly.
When APIs are designed poorly: coordination explodes, fear slows development, changes become painful.
This has nothing to do with frameworks. Everything to do with thinking.
The Thread So Far
Let’s zoom out.
We learned how data moves
How delivery is handled
How HTTP communicates
How statelessness works
How meaning is encoded
How REST constrains systems
This article is where it all becomes usable.
What Comes Next
Now that we can build servers and design APIs, the next hard problem appears: “How do we secure this without breaking everything?”
That’s where authentication enters.
➡️ Next article:
Authentication Over HTTP: Why It’s Weird and Why It Works
This article is part of the Thinking in Backend series, where we learn backend engineering by focusing on how systems think, not just how code runs.




