Free lesson · System Design Interviews
Step 3: Core API Design
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
-
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.
-
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.
-
Separate API namespace from redirect namespace. Use
/api/v1/...for management APIs and/:shortCodefor redirects. This prevents collisions between short codes and API paths. -
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.
-
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.
-
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?