A URL shortener looks like a small feature: save a long URL, generate a code, and redirect anyone who visits it.
But a link can outlive the interface that created it. It may sit in an email, a saved message, or a printed brochure for months. The redirect becomes a public contract between that artifact and the destination.
Several decisions hide inside that contract: whether the destination can change, whether caches can bypass the redirect service, which query parameters reach the landing page, and what a “click” actually means.
This is a reference architecture for developers. It does not describe BatchSet's internal status codes, cache configuration, event pipeline, or bot filtering. Those require implementation evidence, not assumptions from a product page.
1. Choose the behavior before choosing the status code
Suppose a business prints this illustrative address:
https://go.example.com/summer-menu
The destination is a seasonal menu. The business expects to replace it later without reprinting the card.
That is different from permanently moving a documentation page to a new canonical location.
| Status | Meaning relevant to this design | Method behavior |
|---|---|---|
| 301 | Permanent redirect | Clients may change POST to GET |
| 302 | Temporary redirect | Clients may change POST to GET |
| 307 | Temporary redirect | Preserves the request method and body |
| 308 | Permanent redirect | Preserves the request method and body |
For an ordinary navigation-only short-link endpoint, I would explicitly support GET and HEAD and reject other methods. A temporary redirect can fit an editable destination. That is a design choice, not a universal requirement.
If an application really needs to forward non-GET requests, method preservation becomes a separate, deliberate decision. Sending a submitted body to a different destination can have consequences that do not exist for a simple link click.
See the HTTP semantics in RFC 9110, redirection, along with MDN's 302 and 307 references.
2. Cache policy is part of editability
A temporary status does not substitute for a cache strategy.
If a client or intermediary reuses a stored redirect, an updated destination may not be consulted on that visit. The same reuse can keep the request from reaching the application analytics handler.
For a small editable-link service, a straightforward reference policy is:
HTTP/1.1 302 Found
Location: https://shop.example.com/menu
Cache-Control: no-store
no-store tells compliant caches not to store that response. no-cache has a different meaning: storage is allowed, but reuse requires validation. MDN explains the distinction.
This does not erase older cached redirects or make browser history behavior identical across clients. Configure and test the CDN as well as the application. Some deployments have explicit platform rules that must be reconciled with response headers.
At higher volume, you may cache the destination lookup internally while keeping each public redirect observable. That creates a new obligation: invalidate the mapping promptly when someone edits or disables the link.
Caching a database lookup and caching an HTTP redirect are different operations. Name them separately in the design.
3. A redirect request is not proof of a human click
Imagine sharing a link in a messaging app. The app may retrieve it to build a preview. A security scanner may inspect it before the recipient opens the message. A user may also visit twice.
A request counter can be useful without claiming to count people.
| Metric | What it can mean | What it does not prove |
|---|---|---|
| Redirect requests | Requests observed by the redirect service | Human intent |
| Filtered requests | Requests remaining after a stated filter | Perfect bot exclusion |
| Estimated unique visitors | Deduplicated observations under a stated rule | Exact distinct people |
| Landing-page sessions | Sessions recorded at the destination | Every short-link request arrived |
| Conversions | A defined destination-side event | Every visit caused a sale |
User agents, request methods, timing, and known crawler signatures can inform filtering. None of them is a flawless human detector.
The product decision I recommend is to document the metric and retain the distinction between raw observations and derived estimates. Avoid replacing one ambiguous label with a more impressive ambiguous label.
If a dashboard says “clicks,” its help text should explain what is counted and what may be missing.
4. Make analytics failure a deliberate trade-off
There are two tempting extremes:
- Wait for every analytics write before redirecting, so logging failures can delay navigation.
- Start an unawaited promise and return immediately, assuming the hosting runtime will finish it.
Neither is automatically correct.
For a reference design, a bounded durable enqueue can keep the event path short. The service can choose to continue redirecting when event ingestion fails, record the failure, and accept that the analytics stream may have a gap.
An alternative is logging at an edge or request layer with a suitable durability contract. Platform-specific background execution should use the platform's supported mechanism, not a detached promise with no lifecycle guarantee.
Define the desired behavior during an outage:
- Does navigation continue?
- Can the failed event be recovered?
- Can operators see the missing-data condition?
- Does retrying an event create duplicates?
A unique event ID helps a consumer deduplicate retries. It does not make an event pipeline “exactly once” by itself.
These are engineering choices for a new implementation. They are not claims about how BatchSet currently records clicks.
5. Validate destinations without claiming they are trustworthy
For a link that should open a website, require an absolute HTTP or HTTPS URL and reject embedded credentials.
function parseDestination(input) {
const destination = new URL(input);
if (!["http:", "https:"].includes(destination.protocol)) {
throw new Error("Use an absolute HTTP or HTTPS URL");
}
if (destination.username || destination.password) {
throw new Error("Embedded credentials are not supported");
}
return destination.href;
}
This is syntax and scheme validation. It does not establish that a website is honest, safe, reachable, or owned by the creator.
Production abuse handling needs its own decisions: creation limits, reserved codes, reporting, review, disabling malicious links, and appropriate checks for private-network destinations if those should not be supported.
If you later add server-side destination previews, the threat model changes again. A browser redirect does not require your server to fetch the destination; generating a preview does. That new fetcher needs its own SSRF protections.
6. Do not accidentally create a public open-redirect parameter
This pattern makes the saved mapping irrelevant:
/r/summer-menu?destination=https://unexpected.example
If any visitor can override the target, the short-code owner no longer controls what the public artifact means.
Prefer a stored, authorized mapping. Editing that mapping should require permission. Anonymous visitors should only resolve it.
Also decide what happens to query parameters. If the saved destination already has campaign tags, blindly appending every incoming parameter can overwrite attribution or introduce parameters the landing application never expected.
My default for a reference service is do not forward incoming parameters. If forwarding is needed, use a small allowlist and define precedence:
function addAllowedCampaignParameters(savedURL, incomingURL) {
const target = new URL(savedURL);
const incoming = new URL(incomingURL);
for (const key of ["utm_source", "utm_medium", "utm_campaign"]) {
const value = incoming.searchParams.get(key);
if (value !== null && !target.searchParams.has(key)) {
target.searchParams.set(key, value);
}
}
return target.href;
}
The saved value wins here. Other applications may choose differently, but the rule should be intentional and testable.
Campaign parameters should not carry passwords, personal email addresses, or other secrets. URLs can be retained in histories, logs, screenshots, and downstream systems.
7. Test the public contract, including negative cases
A useful acceptance matrix for a short-link implementation includes:
| Scenario | Expected decision to verify |
|---|---|
| Existing code, GET | Correct destination and cache policy |
| Existing code, HEAD | Deliberate response and counting policy |
| POST to navigation endpoint | Rejected if unsupported |
| Unknown code | Clear not-found response |
| Disabled or expired code | No silent redirect to an unrelated page |
| Destination edited | Subsequent fresh requests use the new target |
| Analytics unavailable | Navigation follows the documented outage policy |
| Replayed event | Consumer deduplicates under its stated rule |
| Unexpected query parameter | Cannot override the destination |
| Non-web scheme or credentials | Rejected at creation and edit time |
Use example domains when testing destination behavior. Do not test a forwarding policy by sending real form data to arbitrary external sites.
Also compare redirect counts with destination-side analytics, expecting disagreement. A discrepancy is a debugging starting point, not proof that either system measures the same event incorrectly.
Put the link workflow to use
BatchSet offers a URL Shortener with click analytics and optional custom codes, available with a free account.
If you want to organize links for a campaign, start there. Verify the final destination before sharing, keep campaign labels consistent, and interpret click reporting alongside landing-page outcomes.
A short link should be easy to share. The meaning of its destination and its metrics should be just as easy to explain.
Top comments (0)