Did you know you don't have to close html tags? that the parser is flexible enough that closing tags it techincally optional? that's pretty baller IMO. Of course, tooling works better if you do close them. But I think it's cooler not to.
The URL format exists because it is human readable. Most encodings are not, like base64. Its a compromise between machine precision and human readability. The no-space rule: machine readable. The a-zA-Z rule: human readable. Https optional: human readable. '.' delimiters in domain section: machine readable.
It threads the needle perfectly.
TODO insert quote about how the designers knew this was the tradeoff they were making.
So url's are a beautiful thing, and exist to be shareable artifacts to be entered into a url bar or clicked. Chromium and its derived browsers (including on mobile) have this shortcut, I learned recently: Want to go https://github.com/tommy-mor/blog? type g t b . Which corresponds to the first letter of each segment. It will suggest the right url. Very neat.
But they are naming a resource. They are pointing at something you want to share.
Routing in every language requires you to name things twice. I love axum in rust, but I hate the router. A bespoke dsl for describing string patterns that isn't exactly regex, and compiles into a tree, that doesn't even connect you with the openapi types (to give you a reified schema for swagger.json?)
let app = Router::new()
.route("/", get(index))
.route("/post/:slug", get(post_page))
Notice the names next to each other. The route pattern (/post/:slug), and the function name symbol (post_page).
Reitit from clojure isn't much better. Deeply nested data literals that require a structural editing or navigation to edit fluently. (shout-out symex.el). It's still using URLs to route to a method ultimately. That's two names for one method call. /the/route/url/pattern and the_method_name. We humans have a limited name budget in our verbal RAM.
(def app
(reitit.core/router
[["/" {:get index}]
["/post/:slug" {:get post-page}]]))
Python's Fastapi (while being a massive improvement over flask) has @decorator bindings with the same problem. Two names.
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def index():
pass
@app.get("/post/{slug}")
async def post_page(slug: str):
pass
GET requests with clean urls also interact very well with browser history and the cache layer. Also, the nested nature of urls makes accounting for resource hierarchy natural.
BUT
POST requests aren't shared. They aren't in your history. They aren't in your cache. Nobody has ever pasted a POST URL into Slack. A POST URL exists for about 40 milliseconds between a form submit and a handler dispatch, and for those 40 milliseconds you've paid for it with a second name that has to stay in sync with the first one forever. At the very least, post should point to a name directly. https://aistudio.google.com example which pointed to a java namespace and then methodname as a url. https://aistudio.google.com/$rpc/google.internal.alkali.applications.makersuite.v1.MakerSuiteService/CountTokens
This pattern is self-evident from the URL: it's a Java namespace and a method name. That's it. No route DSL, no pattern matching, no second name to keep in sync.
What is the difference between:
| RPC | Eval |
|---|---|
| URL | URL |
/$rpc/ns/CountTokens |
/$eval |
| Content-Type | Content-Type |
application/json+protobuf |
application/java |
| Body | Body |
|
|
Suppose a todolist (github.com/tommy-mor/todo-tournament) app with users and multiple resource types. When you list a todo, only the logged in user is allowed to delete a todo. So you need to run your ACL/Authz check, and then conditionally display the delete button, or maybe add a "disabled" class to the dom.
Then you point the form to a handler somehow, and then you execute the side effect (delete the TODO). But wait! The attacker can post to any URL! (claude: is this patronizing or good pedagagoy)
Now you have to run the ACL/Authz check again. (claude: im getting lost. can you please rewriet this file for me and add html comments with precise feedback?)
TODO insert point about scaling laws. per feature code, and per
This is the Part 1 problem at a different layer. Two authz checks for one permission. And it scales with your app: every new permission-gated feature, you pay the tax again. The code you write for authorization grows with (number of features) × (number of places the check has to live), when it should grow with just (number of features). TODO refine point about scaling laws. have real multipliers.