A single route handler, one data fetch, three response formats. This is the recommended server architecture for html★ backends.
The Problem
html★ fetches HTML fragments from the server and swaps them into the page. But the same endpoints often need to serve other consumers: full HTML pages for initial loads, JSON for mobile apps or APIs. Maintaining parallel endpoint sets is duplication.
The Three-Tier Response Model
A single route handler returns data. The response format is determined by who's asking:
| Request Type | How to Detect | Response |
|---|---|---|
| Initial page load | Normal browser request | Full HTML page |
| html★ swap | X-Requested-With: htmlstar header | HTML fragment |
| API consumer | Accept: application/json | JSON data |
One handler. One data fetch. Three formats. No duplication.
Express Example
app.get('/products/:slug', async (req, res) => { const product = await db.getProduct(req.params.slug) // html★ partial request if (req.headers['x-requested-with'] === 'htmlstar') { res.set('Vary', 'X-Requested-With') return res.type('html').send(renderFragment('product-card', { product })) } // API/JSON request if (req.accepts('json') && !req.accepts('html')) { return res.json({ product }) } // Full page request (initial load, no JS) return res.type('html').send(renderPage('product', { product }))})
Hono Example
app.get('/products/:slug', async (c) => { const product = await db.getProduct(c.req.param('slug')) // html★ partial request if (c.req.header('X-Requested-With') === 'htmlstar') { c.header('Vary', 'X-Requested-With') return c.html(renderFragment('product-card', { product })) } // API/JSON request if (c.req.header('Accept')?.includes('application/json')) { return c.json({ product }) } // Full page return c.html(renderPage('product', { product }))})
Ruby on Rails Example
class ProductsController < ApplicationController def show @product = Product.find_by!(slug: params[:slug]) if request.headers['X-Requested-With'] == 'htmlstar' response.headers['Vary'] = 'X-Requested-With' render partial: 'product_card', locals: { product: @product } elsif request.format.json? render json: @product else render :show end endend
Django Example
from django.http import JsonResponsefrom django.shortcuts import render def product_detail(request, slug): product = get_object_or_404(Product, slug=slug) if request.headers.get('X-Requested-With') == 'htmlstar': response = render(request, 'products/_card.html', {'product': product}) response['Vary'] = 'X-Requested-With' return response if 'application/json' in request.headers.get('Accept', ''): return JsonResponse({'product': product.to_dict()}) return render(request, 'products/detail.html', {'product': product})
The Vary Header
When the same URL returns different content based on request headers, you must set Vary: X-Requested-With. This tells caches (CDNs, proxies, browsers) that the response varies based on that header. Without it, a cached fragment might be served to a full-page request or vice versa.
res.set('Vary', 'X-Requested-With')
Why Not Accept Headers Alone?
html★ sends X-Requested-With: htmlstar rather than relying on the Accept header because:
- Explicit intent —
X-Requested-Withunambiguously says "this is an html★ request" - Browser defaults — Browsers send
Accept: text/htmlfor normal navigation, which overlaps with fragment requests - Cache differentiation —
Vary: X-Requested-Withcleanly separates full-page and fragment caching
The Accept header is still useful for distinguishing HTML from JSON responses.
Pattern A vs Pattern B
You don't always need server-side fragment detection. html★'s data-select attribute can extract fragments from full-page responses client-side:
<!-- Pattern A: Server always returns full page, client extracts --><nav data-target="#main" data-select="#content" data-push> <a href="/products/shoes">Shoes</a></nav>
Pattern A (full pages with data-select) is simpler and handles progressive enhancement automatically. Pattern B (server-side fragments) reduces bandwidth but requires server logic. Start with A, upgrade to B when you need it.
This Site Uses Pattern A
This documentation site uses data-select="#content" to extract the article content from full-page responses. Each page is a complete HTML document, and html★ picks out just the content area during SPA navigation — no server-side fragment logic needed.