AlgoInsight

Free lesson · System Design Interviews

Step 3: Core API Design

Hard7 min readsystem-designinterview-prep

Now we know what we're building and at what scale. Before drawing the architecture, let's define the API - the contract between the client and the system. This step takes 2-3 minutes in an interview, and it forces you to think precisely about what data flows in and out of each operation.

I've seen candidates skip API design and go straight to drawing boxes. The problem is they end up with an architecture that doesn't clearly support the operations their system needs to perform. Defining the API first ensures your architecture has a clear purpose - every component exists to serve one of these endpoints.

The Three Core APIs

A URL shortener needs exactly three endpoints for the core functionality:

flowchart TD
    subgraph APIS["Core APIs"]
        CREATE["POST /api/v1/shorten<br/>Create a short URL"]
        REDIRECT["GET /:shortCode<br/>Redirect to long URL"]
        STATS["GET /api/v1/stats/:shortCode<br/>Get click analytics"]
    end
    
    CREATE -->|"Returns short URL"| CLIENT["Client"]
    REDIRECT -->|"301/302 Redirect"| CLIENT
    STATS -->|"Returns click count"| CLIENT
The three core APIs for a URL shortener. POST to shorten creates a short URL from a long URL and returns the short URL. GET with the short code redirects the user's browser to the original long URL using an HTTP 301 or 302 redirect. GET stats returns the click analytics for a given short code. These three endpoints cover all functional requirements.

API 1: Create Short URL

POST /api/v1/shorten

Request Body:
{
    "long_url": "https://www.example.com/very/long/path?with=params",
    "custom_code": "my-brand",          // Optional: user-specified short code
    "expiration_days": 365              // Optional: days until expiration (default: 1825 = 5 years)
}

Response (201 Created):
{
    "short_url": "https://short.ly/my-brand",
    "short_code": "my-brand",
    "long_url": "https://www.example.com/very/long/path?with=params",
    "expires_at": "2027-06-14T00:00:00Z",
    "created_at": "2026-06-14T12:30:00Z"
}

Error Responses:
- 400 Bad Request: Invalid URL format, custom_code contains invalid characters
- 409 Conflict: custom_code already taken
- 429 Too Many Requests: Rate limit exceeded

Design Decisions in the Create API

Why POST, not GET? Creating a resource is a side effect - it changes server state. POST is the semantically correct HTTP method for resource creation. GET should be idempotent (no side effects).

Why /api/v1/shorten and not just /shorten? The /api/v1/ prefix separates the API namespace from the redirect namespace. The root path /:shortCode is reserved for redirects - you don't want a collision where short.ly/shorten is ambiguous (is it the API or a short code?). Versioning (v1) allows future API changes without breaking existing clients.

Why include custom_code as optional? If not provided, the server generates a random 7-character code. If provided, the server validates it (length, characters, uniqueness) and uses it. This single endpoint handles both auto-generated and custom URLs.

Validation rules for custom_code:

Rule Reason
3-20 characters Too short is ambiguous, too long defeats the purpose
Alphanumeric + hyphens only URL-safe characters
Not a reserved word (api, admin, health) Prevent namespace collisions
Case-sensitive MyBrand ≠ mybrand - doubles the code space

Idempotency Consideration

Should creating the same long URL twice return the same short URL or a new one?

Approach Pros Cons
Same short URL (deduplicate by long URL) Saves storage, fewer codes used Different users share the same short URL and analytics
New short URL each time Each creator gets their own analytics, isolation More codes used, more storage

Most production systems create a new short URL each time. The storage cost is negligible (300 bytes per entry), and isolation between users' analytics is important. If the interviewer asks about deduplication, mention this trade-off explicitly.

Interview: What to say: "I'd create a new short code for each request, even if the same long URL has been shortened before. This gives each user independent analytics and avoids a slow lookup-by-long-URL query on every creation. The storage cost of a duplicate entry is trivial - 300 bytes. If the interviewer prefers deduplication, I'd add a hash index on the long_url column to enable fast duplicate detection, but I'd recommend against it for the reasons above."

API 2: Redirect (The Core Operation)

GET /:shortCode

Example: GET /a3Xb9k

Response (301 Moved Permanently):
HTTP/1.1 301 Moved Permanently
Location: https://www.example.com/very/long/path?with=params

OR

Response (302 Found):
HTTP/1.1 302 Found
Location: https://www.example.com/very/long/path?with=params

Error Responses:
- 404 Not Found: Short code doesn't exist or has expired
- 410 Gone: Short code existed but has been explicitly deleted

301 vs 302: The Most Frequently Asked Follow-Up

This is the question interviewers love. The difference matters:

flowchart TD
    subgraph R301["301 Moved Permanently"]
        B301["Browser caches the redirect.<br/>Next visit goes directly<br/>to the long URL.<br/>Server never sees the<br/>request again."]
    end
    
    subgraph R302["302 Found (Temporary)"]
        B302["Browser does NOT cache.<br/>Every visit hits the<br/>short URL server first.<br/>Server sees every click."]
    end
    
    R301 -->|"Better for performance"| PERF["Reduces server load"]
    R302 -->|"Better for analytics"| ANALYTICS["Every click is tracked"]
The difference between 301 and 302 redirects. A 301 Moved Permanently tells the browser to cache the redirect - future visits go directly to the long URL without hitting the shortener server. This reduces server load but means clicks after the first one are invisible to analytics. A 302 Found tells the browser the redirect is temporary - every visit hits the shortener server first, allowing every click to be tracked. For a URL shortener with analytics, 302 is usually the right choice because you want to count every click.
Factor 301 (Permanent) 302 (Temporary)
Browser caching Yes - browser remembers the redirect No - browser always hits server
Click tracking Only first click is tracked Every click is tracked
Server load Lower (clients cache) Higher (every click hits server)
SEO Passes link equity to destination Does not pass link equity
Can change destination later? Unreliable (clients may use cached redirect) Yes - server controls every redirect

Recommendation: Use 302 if you need analytics (which we do). Use 301 if performance is more important than tracking.

Interview: What to say: "I'd use 302 Found because we need to track click analytics. A 301 tells the browser to cache the redirect, so subsequent clicks bypass our server entirely - we'd lose visibility into click counts. With 302, every click hits our server, we increment the counter, and then redirect. The trade-off is higher server load, but our cache layer handles 100K QPS comfortably. If we didn't need analytics, 301 would be more efficient."

API 3: URL Statistics

GET /api/v1/stats/:shortCode

Example: GET /api/v1/stats/a3Xb9k

Response (200 OK):
{
    "short_code": "a3Xb9k",
    "long_url": "https://www.example.com/very/long/path?with=params",
    "created_at": "2026-06-14T12:30:00Z",
    "expires_at": "2027-06-14T00:00:00Z",
    "total_clicks": 15847,
    "last_clicked_at": "2026-06-14T18:45:00Z"
}

Error Responses:
- 404 Not Found: Short code doesn't exist
- 403 Forbidden: Not authorized to view stats for this URL (if auth exists)

This is a simple read operation. For our basic analytics requirement (click count only), the data is stored alongside the URL mapping. For detailed analytics (clicks by country, device, time), you'd need a separate analytics data store - but we scoped that out in requirements.

Rate Limiting

Every public API needs rate limiting to prevent abuse. For a URL shortener:

Endpoint Rate Limit Why
POST /api/v1/shorten 100 requests/min per IP Prevent spammers from creating millions of short URLs
GET /:shortCode 1,000 requests/sec per IP Generous limit - legitimate traffic can be very high
GET /api/v1/stats 60 requests/min per IP Not latency-critical, lower limit is fine
Rate Limit Response (429 Too Many Requests):
{
    "error": "Rate limit exceeded",
    "retry_after_seconds": 60
}

Headers on every response:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 37
X-RateLimit-Reset: 1718380800

Tip: Mention rate limiting proactively in interviews. Even if the interviewer doesn't ask, saying "I'd rate-limit the creation endpoint to prevent abuse - 100 requests per minute per IP" shows you think about production concerns. It takes 5 seconds to mention and signals operational maturity.

API Design Principles Demonstrated

This URL shortener API demonstrates several important design principles that interviewers look for:

Principle How It's Applied
RESTful resource naming Nouns for resources (/api/v1/shorten, /api/v1/stats/:code)
Appropriate HTTP methods POST for creation, GET for reads and redirects
API versioning /api/v1/ allows future breaking changes
Proper status codes 201 Created, 301/302 Redirect, 404 Not Found, 409 Conflict, 429 Rate Limited
Idempotency considerations Discussion of whether same URL → same short code
Error handling Clear error responses with actionable messages
Rate limiting Prevents abuse, includes rate limit headers
Namespace separation API routes (/api/v1/...) vs redirect routes (/:shortCode)

Key Takeaways

  1. Define the API before drawing architecture. The API is the contract - it defines what your system must do. Every component in your architecture exists to serve one of these endpoints.

  2. 301 vs 302 is the most-asked follow-up. 301 = browser caches, no analytics after first click. 302 = no caching, every click tracked. Choose 302 when you need analytics.

  3. Separate API namespace from redirect namespace. Use /api/v1/... for management APIs and /:shortCode for redirects. This prevents collisions between short codes and API paths.

  4. New short code per request, not deduplication. Creating a new code for the same long URL gives each user independent analytics. The storage cost is trivial. Mention this trade-off to show depth.

  5. Rate limit every public endpoint. 100 creates/min per IP prevents spamming. 1,000 redirects/sec per IP is generous but prevents DDoS. Include rate limit headers in every response.

  6. Custom short codes need validation rules. Length limits (3-20 chars), character restrictions (alphanumeric + hyphens), reserved word prevention, and uniqueness checking. This is more complex than auto-generation and shows you've thought about edge cases.

Exercise: Exercise 1: Design the API for a URL Shortener that supports user accounts. Add authentication (API key or OAuth), and add endpoints for: list all my URLs, delete a URL, update a URL's destination. How does authentication change the rate limiting strategy? (Hint: authenticated users get higher limits than anonymous users.)

Exercise 2: The interviewer asks: "What if a client sends a creation request but the response is lost due to a network timeout? The client retries. How do you prevent duplicate short URLs being created?" Design an idempotency solution for the creation API. (Hint: the client sends an Idempotency-Key header, and the server checks it before creating.)

Exercise 3: Design the API for bulk URL shortening - a single request that creates 100 short URLs at once. What's the request format? What's the response format if 95 succeed and 5 fail (because the custom codes are taken)? How does this change the rate limiting strategy?

Back to System Design Interviews