Free lesson · Mobile System Design Interviews
Social Feed App - API Design
Step 2: What Does the Mobile App Actually Talk To?
Now that we understand the problem, we need to define how the mobile app communicates with the backend server. This is Step 2 of the framework: API Design.
Think of the API like a contract between two teams:
- Mobile app team: "Hey server, I need the feed posts for my user. Here's what I'm asking for..."
- Backend team: "Here's what I'll give you in response. Use it however you like."
If both sides understand the contract, the app works. If they don't agree, things break.
Why REST and HTTP?
For a social feed app, we'll use REST (Representational State Transfer) over HTTP. Before you panic at the jargon—REST just means:
- We use standard HTTP verbs: GET (fetch data), POST (create data), PUT (update), DELETE (remove)
- We organize the API around resources: posts, comments, likes, users
- Each request gets a response with data
Why REST for a feed app?
✓ Simple and standard: Developers everywhere know REST. It's easy to test. ✓ Works well for request-response: Fetching a feed is a perfect request-response pattern—the app asks, the server responds. ✓ Stateless: Each request is independent. The server doesn't need to remember the app's state. ✓ Cacheable: We can cache responses (more on this later).
What about alternatives?
- WebSockets (real-time updates): We might add WebSockets later if we needed real-time like counts updating as other users like a post. For now, REST polling is simpler.
- GraphQL: A more flexible query language, but adds complexity for a first case study. REST is fine here.
⚠️ Watch out: Don't over-engineer the API. Start with REST. Add complexity only when you need it.
API Endpoints: The Contracts
An endpoint is a specific URL and HTTP verb combination. Here are the main endpoints for our social feed:
1. Get Feed: GET /feed
What: Fetch a paginated list of posts for the user's feed.
Request:
GET /feed?cursor=abc123&limit=20
Authorization: Bearer {access_token}
cursor: A token pointing to where we left off (pagination—we'll explain below)limit: How many posts to return (20 is typical)Authorization: Proves the user is logged in
Response (200 OK):
{
"posts": [
{
"id": "post_001",
"author": {
"id": "user_123",
"name": "Alice Johnson",
"profilePictureUrl": "https://cdn.example.com/profiles/user_123.jpg"
},
"content": "Just finished an amazing hike!",
"imageUrls": [
"https://cdn.example.com/images/post_001_img1.jpg"
],
"createdAt": "2026-03-25T14:30:00Z",
"likeCount": 1247,
"commentCount": 89,
"isLikedByCurrentUser": false,
"shareCount": 12
},
{
"id": "post_002",
"author": { ... },
...
}
],
"nextCursor": "xyz789",
"hasMore": true
}
Why this structure?
posts: Array of post objectsnextCursor: Token to fetch the next page. The app doesn't care how it works; it just passes it back next time.hasMore: Boolean to tell the app if there are more posts to load. Whenfalse, we've reached the end.
2. Get Post Details: GET /posts/{id}
What: Fetch a single post with all its comments.
Request:
GET /posts/post_001
Authorization: Bearer {access_token}
Response (200 OK):
{
"post": {
"id": "post_001",
"author": { ... },
"content": "Just finished an amazing hike!",
"imageUrls": [ ... ],
"createdAt": "2026-03-25T14:30:00Z",
"likeCount": 1247,
"commentCount": 89,
"isLikedByCurrentUser": false
},
"comments": [
{
"id": "comment_1",
"author": {
"id": "user_456",
"name": "Bob Smith",
"profilePictureUrl": "https://cdn.example.com/profiles/user_456.jpg"
},
"content": "Looks amazing! Where was this?",
"createdAt": "2026-03-25T15:00:00Z",
"likeCount": 23
}
]
}
3. Create Post: POST /posts
What: Upload a new post to the server.
Request:
POST /posts
Authorization: Bearer {access_token}
Content-Type: application/json
{
"content": "Check out this sunset!",
"imageUrls": ["https://s3.example.com/uploads/xyz789.jpg"]
}
Note: For image uploads, we'll use presigned URLs (explained below).
Response (201 Created):
{
"id": "post_999",
"author": { ... },
"content": "Check out this sunset!",
"imageUrls": [ ... ],
"createdAt": "2026-03-25T16:45:00Z",
"likeCount": 0,
"commentCount": 0,
"isLikedByCurrentUser": false
}
4. Like a Post: POST /posts/{id}/likes
What: Like a post. If already liked, unlike it.
Request:
POST /posts/post_001/likes
Authorization: Bearer {access_token}
Response (200 OK):
{
"isLiked": true,
"likeCount": 1248
}
Error Response (if post not found): (404 Not Found)
{
"error": "Post not found"
}
5. Add Comment: POST /posts/{id}/comments
What: Post a comment on a post.
Request:
POST /posts/post_001/comments
Authorization: Bearer {access_token}
Content-Type: application/json
{
"content": "This is incredible!"
}
Response (201 Created):
{
"id": "comment_42",
"author": { ... },
"content": "This is incredible!",
"createdAt": "2026-03-25T16:50:00Z",
"likeCount": 0
}
Data Models: The Building Blocks
Let's define the data structures that flow through the API. Here's how you'd define them in code:
Kotlin Data Classes
// User object—represents a person
data class User(
val id: String,
val name: String,
val profilePictureUrl: String
)
// Post object—the main content
data class Post(
val id: String,
val author: User,
val content: String,
val imageUrls: List<String>,
val createdAt: String, // ISO 8601 format: "2026-03-25T14:30:00Z"
val likeCount: Int,
val commentCount: Int,
val isLikedByCurrentUser: Boolean,
val shareCount: Int = 0
)
// Comment object—a reply to a post
data class Comment(
val id: String,
val author: User,
val content: String,
val createdAt: String,
val likeCount: Int
)
// API response for the feed
data class FeedResponse(
val posts: List<Post>,
val nextCursor: String?, // null if no more posts
val hasMore: Boolean
)
// API response for post details
data class PostDetailResponse(
val post: Post,
val comments: List<Comment>
)
Swift Structs
// User object
struct User: Codable {
let id: String
let name: String
let profilePictureUrl: String
}
// Post object
struct Post: Codable {
let id: String
let author: User
let content: String
let imageUrls: [String]
let createdAt: String
let likeCount: Int
let commentCount: Int
let isLikedByCurrentUser: Bool
let shareCount: Int
}
// Comment object
struct Comment: Codable {
let id: String
let author: User
let content: String
let createdAt: String
let likeCount: Int
}
// API responses
struct FeedResponse: Codable {
let posts: [Post]
let nextCursor: String?
let hasMore: Bool
}
struct PostDetailResponse: Codable {
let post: Post
let comments: [Comment]
}
Pagination: How to Load Infinite Posts
The feed has potentially millions of posts. We can't send them all at once—the user's phone would run out of memory in seconds. Instead, we load posts in pages.
Cursor-Based Pagination
We use a cursor, which is a pointer to a position in the data. Think of it like a bookmark:
- First request: App calls
GET /feedwith no cursor - Server responds: "Here are posts 0-19, and here's the cursor to get the next 20"
- Next request: App calls
GET /feed?cursor=xyz789 - Server responds: "Here are posts 20-39, and here's the cursor for posts 40-59"
Why cursor-based instead of offset-based (page 1, page 2, page 3)?
- Cursors are stable: If posts are added/deleted in between requests, cursor-based pagination doesn't mess up. Offset-based would skip or duplicate posts.
- Cursors are efficient: The server doesn't need to count or skip X rows; it just finds the cursor position and returns from there.
💡 Tip: Cursor-based pagination is the industry standard for infinite feeds. Use it unless you have a specific reason not to.
Image Upload: Presigned URLs
When a user creates a post with images, uploading them needs a special strategy. Here's how:
The Problem: Users' images are huge (2-5 MB). We don't want the mobile app to send them through our main API server—that's wasteful.
The Solution: Presigned URLs
- App requests: "I want to upload an image. Can you give me a presigned URL?"
- Server responds: "Sure! Here's a temporary URL. You can upload directly to S3 (cloud storage) using this."
- App uploads: Directly to cloud storage (S3, Google Cloud Storage, etc.), not through our API
- App tells server: "I uploaded the image to this URL. Here's my post with that image."
- Server responds: Creates the post and stores the image URL
Why?
- ✓ Faster uploads (direct to cloud)
- ✓ Reduces load on the API server
- ✓ Automatic retry if the upload fails (just retry directly to cloud)
Authentication: Bearer Tokens
Every request except login needs proof that the user is authenticated. We use Bearer tokens:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
The token proves: "I logged in before, and the server gave me this token. I'm allowed to be here."
⚠️ Watch out: Tokens expire. The mobile app needs to refresh them before they expire, or re-login.
Common API Errors
Here are the errors your mobile app should handle:
| Status Code | Meaning | What to Do |
|---|---|---|
| 200 | Success | Show the data |
| 201 | Created | Show success message |
| 400 | Bad Request | Show error to user (they did something wrong) |
| 401 | Unauthorized | Token expired; ask user to login |
| 404 | Not Found | Post/comment doesn't exist; show error |
| 429 | Rate Limited | Too many requests; wait before retrying |
| 500 | Server Error | Temporary issue; retry later |
Quick Decision Card: API Design Choices
If an interviewer asks why you chose REST, or why cursor-based pagination, here's what to say:
| Choice | Alternative | Why We Chose Ours |
|---|---|---|
| REST/HTTP | WebSocket or GraphQL | Simple, familiar, request-response fits this use case |
| Cursor-based pagination | Offset-based (page 1, 2, 3) | Stable when data changes, efficient for large datasets |
| Presigned URLs for images | Send images through API | Faster, reduces server load, easier retry logic |
| Bearer tokens | API keys or Session cookies | Stateless, works across distributed servers |
Interview's Perspective: What Interviewers Look For
When they ask "Design the API," they're actually testing:
- Do you think about data structures? Can you define clear models (Post, Comment, User)?
- Do you handle pagination? Infinite feeds are useless without pagination.
- Do you think about scale? You mentioned that we can't send all posts at once—that shows you're thinking realistically.
- Do you think about image uploads? Lots of candidates forget this. Presigned URLs shows you understand practical constraints.
- Do you handle errors? Real APIs fail. What happens when a request times out?
🎯 Interviewer's Perspective: Your API design should be simple enough to implement in a weekend, but thoughtful enough to handle millions of requests. That's the sweet spot.
Common Pitfall: Over-Specifying the API
Many candidates spend too much time on API design details like exact header formats or authentication schemes. Remember: the API design matters, but it's not the whole story.
The real challenge is how the mobile app uses these APIs—that's where the architecture and caching strategies come in (next steps).
🔗 Building on Previous: Remember the caching strategies from Module 3 (memory cache, disk cache, network cache)? The FeedResponse and Post objects will flow through all these layers. We'll see that in the next file.
What's Next?
Now that we've defined how the mobile app talks to the server, we need to design how the mobile app internally organizes all this data. This is the architecture—where we'll use patterns like MVVM, Repository, and caching.