6.6 KiB
REST API Cleanup Design
Clean up the REST API to be professional, consistent, and RESTful. No new features — only renames, unifications, and content negotiation.
Route Changes
| Before | After | Reason |
|---|---|---|
GET /users/{username}/profile |
GET /users/{username} |
content negotiation replaces the /profile workaround |
GET /federation/lookup?handle= |
GET /users/lookup?handle= |
federation lookup belongs under /users |
POST /users/{id}/follow |
POST /users/{username}/follow |
param was mislabelled; now also handles remote follows |
DELETE /users/{id}/follow |
DELETE /users/{username}/follow |
param rename |
POST /users/{id}/block |
POST /users/{username}/block |
param rename |
DELETE /users/{id}/block |
DELETE /users/{username}/block |
param rename |
GET /users/{username}/follower-list |
GET /users/{username}/followers |
verbose name |
GET /users/{username}/following-list |
GET /users/{username}/following |
verbose name |
GET /users/me/following-list |
GET /users/me/following |
verbose name |
POST /notifications/{id}/read |
PATCH /notifications/{id} |
POST for state change → PATCH |
POST /notifications/read-all |
PATCH /notifications |
POST bulk action → PATCH |
PUT /users/me |
removed | PATCH /users/me is sufficient |
POST /federation/follow |
removed | unified into POST /users/{username}/follow |
Content Negotiation at GET /users/{username}
The AP router currently owns /users/{username} (returns application/activity+json). The REST profile was at /users/{username}/profile as a workaround.
Solution: Remove /users/{username} from the AP router. Add a single handler at GET /users/{username} in the REST router that checks the Accept header:
Accept: application/activity+json→ return AP actor JSON withContent-Type: application/activity+json- Anything else → return
UserResponsewithContent-Type: application/json
Implementation:
Add actor_json(&self, user_id: &UserId) -> Result<String, DomainError> to FederationActionPort in domain. Implement in ActivityPubService by delegating to the existing self.actor_json(&user_id.as_uuid().to_string()) inherent method.
The unified handler in presentation/src/handlers/users.rs:
- Looks up user by username via
UserRepository→ 404 if not found - Checks
Acceptheader - AP path: calls
s.federation.actor_json(&user.id)→ returns withContent-Type: application/activity+json - REST path: returns
UserResponseas before
The AP router in bootstrap/src/main.rs no longer registers /users/{username}.
Unified Follow at POST /users/{username}/follow
The handler detects whether {username} is a local user or a remote actor:
if username.contains('@') {
// Remote: e.g. "gabrielkaszewski@mastodon.social"
s.federation.follow_remote(&uid, &username).await?;
} else {
// Local: look up by username, call follow_user use case
let target = get_user_by_username(&*s.users, &username).await?;
follow_user(&*s.follows, &*s.events, &uid, &target.id).await?;
}
POST /federation/follow and federation::follow_remote_handler are deleted.
Remote Actor Handle Format Fix
lookup_actor currently returns handle: actor.username (just preferred_username, e.g. gabrielkaszewski). Fix: return the full user@domain handle by extracting the domain from actor.ap_id:
let domain = actor.ap_id.host_str().unwrap_or("");
let full_handle = format!("{}@{}", actor.username, domain);
// RemoteActor { handle: full_handle, ... }
This means RemoteActorResponse.handle = "gabrielkaszewski@mastodon.social", which the frontend passes directly to POST /users/gabrielkaszewski@mastodon.social/follow.
Remote Unfollow Scope
DELETE /users/{username}/follow for a remote handle (contains @) is out of scope. The handler returns 501 Not Implemented when username contains @. Remote unfollow requires an Undo Follow ActivityPub activity and is a separate feature.
Notification Endpoints
Add NotificationUpdateRequest { read: bool } to api-types/src/requests.rs.
PATCH /notifications/{id}— mark single notification read (body:{"read": true})PATCH /notifications— mark all notifications read (body:{"read": true})
Both replace their existing POST counterparts.
Frontend (thoughts-frontend/lib/api.ts)
| Function | Change |
|---|---|
getUserProfile(username) |
URL: /users/${username}/profile → /users/${username} |
getFollowersList(username) |
URL: /follower-list → /followers |
getFollowingList(username) |
URL: /following-list → /following |
getMeFollowingList() |
URL: /me/following-list → /me/following |
lookupRemoteActor(handle) |
URL: /federation/lookup?handle= → /users/lookup?handle= |
followRemoteUser(handle) |
Deleted — use unified followUser(handle) instead |
markNotificationRead(id) |
New — PATCH /notifications/{id} with body {"read":true} (no prior frontend impl) |
markAllNotificationsRead() |
New — PATCH /notifications with body {"read":true} (no prior frontend impl) |
Also update remote-user-card.tsx to call followUser(actor.handle, token) instead of followRemoteUser.
Files Touched
Backend:
crates/domain/src/ports.rs— addactor_jsontoFederationActionPortcrates/domain/src/testing.rs— addactor_jsontoTestStoreimplcrates/adapters/activitypub-base/src/service.rs— addactor_jsontoFederationActionPortimpl; fixlookup_actorhandle formatcrates/presentation/src/handlers/users.rs— unifiedGET /users/{username}handler; remove oldget_user(was /profile)crates/presentation/src/handlers/social.rs— unifypost_follow; rename{id}→{username}in follow/block; rename follower/following list handlerscrates/presentation/src/handlers/federation.rs— deletefollow_remote_handler; movelookup_handlertousers.rs; delete file if emptycrates/presentation/src/handlers/notifications.rs— replace read handlers with PATCHcrates/presentation/src/routes.rs— all route changescrates/api-types/src/requests.rs— addNotificationUpdateRequestcrates/bootstrap/src/main.rs— remove/users/{username}from ap_router
Frontend:
thoughts-frontend/lib/api.ts— all URL/method changes listed abovethoughts-frontend/components/remote-user-card.tsx— usefollowUserinstead offollowRemoteUser- Any page that calls
getFollowersList,getFollowingList,getMeFollowingList,markNotificationRead,markAllNotificationsRead(check all pages underapp/)