Unplanned Obsolescence


The Server Doesn't Render Anything

June 17, 2025

When I advise people on how they should structure a web service, I always start from the same place: make a server that responds to HTTP requests with HTML text. That is the most durable, cost-effective, and user-friendly way to build a web service. Most web services should be built this way absent an excellent reason not to.

Upon hearing this, web developers often reply “oh, you like server-side rendering,” to which I usually wince and answer “more or less.” You have to pick your battles when chipping away at a decade of miseducation. At least people know what I’m talking about.

But “server-side rendering” is a horrible term. It implies that the server is not just doing more work, but doing hard work, work that’s best left to the experts. None of this is true. You, too, can do server-side “rendering,” with essentially no effort, in whatever programming language you prefer.

Once you understand that, you’ll start to see the web the way I do: as the simplest, easiest, and most powerful interface for computation ever created.

HTML is just text

Wherever you can print text, you can make HTML.

Here’s an example in Python, which extends Python’s built-in HTTP server so that it responds to every GET request with the same text: <h1>Python webpage!</h1>

from http.server import BaseHTTPRequestHandler, HTTPServer

WEBPAGE = "<h1>Python webpage!</h1>\n"

class HTMLServer(BaseHTTPRequestHandler):
    def do_GET(self):
        self.send_response(200)
        self.end_headers()
        self.wfile.write(str.encode(WEBPAGE))

webServer = HTTPServer(("localhost", 8080), HTMLServer)
print("Server running at http://localhost:8080")
webServer.serve_forever()

This would be even simpler with flask, but I’m trying to make a point here by using a basic server without any dependencies. There’s no magic that makes HTML text become HTML code. We didn’t even set the Content-Type header to text/html.

Run that script and then try interacting with the server via curl. You’ll see that it’s just text.

$ curl localhost:8080
<h1>Python webpage!</h1>

What if we open http://localhost:8080 in a browser? Instead of showing plain text, the browser will render that HTML into something more dynamic. The <h1> tags are gone and the remaining text is big and bold.

To incorporate color and a fun font, like we did in “The Best ‘Hello World’ in Web Development,” simply add a <style> tag to the string.

from http.server import BaseHTTPRequestHandler, HTTPServer

WEBPAGE = """
<style>
body {
  background-color: lightblue;
  font-family: 'Comic Sans MS', cursive;
}
</style>
<h1>Python webpage!</h1>
"""

# Server code omitted for clarity

Now curling the endpoint will show the additional style tag, and the browser will render the HTML with a nice blue background and a comic sans font.

The browser does the rendering

Did you notice that I used the word “render” twice in the previous section? Both times to refer to actions the browser took, namely the transformation of this text—

<style>
body {
  background-color: lightblue;
  font-family: 'Comic Sans MS', cursive;
}
<h1>Python webpage!</h1>

—into a webpage.

Even something as “simple” as rendering header text on a blue background is a very complicated process. Chapter 3 of Panchekha & Harrelson’s excellent book, Web Browser Engineering, has a basic introduction to the steps involved. Let’s drop in on the part where they talk about measuring text:

Remember that bi_times is size-16 Times: why does font.metrics report that it is actually 19 pixels tall? Well, first of all, a size of 16 means 16 points, which are defined as 72nds of an inch, not 16 pixels, which your monitor probably has around 100 of per inch. Those 16 points measure not the individual letters but the metal blocks the letters were once carved from, so the letters themselves must be less than 16 points. In fact, different size-16 fonts have letters of varying heights.

Okay.

Just getting a couple letters on the page requires layout math that most web developers have never even considered. All this is learnable (that’s what the book is for), but web rendering is astoundingly complex. Imagine trying to implement kerning; instead, you get it for free.

The reason not to call HTML text generation “rendering” is because rendering really is a difficult, complicated problem, it’s just not one the website author ever has to think about. Browser engineers have taken care of it. The required software in every person’s pocket.

All the website author has to do is print text surrounded by tags—no math required.

Expressing data as HTML

What is the appropriate framing for this concept, if not “server-side rendering?” It’s text generation, yes, but more precisely: we are expressing our data as HTML text. Not only is this technique universally available without specialized tools, it’s kind of fun!

boroughs = [
  "The Bronx",
  "Manhattan",
  "Brooklyn",
  "Queens",
  "Staten Island"
]

# Using simple string operations,
# we can express this list as HTML
LIST = "<li>".join(boroughs)
WEBPAGE = "<h1>NYC Boroughs</h1><ul><li>" + LIST + "<ul>"

Manipulating strings is Coding 101. Learn a couple HTML elements and you can use basic string operations to build an interactive view of whatever your code accomplishes. The resulting text isn’t very pretty outside the browser, but inside the browser, it gets the job done.

<h1>NYC Boroughs</h1><ul><li>The Bronx<li>Manhattan<li>Staten Island<li>Brooklyn<li>Queens</ul>

You could, of course, choose to express the same dataset as JSON, using the same techniques.

boroughs = [
  "The Bronx",
  "Manhattan",
  "Brooklyn",
  "Queens",
  "Staten Island"
]

LIST = '","'.join(boroughs)
WEBPAGE = '{ "nyc_boroughs": ["' + LIST + '"] }'

# Server code omitted for clarity

Although, this doesn’t really accomplish all that much, because JSON doesn’t have hypermedia controls. But it’s at least possible, if you need quick and dirty JSON output and don’t have access to a JSON library for some reason.

{ "nyc_boroughs": ["The Bronx","Manhattan","Brooklyn","Queens","Staten Island" ] }

The point is not that you should (necessarily) be generating HTML or JSON via string manipulation, it’s they both operate at similar levels of difficulty and abstraction; I object to the term “server-side rendering” because it implies otherwise.

To start “server-side rendering” all you have to do is format your data as HTML, and return that from the server.

So what do we call this?

My preferred term is “HTML APIs.” Developers are familiar with JSON APIs, and an HTML API works exactly the same way, only it returns HTML, instead of JSON. “HTML responses” works too.

(HTML APIs are also REST APIs, but if you say “REST APIs” then you’ll have to send your coworkers a second article, so save that one for later.)

A lot of people get hung up on the idea that HTML can’t be an API (Application Programming Interface), because HTML is meant to be read by humans and APIs are meant to be read by computer software. But that isn’t quite true.

Take a look at the NYC Boroughs list in both JSON and HTML, side-by-side.

<h1>NYC Boroughs</h1>
<ul>
  <li>The Bronx
  <li>Manhattan
  <li>Staten Island
  <li>Brooklyn
  <li>Queens
</ul>
{
  "nyc_boroughs": [
    "The Bronx",
    "Manhattan",
    "Brooklyn",
    "Queens",
    "Staten Island"
  ]
}

Neither of these is actually intended to be read by the end-user. The end-user is supposed to see a formatted list!

HTML is a hypermedia format, so it contains structured data and a standard interface that the browser can render. JSON APIs only encode the data; they lack the representation. Using an HTML API doesn’t move complexity from the client to the server—it eliminates the less-useful JSON representation altogether.

From this perspective, the HTML API is a software-to-software communication protocol: the software that the server is talking to is the user’s browser, instead of a client-side JavaScript application. The user’s browser reads the HTML API, and renders it as a Graphical User Interface (GUI).

Making real websites

When making real websites, rather than scripts, you want to use tools that are slightly more advanced than string joins. A good place to start is with a template engine.

HTML is a standardized text data format, so it has template engines in pretty much any programming environment, often with syntax that feels native to the language (e.g. zig, ocaml, common lisp). Template engines have helpful affordances for API code re-use. Most importantly, they are equipped with secure defaults for user-generated content, because any content you dynamically insert into an HTML document should be escaped.

Templates are easy to understand—they are a straightfoward automation for building text output—and therefore easy to debug. If text is missing, or escaped improperly, or in the wrong place, there aren’t a lot of places that the mistake could be hiding.

The website is the easy part

Over in the React universe, Dan Abramov has been writing a lot about React Server Components (RSCs). Much of his writing is very insightful—“Progressive JSON” in particular is worth a read.

While reading “JSX Over The Wire”, however, I couldn’t stop thinking about how complicated all this is. It’s essentially transforming all your UX logic into JSON and then transforming it back into HTML; HTML generation with a bunch of additional intermediate steps. Why do at all that when you could just generate the HTML in the first place?

In Dan’s words:

React Server Components are the React team’s answer to the question that plagued the team throughout the 2010s. “How to do data fetching in React?”

React Server Components start from the premise that have to use React, that you’re unwilling to learn how REST works, that the data on one page has wildly varying load characteristics, and that the client and server are abstract ideas rather two different environments with fundamentally different security models.

If those premises hold for you—and I’m not sure they should hold for anyone not building a globally distributed social network—then by all means, learn RSCs. Just don’t make the mistake of thinking the complexity of RSCs reflects some inherent complexity in developing for the web.

Websites are not hard. There are many pitfalls to building a dynamic web service, one with logins, databases, user-generated content, and all that (this is a professional skillset). The website part, however, the expression of your server’s data as an interface in the user’s browser, is quite easy. You really can do it with almost no specialized tools.

After all, it’s basically just string joins.

Thanks to Meghan Denny for her feedback on a draft of this post.

Notes