Files
thoughts/docs/superpowers/specs/2026-05-14-api-cleanup-design.md

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 with Content-Type: application/activity+json
  • Anything else → return UserResponse with Content-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:

  1. Looks up user by username via UserRepository → 404 if not found
  2. Checks Accept header
  3. AP path: calls s.federation.actor_json(&user.id) → returns with Content-Type: application/activity+json
  4. REST path: returns UserResponse as 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) NewPATCH /notifications/{id} with body {"read":true} (no prior frontend impl)
markAllNotificationsRead() NewPATCH /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 — add actor_json to FederationActionPort
  • crates/domain/src/testing.rs — add actor_json to TestStore impl
  • crates/adapters/activitypub-base/src/service.rs — add actor_json to FederationActionPort impl; fix lookup_actor handle format
  • crates/presentation/src/handlers/users.rs — unified GET /users/{username} handler; remove old get_user (was /profile)
  • crates/presentation/src/handlers/social.rs — unify post_follow; rename {id}{username} in follow/block; rename follower/following list handlers
  • crates/presentation/src/handlers/federation.rs — delete follow_remote_handler; move lookup_handler to users.rs; delete file if empty
  • crates/presentation/src/handlers/notifications.rs — replace read handlers with PATCH
  • crates/presentation/src/routes.rs — all route changes
  • crates/api-types/src/requests.rs — add NotificationUpdateRequest
  • crates/bootstrap/src/main.rs — remove /users/{username} from ap_router

Frontend:

  • thoughts-frontend/lib/api.ts — all URL/method changes listed above
  • thoughts-frontend/components/remote-user-card.tsx — use followUser instead of followRemoteUser
  • Any page that calls getFollowersList, getFollowingList, getMeFollowingList, markNotificationRead, markAllNotificationsRead (check all pages under app/)