# How to Implement Follow and Unfollow Functionality in the RealWorld API

> Learn to implement user follow and unfollow functionality in the RealWorld API using Prisma. Discover authenticated endpoints and normalized profile objects with a following flag.

- Repository: [Thinkster/realworld](https://github.com/gothinkster/realworld)
- Tags: how-to-guide
- Published: 2026-02-28

---

**The RealWorld application implements follow and unfollow functionality through a self-referencing many-to-many relationship in Prisma, exposed via authenticated POST and DELETE endpoints that return a normalized Profile object with a boolean `following` flag.**

The **follow and unfollow functionality** allows authenticated users to curate their personal article feed by subscribing to or removing specific authors. As implemented in the `gothinkster/realworld` monorepo, this feature spans the database schema, REST API, and frontend integration layers using a consistent contract across all framework implementations.

## Database Schema Design

The foundation relies on a **self-referencing many-to-many relation** defined in the Prisma schema. In `apps/api/prisma/schema.prisma`, the `User` model declares two relation fields that reference each other:

```prisma
model User {
  id         Int      @id @default(autoincrement())
  username   String   @unique
  // …other fields…

  // Self-referencing many-to-many relation:
  followedBy User[]   @relation("UserFollows")
  following  User[]   @relation("UserFollows")
}

```

- **`followedBy`** contains the users that follow this specific user.
- **`following`** contains the users this specific user follows.

Prisma automatically generates the junction table `_UserFollows` to manage this relationship, as seen in the migration files.

## API Endpoints and Contract

The backend exposes two private endpoints under the `/api/profiles/:username/follow` route. Both require Bearer token authentication and return a standardized `Profile` payload.

| Method | Endpoint | Auth | Response |
|--------|----------|------|----------|
| `POST` | `/api/profiles/:username/follow` | Bearer token | `{ profile: { username, bio, image, following: true } }` |
| `DELETE` | `/api/profiles/:username/follow` | Bearer token | `{ profile: { username, bio, image, following: false } }` |

These routes utilize the `definePrivateEventHandler` wrapper, which injects the authenticated user's ID into the request context.

## Server-Side Implementation

### Following a User (POST)

In `apps/api/server/routes/api/profiles/[username]/follow/index.post.ts`, the handler connects the current user to the target's `followedBy` relation:

```typescript
import profileMapper from "~/utils/profile.utils";
import { definePrivateEventHandler } from "~/auth-event-handler";

export default definePrivateEventHandler(async (event, { auth }) => {
  const username = getRouterParam(event, "username");

  const profile = await usePrisma().user.update({
    where: { username },
    data: {
      // Add the current user to the target's followedBy list
      followedBy: { connect: { id: auth.id } },
    },
    include: { followedBy: true },
  });

  // Return a normalized profile payload
  return { profile: profileMapper(profile, auth.id) };
});

```

### Unfollowing a User (DELETE)

In `apps/api/server/routes/api/profiles/[username]/follow/index.delete.ts`, the handler disconnects the current user from the target's `followedBy` relation:

```typescript
import profileMapper from "~/utils/profile.utils";
import { definePrivateEventHandler } from "~/auth-event-handler";

export default definePrivateEventHandler(async (event, { auth }) => {
  const username = getRouterParam(event, "username");

  const profile = await usePrisma().user.update({
    where: { username },
    data: {
      // Remove the current user from the target's followedBy list
      followedBy: { disconnect: { id: auth.id } },
    },
    include: { followedBy: true },
  });

  return { profile: profileMapper(profile, auth.id) };
});

```

### Profile Mapping Utility

Both handlers rely on `profileMapper` in [`apps/api/server/utils/profile.utils.ts`](https://github.com/gothinkster/realworld/blob/main/apps/api/server/utils/profile.utils.ts) to compute the `following` boolean and transform the database record into the API response format:

```typescript
import { User } from "~/models/user.model";
import { Profile } from "~/models/profile.model";

const profileMapper = (user: any, id: number | undefined): Profile => ({
  username: user.username,
  bio: user.bio,
  image: user.image,
  following: id
    ? user?.followedBy.some((followingUser: Partial<User>) => followingUser.id === id)
    : false,
});

export default profileMapper;

```

The function checks if the authenticated user's ID exists within the target user's `followedBy` array, returning `true` if the follow relationship exists.

## Frontend Integration

The client implementation toggles the follow state by calling the appropriate endpoint and updating the UI based on the response. A generic JavaScript implementation follows this pattern:

```javascript
async function toggleFollow(username, currentlyFollowing) {
  const method = currentlyFollowing ? "DELETE" : "POST";
  const response = await fetch(`/api/profiles/${username}/follow`, {
    method,
    credentials: "include", // sends the auth cookie / token
    headers: { "Content-Type": "application/json" },
  });
  const { profile } = await response.json();
  
  // Update button text based on the new state
  button.textContent = profile.following ? "Unfollow" : "Follow";
}

```

The expected UI selectors are documented in the end-to-end test helpers at [`specs/e2e/helpers/profile.ts`](https://github.com/gothinkster/realworld/blob/main/specs/e2e/helpers/profile.ts):

```typescript
export async function followUser(page: Page, username: string) {
  await page.goto(`/profile/@${username}`);
  await page.waitForSelector('button:has-text("Follow")', { timeout: 10000 });
  await page.click('button:has-text("Follow")');
}

export async function unfollowUser(page: Page, username: string) {
  await page.waitForSelector('button:has-text("Unfollow")', { timeout: 10000 });
  await page.click('button:has-text("Unfollow")');
}

```

When the button displays **Unfollow**, the client has received `{ following: true }` from the server; clicking it again invokes the DELETE endpoint and receives `{ following: false }`.

## Feed Integration

The follow and unfollow functionality directly impacts the "Your Feed" feature. In [`apps/api/server/routes/api/articles/feed.get.ts`](https://github.com/gothinkster/realworld/blob/main/apps/api/server/routes/api/articles/feed.get.ts), the endpoint filters articles where the author's `followedBy` relation includes the authenticated user:

```typescript
const articles = await usePrisma().article.findMany({
  where: { author: { followedBy: { some: { id: auth.id } } } },
  include: { author: { include: { followedBy: true } } },
});

```

Therefore, successful follow operations immediately populate the user's personalized feed with the followed author's articles, while unfollow operations remove them.

## Summary

- **Database Layer**: Uses a self-referencing many-to-many Prisma relation (`followedBy` / `following`) on the `User` model defined in `apps/api/prisma/schema.prisma`.
- **API Layer**: Provides authenticated `POST` and `DELETE` endpoints at `/api/profiles/:username/follow` using `definePrivateEventHandler`.
- **Business Logic**: Leverages Prisma's `connect` and `disconnect` operations to mutate relationships atomically.
- **Response Format**: Returns a normalized `Profile` object via `profileMapper` in [`apps/api/server/utils/profile.utils.ts`](https://github.com/gothinkster/realworld/blob/main/apps/api/server/utils/profile.utils.ts), including a computed `following` boolean.
- **Frontend Contract**: Expects UI implementations to toggle between "Follow" and "Unfollow" button states based on the API response.
- **Feed Impact**: The "Your Feed" endpoint filters articles by the `followedBy` relation, ensuring real-time feed updates.

## Frequently Asked Questions

### How does the RealWorld API track which users follow each other?

The system tracks relationships through a **self-referencing many-to-many relation** in the Prisma schema. The `User` model contains two fields—`followedBy` and `following`—that both participate in the `"UserFollows"` relation. Prisma automatically manages the junction table `_UserFollows` behind the scenes, storing pairs of user IDs to represent each follow relationship.

### What is the difference between the POST and DELETE follow endpoints?

The **POST** endpoint (`/api/profiles/:username/follow`) creates a follow relationship by using Prisma's `connect` operation to add the authenticated user to the target's `followedBy` list. The **DELETE** endpoint removes the relationship using Prisma's `disconnect` operation. Both endpoints return an identical `Profile` payload structure, differing only in the `following` boolean value (`true` for POST, `false` for DELETE).

### How does the frontend know whether to show "Follow" or "Unfollow"?

The `profileMapper` utility in [`apps/api/server/utils/profile.utils.ts`](https://github.com/gothinkster/realworld/blob/main/apps/api/server/utils/profile.utils.ts) computes the `following` boolean by checking if the authenticated user's ID exists in the target user's `followedBy` array. The API includes this flag in every Profile response, allowing the frontend to render the appropriate button state without maintaining separate client-side logic.

### Why do followed users appear in "Your Feed" immediately?

The feed endpoint in [`apps/api/server/routes/api/articles/feed.get.ts`](https://github.com/gothinkster/realworld/blob/main/apps/api/server/routes/api/articles/feed.get.ts) queries articles using a Prisma `where` clause that filters for authors whose `followedBy` relation contains the current user's ID. Because the follow endpoints modify this same relation using atomic `connect` and `disconnect` operations, the feed query reflects changes immediately without requiring additional caching or synchronization steps.