Tags: web-dev concept

Routing and Resolution

Date: 2026-08-16


Every web system has to decide which code handles a given URL. There are two answers — the code declares the URLs, or the content does — and which one a platform picked explains most of what it feels like to work in.


Routing and resolution is the mechanism by which a request URL is matched to the code that produces a response.

The distinction worth holding:

ROUTE-CENTRIC              CONTENT-CENTRIC
─────────────              ───────────────
code declares the URLs     content IS the URL
  /products/:id              /content/en/shoes
      ↓                            ↓
  a handler function         read the node's
      ↓                        own type
  it fetches content               ↓
                             find a renderer
                               for that type

In one line: route-centric asks “what handles this pattern?”; content-centric asks “what is the thing at this path, and what renders that kind of thing?”


The three shapes

Route-centric — a table of patterns, declared in code.

// Express
app.get('/products/:id', (req, res) => {
  const product = db.find(req.params.id)
  res.render('product', product)
})

The URL space is fixed by code. A new URL is a deploy.

Convention-centric — the filesystem is the route table.

app/products/[id]/page.tsx   →  /products/:id
app/about/page.tsx           →  /about

Same model, less typing. The URL space is still fixed by code, so a new URL is still a deploy — Next.js, Astro, SvelteKit.

Content-centric — the content declares its own type, and the type selects the renderer.

GET /content/site/en/shoes.html
   ↓
node at /content/site/en/shoes
   ↓  the node's own property says
   sling:resourceType = "mysite/page"
   ↓
render /apps/mysite/page/page.html

The URL space is fixed by content. A new URL is an author clicking create — no deploy, no developer. This is AEM’s Sling, and it’s the model most CMSes approximate — Adobe Experience Manager.


The pattern you’ve already met

WordPress’s template hierarchy is content-centric resolution with a fallback chain, and it’s the clearest small example of the idea:

request resolves to a "product" post
   ↓  try, in order
single-product.php
single.php
singular.php
index.php          ← always exists

Nothing declared that route. The content’s type selected the renderer, and the chain guarantees a result. That’s resolution, not routing.


What changes downstream

The choice is not stylistic. It decides who can do what:

Route-centricContent-centric
A new pagea deployan author saves
A new page typea deploya deploy
URL shape followsthe code’s designthe content tree
Renaming contentURL unaffectedURL moves
Who owns the URL spaceengineeringeditorial
The sitemap comes fromthe route tablethe tree

The rename row is the one that bites. If the URL is a path into the content tree, moving or renaming content moves the URL — so every reorganisation is a redirect exercise, and forgetting it silently destroys accumulated link equity — Redirects and Link Equity, Technical SEO.

Platforms that know this decouple the two: Shopify freezes a product’s handle at creation so renaming the title never changes the URL, which is a deliberate patch on exactly this problem — Shopify.

The row that gets missed: a new page type is a deploy in both models. Content-centric buys authors freedom over instances, never over kinds. Teams often expect otherwise and are disappointed.


Hybrids, which is most real systems

Almost nothing is pure. Shopify is the clean example:

FIXED ROUTE TABLE      CONTENT PICKS RENDERER
/products/:handle  →   the product's assigned
                       template decides:
                       product.json
                       product.bundle.json

Routing gets you to the product; resolution decides how it renders. Most mature platforms land here, because the two models are good at different halves of the job.


The deeper idea

This is dispatch — the general question of who decides which code runs.

CALLER DECIDES       THE THING DECIDES
if (type == 'a')     shape.draw()
  drawA()
else if (b)          ↑ polymorphism
  drawB()

Content-centric resolution is polymorphism applied to URLs: the resource carries its own type, and the type selects the behaviour. That’s why adding a new content type doesn’t require touching a central registry — the same reason adding a subclass doesn’t require touching a switch statement.

Route tables are the switch statement. They’re explicit, greppable and easy to reason about, and they centralise every change — Coupling and Cohesion.


Choosing

  • Who needs to create URLs? If non-developers publish pages daily, content-centric or a CMS. If the URL space is small and stable, a route table is simpler and clearer
  • How many content types? Few types, many instances favours content-centric. Many types, few instances favours explicit routes
  • Is the URL structure a product decision or an editorial one? Content-centric hands it to editorial, permanently
  • What’s the caching key? Route-centric caches by pattern; content-centric caches by path, which is finer-grained and harder to invalidate in bulk — Caching Strategies

The failure mode of route-centric is a bottleneck: every new landing page is a ticket.

The failure mode of content-centric is that nobody knows what the URL space contains, because it grew by authoring rather than by design. A sitemap becomes a discovery exercise rather than a fact.