GET /api/v1/users/{user_id} - Get Public User Profile
Overview
This endpoint retrieves public profile information for any user by their UUID. Unlike /users/me, this endpoint is unauthenticated and returns only public information (no sensitive data).
Why it exists: Allows displaying user information on video listings, comments, and other public-facing features without requiring authentication.
HTTP Details
- Method: GET
- Path:
/api/v1/users/{user_id} - Auth Required: No (public endpoint)
- Success Status: 200 OK
Request
GET /api/v1/users/550e8400-e29b-41d4-a716-446655440000
Response Body
{
"userId": "550e8400-e29b-41d4-a716-446655440000",
"firstname": "John",
"lastname": "Doe",
"email": "john.doe@example.com",
"account_status": "creator",
"created_date": "2025-10-31T10:30:00Z",
"last_login_date": "2025-10-31T14:22:15Z"
}
Note: While this returns the same fields as /users/me, in a production app you might want to hide sensitive fields like email or last_login_date.
Cassandra Concepts Explained
UUID as Partition Key
This endpoint demonstrates the ideal Cassandra query pattern:
await table.find_one(filter={"userid": user_id})
Why this is fast:
useridis the partition key- Cassandra uses consistent hashing to map UUID → node
- Single node lookup, no coordination needed
- O(1) time complexity
UUID Format Validation
FastAPI validates the UUID format automatically:
@router.get("/{user_id_path:uuid}")
async def get_user_by_id(user_id_path: UUID):
...
Invalid UUID examples (return 422 Validation Error):
not-a-uuid123550e8400-e29b-41d4-a716(too short)
Valid UUID examples:
550e8400-e29b-41d4-a716-446655440000(standard format)550E8400-E29B-41D4-A716-446655440000(case insensitive)
Public vs Private Endpoints
Public endpoint (/users/{user_id}):
- No authentication required
- No dependency injection
- Direct service call
- Fast, cacheable
Private endpoint (/users/me):
- Requires JWT token
- Dependency injection (
get_current_viewer) - Additional auth checks
- Slightly slower
Code comparison:
# Public
async def get_user_by_id(user_id_path: UUID):
return await user_service.get_user_by_id_from_table(user_id_path)
# Private
async def read_users_me(current_user: Annotated[User, Depends(get_current_viewer)]):
return current_user # Already fetched by dependency
Data Model
Table: users
CREATE TABLE killrvideo.users (
userid uuid PRIMARY KEY,
created_date timestamp,
email text,
firstname text,
lastname text,
account_status text,
last_login_date timestamp
);
Database Queries
Single Query: Fetch User by ID
async def get_user_by_id_from_table(user_id: UUID):
table = await get_table("users")
user_data_dict = await table.find_one(filter={"userid": user_id})
if not user_data_dict:
return None
return User.model_validate(user_data_dict)
Equivalent CQL:
SELECT *
FROM killrvideo.users
WHERE userid = 550e8400-e29b-41d4-a716-446655440000;
Performance: O(1) - Direct partition key lookup (~5ms)
Implementation Flow
┌──────────────────────────────────────────────────────────┐
│ 1. Client sends GET /api/v1/users/{user_id} │
│ No authentication required │
└────────────────────┬─────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────┐
│ 2. FastAPI validates UUID format │
│ ├─ Invalid format? → 422 Validation Error │
│ └─ Valid UUID? → Continue │
└────────────────────┬─────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────┐
│ 3. Query users table by userid │
│ SELECT * FROM users WHERE userid = ? │
│ ├─ Not found? → 404 User not found │
│ └─ Found? → Continue │
└────────────────────┬─────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────┐
│ 4. Map database document to User model │
│ User.model_validate(user_data_dict) │
└────────────────────┬─────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────┐
│ 5. Return 200 OK with user profile │
└──────────────────────────────────────────────────────────┘
Total Queries: 1 SELECT
Expected Latency: 5-10ms
Special Notes
1. Privacy Considerations
Current behavior: Returns ALL user fields including email and last_login_date
Production recommendation: Create a separate PublicUserProfile model:
class PublicUserProfile(BaseModel):
userId: UUID
firstname: str
lastname: str
account_status: str
created_date: datetime
# Omit: email, last_login_date
@router.get("/{user_id_path:uuid}", response_model=PublicUserProfile)
async def get_user_by_id(user_id_path: UUID):
user = await user_service.get_user_by_id_from_table(user_id_path)
if not user:
raise HTTPException(status_code=404, detail="User not found")
return user # Pydantic filters to only public fields
Why?
- Email is PII (Personal Identifiable Information)
- Last login date reveals activity patterns
- Principle of least privilege: only expose what's needed
2. Caching Opportunity
This endpoint is a perfect caching candidate:
from fastapi import Response
@router.get("/{user_id_path:uuid}")
async def get_user_by_id(user_id_path: UUID, response: Response):
user_obj = await user_service.get_user_by_id_from_table(user_id=user_id_path)
if user_obj is None:
raise HTTPException(status_code=404, detail="User not found")
# Cache for 5 minutes
response.headers["Cache-Control"] = "public, max-age=300"
return user_obj
Benefits:
- CDN/browser can cache responses
- Reduces database load for popular profiles
- Stale data risk is low (profiles change infrequently)
3. Bulk Lookup Pattern
Scenario: Display 100 video thumbnails, each needs uploader name
Naive approach (N+1 query problem):
for video in videos:
uploader = await get_user_by_id(video.userid)
video.uploader_name = uploader.firstname
# 100 database queries!
Better approach: Bulk fetch
user_ids = [video.userid for video in videos]
users_map = await get_users_by_ids(user_ids) # 1 query with $in
for video in videos:
uploader = users_map.get(video.userid)
video.uploader_name = uploader.firstname if uploader else "Unknown"
# 1 database query!
Service function:
async def get_users_by_ids(user_ids: List[UUID]) -> Dict[UUID, User]:
table = await get_table("users")
cursor = table.find(
filter={"userid": {"$in": [str(uid) for uid in user_ids]}},
limit=len(user_ids)
)
docs = await cursor.to_list()
return {UUID(d["userid"]): User.model_validate(d) for d in docs}
4. 404 vs 200 with null
Question: Should non-existent users return 404 or 200 with null?
Current behavior: 404 "User not found"
if user_obj is None:
raise HTTPException(status_code=404, detail="User not found")
Trade-offs:
| Approach | Pros | Cons |
|---|---|---|
| 404 | Clear error signal, semantic correctness | Clients must handle errors |
| 200 + null | Easier client code | Ambiguous (null could mean loading) |
Recommendation: Stick with 404 (REST best practice)
5. User Deletion Handling
What if a user is deleted?
Better schema for soft deletes:
class User(BaseModel):
userId: UUID
firstname: str
deleted: bool = False # Add deleted flag
# In endpoint:
if user.deleted:
return PublicUserProfile(
userId=user.userId,
firstname="[Deleted User]",
lastname="",
...
)
Not implemented in current codebase.
Developer Tips
Common Pitfalls
-
Exposing sensitive data: Filter PII in public endpoints
-
N+1 queries: Use bulk fetch for lists
-
No caching: Public data should be cached aggressively
-
Not validating UUID: Let FastAPI's path parameter do it
-
Inconsistent 404 handling: Decide on a pattern and stick to it
Best Practices
-
Use separate response models for public vs private:
PublicUserProfile # Subset of fields User # All fields -
Add HTTP caching headers:
Cache-Control,ETag -
Implement bulk fetch helpers: Avoid N+1 query anti-pattern
-
Log 404s separately: Distinguish real errors from user not found
-
Consider soft deletes: Keep data for referential integrity
Performance Expectations
| Scenario | Latency | Notes |
|---|---|---|
| Cache hit (Redis) | < 1ms | Ideal for hot profiles |
| Cache miss | 5-10ms | Single Cassandra query |
| Bulk fetch (10 users) | 10-15ms | One query with $in |
| N+1 pattern (10 users) | 50-100ms | Avoid this! |
Scalability: Horizontal - add Cassandra nodes and Redis instances as needed
Testing Tips
# Test successful lookup
async def test_get_user_by_id():
response = await client.get(f"/api/v1/users/{test_user_id}")
assert response.status_code == 200
user = response.json()
assert user["userId"] == str(test_user_id)
# Test non-existent user
async def test_user_not_found():
fake_id = "00000000-0000-0000-0000-000000000000"
response = await client.get(f"/api/v1/users/{fake_id}")
assert response.status_code == 404
assert "not found" in response.json()["detail"].lower()
# Test invalid UUID format
async def test_invalid_uuid():
response = await client.get("/api/v1/users/not-a-uuid")
assert response.status_code == 422 # Validation error
Related Endpoints
- GET /api/v1/users/me - Get your own profile (authenticated)
- POST /api/v1/users/register - Create a new account
- POST /api/v1/users/login - Authenticate