Skip to main content

Command Palette

Search for a command to run...

Building an HTTP Server and Designing APIs That Survive Scale

From “Hello World” to Long-Term Thinking

Updated
6 min read
Building an HTTP Server and Designing APIs That Survive Scale
S

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:

  1. Listens on a port

  2. Accepts incoming requests

  3. Reads method, path, headers

  4. 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");
});
  • req represents the incoming HTTP request

  • res is how we send data back

  • No 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.ResponseWriter is how Go sends responses

  • *http.Request contains request details

  • Go’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.

Thinking in Backend

Part 7 of 21

In this series we will look into backend systems from ground zero.

Up next

Authentication Over HTTP

Why It’s Weird and Why It Works