skip to content

How would you design HTTP API resource URLs so they remain stable and canonical over years, and what do you do when a resource's human-readable slug changes?

level: seniorimportance: should knowfreq 40%

answer

  1. identity immutable, facts mutable
  2. opaque id canonical, slug = alias
  3. slug history table + 301
  4. never reuse a deleted id
  5. 410 Gone for retired, 308 preserves method

basics

~20 s

Identify resources by an immutable, opaque id in the path; keep mutable slugs out of identity or treat them as aliases. When a slug changes, keep the id URL canonical and answer old slug URLs with 301 to it. Never reuse a retired identifier.

solid answer

~50 s

Treat a resource URL as a promise. Two rules follow. **Identity must be immutable.** Put a stable, opaque identifier in the path — a UUID or an id you never recycle — and keep volatile facts out of it: display names, owning team, region, storage backend, current status. Anything that can change should not sit in an identifier, because changing it either breaks stored links or forces a permanent alias. **Slugs are aliases, not identity.** When human-readable URLs matter, serve `/articles/{id}` as canonical and let `/articles/{slug}` resolve to it with `301`, or accept a combined `{id}-{slug}` form where only the id is authoritative. On rename, the id path is untouched and old slugs redirect. Operationally: never reuse a deleted id (reuse turns a stale link into a wrong record, which is worse than a 404); return `410 Gone` for deliberately retired resources; and version the *contract*, not individual resource identities, so a v2 rollout does not invalidate every stored URL.

go deeper

for a junior

Say identity should be a stable id, not a name that can change, and that old URLs should redirect rather than break.

for a middle

Explain the id-canonical/slug-alias pattern, slug history, and why deleted ids must never be reused.

for a senior

Discuss operational consequences — stored webhook URLs, partner integrations, 301 vs 308, 410 for retired resources, and advertising the canonical URL via a self link.

for a principal

Frame URL stability as a long-lived external contract with migration cost across unknown consumers, and set the policy: what may appear in a path, redirect retention windows, and how contract versioning coexists with immutable identity.

## Why URL stability is a real cost centre API URLs leak into places you do not control: stored webhook targets, customer scripts, database columns holding a link, bookmarks, third-party integrations, logs used for audit. Changing one is not a code change — it is a coordination problem across everyone who ever persisted it. So the design goal is to make identity something that *cannot* need to change. ## Immutable, opaque identifiers The identifier in the path should be stable for the lifetime of the resource and carry no meaning that can go stale. Sequential integers are stable but leak volume and enable enumeration; UUIDs or random string ids avoid both. What matters most is what the id is *not*: not the email address, not the display name, not the SKU that marketing may reissue, not a composite encoding the current owner or region. Each of those is a fact about the resource, and facts change; identity should not. The same discipline applies to the segments around the id. `/eu-west/customers/{id}` bakes an infrastructure decision into the contract; move the customer and every stored link is wrong. Region, tenancy, and storage details belong in headers, hostnames, or the representation — not in the resource's identity. ## Slugs: readable without being authoritative Human-readable URLs are genuinely valuable for docs, support, and shareability, and they conflict directly with immutability because slugs derive from names people edit. The standard resolutions: 1. **Id canonical, slug alias.** `/articles/9f3a` is canonical; `/articles/how-to-deploy` looks up the slug and answers `301` to the id form. Old slugs are retained in a slug-history table forever, so every historical link keeps working. 2. **Composite path.** `/articles/9f3a-how-to-deploy`, where the server parses only the leading id and ignores the tail. Any stale tail still resolves; you can 301 to the current spelling for tidiness. 3. **Slug canonical with history.** Acceptable in content systems, but every rename now requires a redirect entry, and slug uniqueness becomes a hard constraint you must police. Whichever you choose, advertise the canonical form. `Content-Location` on the response, or a `self` link in the representation, tells clients which URL to store. ## Deletion, reuse, and gone-ness Never reuse an identifier. A recycled id turns every stale reference into a **silent pointer at the wrong record** — a data-integrity and often a security problem — whereas an unreused id gives a clean `404`. When a resource is intentionally and permanently retired, `410 Gone` is more useful than `404 Not Found`: it tells crawlers, caches, and integration owners to stop retrying and to clean up their stored link, rather than treating it as a possible transient miss. ## Redirects as a migration tool When a path genuinely must move — a restructure, a merged product, a corrected pluralisation — `301 Moved Permanently` (for GET) and `308 Permanent Redirect` (which preserves the method and body, so POST/PUT/PATCH are not silently downgraded to GET) let old clients keep working while new ones learn the new URL. Keep the redirects far longer than feels necessary; the cost of an extra route table entry is trivial against the cost of a partner's overnight batch job breaking. ## Where change actually belongs Stability of identity does not mean the API is frozen. Representations can gain fields, new endpoints can appear, and the *contract* can be versioned as a whole. The invariant worth defending is narrower and cheaper than it sounds: the string that identifies this particular thing keeps identifying this particular thing.

  • When redirecting a moved API resource, why might 308 be safer than 301?
    Historically many clients turned a 301 on a POST into a GET to the new location, dropping the request body. 308 Permanent Redirect is defined to preserve the method and body, so a POST stays a POST. For read-only GET endpoints 301 is fine; for anything with a body, prefer 308.
  • What is wrong with recycling identifiers after a resource is deleted?
    Any client, log entry, or stored link still holding the old id will now resolve to a completely different resource, silently. That converts a harmless 404 into wrong data and potentially a cross-tenant exposure. Non-reuse costs nothing and turns every stale reference into an honest error.

saying these in an interview costs you the question

  • Putting an email address, username, or display name in the identity path segment
  • Reusing ids after deletion to keep the id space tidy
  • Encoding region, shard, or owning team into the resource path
  • Changing slugs in place with no redirect and calling it a cosmetic change
  • Believing 404 and 410 are interchangeable for permanently removed resources

context