I built a link shortener with FastAPI and htmx (no JS framework) — the parts that were actually hard

2026年9月5日1 次浏览来源:Dev.to阅读原文

"A URL shortener" sounds like a weekend project.

Slug in, long URL out, , done.

That's what I thought too.

Then real usage showed up: links opened inside Instagram's in-app browser and didn't convert, bot traffic wrecked the analytics, and one link needed to send a US visitor somewhere different from an EU visitor.

Suddenly the "trivial" part was 5% of the work.

I built the whole thing on FastAPI + Redis + MySQL + htmx, deliberately with no frontend framework.

This post is about the parts that turned out to be interesting — the redirect hot path, geo/device routing, and escaping in-app browsers — and why htmx was the right call for a one-person team.

Disclosure: I build tapurl.io, a link shortener for marketers.

This is a write-up of the engineering behind it, not a pitch — everything below is patterns you can apply to any shortener.

The redirect is a hot path, so treat it like one Every other page in the app can be a bit slow.

The redirect cannot.

It sits in front of someone's click, and it runs on every click, so it has to be a tight, predictable read.

The naive version hits your database for every redirect: That's fine until you have traffic.

The slug-to-link lookup is a near-perfect cache candidate — a slug maps to the same link record every time.

So the real path reads from Redis first and only falls back to MySQL on a miss: Two things worth saying out loud: Cache the lookup, not the decision.

You cache the link record, but the actual destination is still computed per click from the routing rules (below).

And when someone edits a link's rules, invalidate its cache key — otherwise you'll happily serve stale destinations from Redis.

Analytics must not block the redirect.

Recording the click (country, device, referrer) happens after you've already decided where to send the user — push it to a background task or a queue, never make the visitor wait on a write.

Prefer 302 over

301.

A permanent redirect gets cached by browsers and you stop seeing clicks.

For anything you want to measure — or ever re-point — you want a temporary redirect.

The exception is when you need a page in between (more on that in the in-app-browser section): then you serve a lightweight interstitial instead of redirecting straight away. htmx for the dashboard, and I don't miss React The dashboard is a normal CRUD app: lists of links, click charts, forms for routing rules.

The default 2026 instinct is React + an API.

I went the other way: server-rendered Jinja2 templates with htmx for the interactive bits.

The pitch for htmx is that you get partial updates without shipping a SPA.

A button that adds a routing rule just asks the server for the new row: The server returns HTML, not JSON: Why this fit a solo project: No build step.

No bundler, no , no separate frontend deploy.

The thing that renders the page is the thing that has the data.

One mental model.

State lives on the server.

I'm not reconciling a client store with a database.

Tiny payloads.

Pages ship almost no JS, which — for a product whose whole value is fast redirects — is on-brand.

It's not free.

Anything genuinely stateful and client-heavy (a live-updating chart) still needs real JavaScript, and htmx doesn't change that.

But for a forms-and-lists dashboard, it removed an entire category of work.

Geo and device routing: one link, many destinations This is the first feature that made it not a shortener.

The requirement: one short link where a US visitor goes to with a US tag and an EU visitor goes to with an EU tag — decided at click time.

The pieces: Country from IP.

A local MaxMind GeoLite2 database keeps this fast and avoids a network call on the hot path. (Local lookup ≈ microseconds; an external geo-API call would blow your redirect latency budget.) Device from the User-Agent.

Coarse is fine — mobile / desktop / tablet, plus OS when you need iOS vs Android.

Rules with priority + a fallback.

Rules are ordered; the first match wins; if nothing matches, you must have a fallback destination.

A routing feature without a fallback is a 404 generator.

The subtle bug I hit: rule order is the whole product. "US → A, everything else → B" and "everything else → B, US → A" are different links, and if your UI lets people reorder rules but your resolver reads them in insert order, you'll ship confident, wrong redirects.

Make priority explicit and test it.

The genuinely hard one: escaping in-app browsers Here's the problem almost nobody documents.

Someone taps your link inside Instagram or TikTok.

It opens in that app's in-app webview — a stripped browser where the user isn't logged into anything.

If your link points at a destination that has a native app (YouTube, Spotify, Amazon), the conversion falls off a cliff, because the visitor would have to log in by hand instead of landing in an app where they're already signed in.

The fix is deep-linking: bounce the user out of the webview into the native app.

On the web platform this leans on Universal Links (iOS) and App Links (Android).

And here's the honest part that took me longest to accept: Android is reliable.

App Links resolve cleanly; you can get people into the native app most of the time. iOS has a hard ceiling.

You cannot programmatically force an escape from every in-app browser 100% of the time — the platform doesn't allow it.

Anyone claiming a silver bullet here is overselling.

So the correct design isn't "guarantee the escape." It's: attempt the escape, and always render a visible fallback button ("Open in app") for the cases the platform won't let you handle silently.

Then measure it — track escape attempts vs. successes, split by OS, because Android and iOS numbers are so different that a blended number is meaningless.

The lesson generalizes: when a platform gives you a ceiling, don't hide it behind a claim you can't keep.

Design for the ceiling and make the fallback good.

Don't let bots into your analytics If you fire tracking and count clicks on every request, bots and link-preview crawlers (every time a link is pasted into a chat app, something fetches it) quietly poison your data — and if you fire retargeting pixels, they poison your ad audiences too.

So bot detection runs before anything is counted or fired.

Known crawler UAs, headless signatures, and preview fetchers get the redirect (you don't want to break link previews) but are excluded from analytics and never trigger pixels.

Real humans get counted.

It's not glamorous, but it's the difference between analytics you trust and a dashboard full of lies.

Closing the loop: conversion postbacks The last piece, for the affiliate use case, is server-to-server postbacks.

You attach a macro to the outgoing URL; the destination's system returns it later on a conversion (, , ); you match it back to the original click.

The one thing that will bite you: dedup.

Networks retry and re-fire postbacks, and a conversion can move .

Deduplicate on and model status as a transition — a conversion comes in and later flips to — rather than counting every postback as a new event, or you'll double-count revenue.

Would I do it this way again?

Yes, with one asterisk.

FastAPI + Redis + MySQL + htmx let one person ship a product that does real routing, deep-linking and attribution without a frontend team or a build pipeline.

The redirect stays fast because the hot path is a Redis read, and the dashboard stays maintainable because there's one source of truth.

The asterisk: htmx is a genuine sweet spot for forms-and-lists, but know where its edge is.

The moment you need rich, continuously-updating client state, you're writing JavaScript again — and that's fine, just don't fight the tool past its range.

If you want to see the finished product, it's tapurl.io.

And if you've solved the iOS in-app-browser escape more completely than "attempt visible fallback," I'd genuinely love to hear how — that one still feels unfinished.

分享
Baike.dev

baike.dev helps you discover great languages, frameworks, databases, DevOps and cloud-native tools.

Quick links

About

Contribute

Found a great developer tool? Share it with the community.

Submit a tool
© 2026 baike.dev Developer EncyclopediaUpdated daily · Discover great developer tools