AlgoMaster Logo

URL Shortener API

20 min readUpdated September 13, 2026
Listen to this chapter
Unlock Audio

A URL shortener turns a long destination into a compact link that people can share. The basic operation is small, but the API must answer several questions: Who can change the destination? What happens when a client retries creation? How quickly does a disabled link stop working? What does a reported click actually measure?

This chapter designs a URL Shortener API around those decisions. We will define the resource, work through its HTTP contract, and connect redirects, lifecycle rules, and analytics into one consistent design.

1. Requirements and Boundaries

Assume a service for teams sharing campaign and documentation links. Team members manage links through an authenticated JSON API at api.short.example. Visitors open public links on go.short.example without authenticating to the shortener.

The first release supports creating links, optional custom codes, listing and retrieving owned links, changing destinations, disabling links, expiration, deletion, and aggregate redirect statistics. Custom domains, password-protected links, previews, and geographic routing are outside this design.

The product makes three explicit promises:

  • A successful creation returns a link that is ready to resolve.
  • Changes affect redirect lookups started after the successful write response; the service cannot recall a redirect it has already issued.
  • Analytics are delayed and approximate, and their failure does not prevent a redirect.

These promises are design choices, not automatic consequences of using REST. In particular, the second requires coordination between writes and every serving location. A deployment that allows stale replicas must publish a weaker propagation guarantee instead.

The diagram separates the two audiences and their contracts.

The management API returns link metadata. The public endpoint tells the visitor's client where to navigate; it does not proxy the destination page. Separate hosts also keep management routes and authentication cookies away from the public code namespace.

2. Resource and Endpoint Design

Model a link as an owned resource with a stable management ID and an immutable public code. The management ID, such as lnk_7m2p9q4r, identifies the resource in authenticated operations. The public code, such as autumn-guide, appears in the short URL.

Keeping these identities separate lets the API use uniform management identifiers while supporting human-readable public codes. Neither identifier grants management access.

Scroll
FieldMeaningClient control
idStable management identifierRead-only
codeUnique public path segmentOptional at creation; immutable afterward
shortUrlComplete public HTTPS URLRead-only
destinationUrlAbsolute destination URLRequired at creation; editable
enabledOwner-controlled redirect switchDefaults to true; editable
expiresAtUTC expiration time, or null for no expirationOptional at creation; editable before expiration
createdAt, updatedAtServer-generated timestampsRead-only

There is also an internal abuse block that owners cannot clear. It overrides enabled. Deletion is terminal, and expiration is terminal in this release: an owner must create a new link after expiration. This avoids an old expired link unexpectedly becoming active again.

The endpoint surface is deliberately small.

Scroll
HostMethod and pathPurposeSuccess
api.short.examplePOST /v1/linksCreate a link201
api.short.exampleGET /v1/linksList the caller's team links200
api.short.exampleGET /v1/links/{id}Retrieve management metadata200
api.short.examplePATCH /v1/links/{id}Change allowed fields200
api.short.exampleDELETE /v1/links/{id}Permanently remove a link204
api.short.exampleGET /v1/links/{id}/statsRetrieve aggregate statistics200
go.short.exampleGET /{code}, HEAD /{code}Resolve a public link302

Management requests require a bearer token scoped to one team. The service derives ownership from the verified token rather than accepting a writable teamId. It checks team membership and operation permission on every resource access, including statistics and collection queries.

Examples use HTTPS, placeholder credentials, and omitted transport framing headers for readability. Times and limits below are illustrative product contracts.

3. Creating a Link

A team member creates a campaign link with a chosen code:

Location identifies the created management resource. shortUrl is the address to share. Returning both avoids making clients construct URLs or confuse a public redirect with a JSON endpoint.

Destination Validation

For this service, destinationUrl must be an absolute http or https URL with a host and no more than 4,096 UTF-8 bytes. Reject embedded credentials, control characters, malformed escapes, and destinations on the shortener's own public host. The size limit is a product limit, not a universal maximum URL length.

Use a URL parser and validate the parsed result. Preserve destination path case, query order, encoding, and fragments; changing these can change the target or invalidate a signed URL. Define any permitted normalization and return the stored value to the caller. Do not silently upgrade an HTTP destination to HTTPS unless the product explicitly supports that transformation.

The service checks syntax and policy, not whether the destination will always exist. It does not fetch a page during creation. Rejecting its own host prevents direct self-links and same-service chains, but cannot rule out redirect loops through external websites.

An unsupported scheme receives a semantic validation error:

code, errors, and their nested fields are application-defined extensions to Problem Details. Clients branch on stable codes, not the wording of message. Malformed JSON receives 400; an unsupported request media type receives 415.

Code Allocation

Custom codes must match [a-z0-9][a-z0-9-]{3,31}: 4–32 lowercase ASCII characters, starting with a letter or digit. The service rejects uppercase input rather than silently converting it to lowercase. Requests to the public endpoint also use exact code matching. Reserve operational names such as health and admin before exposing the namespace.

When the client omits code, generate a random code using the same alphabet and a cryptographically secure random generator. Enforce uniqueness atomically across the public host. A preliminary availability check cannot prevent two concurrent requests from claiming the same code.

If a generated code collides with an existing code, the service tries another candidate internally. A custom-code collision returns 409 Conflict with code: "code_unavailable". Reserved and previously used codes receive the same response, without identifying the owner. The service never reassigns codes, including after deletion, so an old email or printed QR code cannot later redirect to another team's destination.

The service does not deduplicate by destination URL. Two campaigns can intentionally point to the same page while having separate ownership, expiration, and statistics.

Creation Retries

Require Idempotency-Key on creation and document it as this API's retry contract. Scope keys to the authenticated team and create operation, retain completed results for 24 hours, and compare the parsed request fields on reuse. The same key with different input receives 409 with code: "idempotency_key_reused".

For matching requests, replay the original status, body, resource location, and entity tag. If the original operation is still running, return 409 with code: "request_in_progress" and instruct clients to retry the same request with bounded backoff. The service checks authentication and authorization again before replaying a stored result.

Store successful creation and its replay record atomically. Otherwise, a crash between saving the link and saving the key can create a duplicate on retry. Validation failures before execution do not reserve the key. After retention expires, the key no longer protects against duplicate creation; clients should reconcile an uncertain outcome rather than assume another POST is harmless.

Replaying a creation result describes that original operation, even if the resource has since changed. Retrieve the management resource to learn its current state.

4. Public Redirects

Opening the short link resolves its code, checks whether it is usable, and returns the stored destination:

This API chooses 302 for editable destinations. 307 would be another temporary redirect option with explicit method preservation. Permanent redirects such as 301 and 308 communicate a different commitment and are a poor default for links that owners can retarget or revoke.

Redirect status and caching policy are separate decisions. Here, no-store prevents compliant HTTP caches from storing the response. A temporary redirect alone does not establish that policy.

HEAD resolves under the same rules and returns the corresponding headers without a response body. It does not contribute to redirect statistics. Unsupported methods receive 405 Method Not Allowed with Allow: GET, HEAD; the shortener must not forward arbitrary request bodies to user-selected destinations.

In this design, the shortener ignores query parameters that visitors append to a short URL. For example, /autumn-guide?next=https%3A%2F%2Fother.example still uses the stored destination exactly. Merging visitor parameters into the target can overwrite campaign data or create unintended routing behavior.

The browser does not send URL fragments to the shortener in the HTTP request. If Location has no fragment, redirect processing can inherit a fragment from the original URL. If the stored destination supplies a fragment, that fragment controls navigation. The API therefore does not promise to remove visitor-supplied fragments or include them in analytics.

The flow below keeps analytics work outside the visitor's critical path.

The dotted branch represents asynchronous, best-effort recording. Delivering a redirect does not depend on the analytics store being available, and issuing a redirect does not prove that the destination loaded successfully.

5. Reading and Updating Links

An authorized GET /v1/links/lnk_7m2p9q4r returns the current representation and a strong ETag, an opaque validator for that representation. Before any edits, its body and tag match the creation example. Management responses use Cache-Control: no-store because they contain private account metadata.

The collection accepts limit with a default of 20 and maximum of 100, plus an opaque cursor. The API sorts results by createdAt descending and uses id to break ties consistently. A response contains items and nextCursor; nextCursor: null marks the end. Each item uses the link representation already shown. The cursor binds the ordering and team context, and the service authorizes every page independently. Invalid cursors receive 400. Pagination is not a historical snapshot: deleted records can disappear between requests.

Updates use JSON Merge Patch. For this resource, clients may patch only destinationUrl, enabled, and expiresAt. Omitting a field leaves it unchanged. Removing the optional expiration with "expiresAt": null means no expiration, and the response represents that absence as null. Null is invalid for the required destination and enabled flag. Unknown or read-only fields receive 422.

Require the last observed strong entity tag on updates to prevent one editor from silently overwriting another:

A missing precondition receives 428 Precondition Required; a stale tag receives 412 Precondition Failed. The client retrieves current state and reconciles its intended change. After a lost success response, retrying with the old tag can also produce 412, so that status does not prove the earlier write failed.

Destination updates apply the same validation and abuse checks as creation. The write and version check are atomic. Return success only when every redirect lookup that starts afterward can read the new record, as the consistency promise requires.

6. Disablement, Expiration, and Deletion

Disabling is a reversible owner action: patch enabled to false, using the current entity tag. The server checks expiration against its clock on every redirect lookup, regardless of whether a cleanup job has run. A link is expired when now >= expiresAt. New expiration values must be in the future, and expired links reject patches with 409 and code: "link_expired".

The diagram shows the owner-visible lifecycle. Abuse blocking is an additional serving restriction that applies independently to any retained link.

Re-enabling works only before expiration and cannot override an abuse block. Owners can still retrieve expired or disabled metadata and statistics until deletion.

Deletion uses the same conditional-write policy:

This example assumes no intervening edit after the destination update. Subsequent management reads, statistics requests, and repeated deletes return 404. Repeating a successful delete still leaves the resource absent. Retain a minimal code reservation so the service never reassigns the public address; deletion need not retain the destination indefinitely.

For public visitors, unknown, disabled, expired, blocked, and deleted codes all receive the same 404 response with no destination. This concealment policy does not reveal why a code is unavailable. A product that deliberately publishes permanent removal could choose 410, but that is a different disclosure policy.

Use no-store on these public errors as well. Otherwise a cached unavailable response could hide a newly created or re-enabled link. If the service cannot determine the current mapping or policy because a dependency failed, return 503, not a fabricated 404.

Cache and Revocation Guarantees

The baseline design uses authoritative redirect reads and no reusable HTTP redirect cache. That costs more serving capacity, but gives the stated update guarantee a straightforward meaning.

Internal mapping caches are a separate mechanism: Cache-Control does not invalidate an application cache. Before adding them, coordinate cached entries with record versions or document the longest delay before a change reaches every cache. The service must still check expiration during lookup, even when it uses a cached mapping.

If a later design allows a 30-second stale-mapping window, tell owners that disabling can take that long. Cache duration must not outlive expiration, and security blocks need an enforcement path consistent with the promised revocation bound. Purging a CDN does not retract a redirect a browser has already saved or a destination a visitor has already seen.

7. Statistics and Their Meaning

Statistics belong behind management authorization. Before deleting the example link, its owner can request a bounded UTC interval:

The interval includes from and excludes to, with a maximum range of 31 days. processedThrough describes the aggregation pipeline's progress, not a guarantee that the pipeline lost no events. This example is a partial-day result, and later requests can return a higher count.

redirectCount counts recorded successful GET redirects, excluding HEAD and unavailable responses. It is not a count of unique people or confirmed destination visits. Link scanners, browser prefetching, and retries can increase it. Event loss can reduce it; deduplicating repeated event deliveries prevents counting one event twice but does not identify a unique person.

Aggregate by link ID across destination changes. A campaign needing separate statistics for a new destination should create another link. Keep visitor identifiers and full destination query strings out of routine analytics logs, and define retention for any data the analytics system actually collects. If the service cannot retrieve statistics, return 503 rather than reporting zero traffic.

8. Authorization and Abuse Controls

Management authentication failures receive 401 with a bearer challenge. An authenticated caller without write permission receives 403 for a link they may view:

This is an alternative scenario before deletion. Requests for another team's link receive 404 under the management concealment policy. Error ordering must not leak that resource's current version or destination before authorization.

A public short link is discoverable and shareable. Random codes reduce enumeration but do not make a link an authorization mechanism. The destination must enforce its own access controls for private content.

A shortener intentionally redirects to externally supplied URLs, so syntax validation alone cannot establish trust. Apply destination policy checks at creation and update, offer abuse reporting, and support blocks that owners cannot bypass by toggling enabled. Keep the serving path free of synchronous remote reputation checks; use an available local policy decision and a defined failure policy.

Rate-limit creation and expensive management operations by authenticated team, with separate controls for anonymous redirect abuse. Creation throttling must not automatically disable existing campaign traffic. Return 429 Too Many Requests with Retry-After when temporary throttling applies, and bound body size, page size, and statistics ranges separately.

The baseline service returns a destination without fetching it. If preview or scanning workers later fetch destinations, they introduce server-side request forgery risk: an attacker may cause the worker to contact internal services. Those workers need restricted network access, checks on resolved addresses, and validation of every redirect hop. Checks that run only when the owner creates the link are insufficient because DNS and remote redirects can change.

Summary

A URL Shortener API has two distinct contracts: authenticated management of owned links and public navigation to their destinations. Stable IDs, atomic code allocation, creation retry protection, and conditional updates make management predictable.

Redirect behavior depends on explicit lifecycle and caching rules. Define when edits become visible, evaluate expiration during lookup, and avoid reassigning old codes. Keep statistics asynchronous and honest about what they measure, while enforcing ownership and abuse controls independently of whether a link is publicly reachable.