# How to Implement Article Favoriting in a RealWorld Backend Service

> Learn to implement article favoriting in a RealWorld backend. Discover how authenticated REST endpoints use Prisma many-to-many relations and mappers for seamless user article connections.

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

---

**RealWorld backends implement article favoriting as authenticated REST endpoints that leverage Prisma's many-to-many relations to connect or disconnect users from articles, with response transformation handled by dedicated mapper utilities.**

The `gothinkster/realworld` repository provides a full-stack specification for building production-ready applications. Implementing **article favoriting functionality** requires handling protected routes, managing database relations, and returning standardized responses that include real-time favorite counts and user-specific states.

## Authentication and Route Protection

All favorite endpoints require authentication using JWT tokens. The application wraps route handlers with **`definePrivateEventHandler`**, which extracts the authenticated user from the `Authorization` header and injects the `auth.id` into the request context.

In [`apps/api/server/utils/auth.ts`](https://github.com/gothinkster/realworld/blob/main/apps/api/server/utils/auth.ts), the authentication logic validates the token and ensures only logged-in users can favorite or unfavorite articles. Attempting to access `POST /articles/{slug}/favorite` or `DELETE /articles/{slug}/favorite` without valid credentials returns a 401 Unauthorized error.

## Database Schema Design

The favoriting system relies on a many-to-many relationship between the `User` and `Article` models. In `apps/api/prisma/schema.prisma`, this relation is explicitly named `UserFavorites`:

```prisma
model User {
  id        Int      @id @default(autoincrement())
  favorites Article[] @relation("UserFavorites")
}

model Article {
  id          Int      @id @default(autoincrement())
  favoritedBy User[]   @relation("UserFavorites")
}

```

This schema allows Prisma to maintain the join table automatically. When a user favorites an article, the system creates a connection in this relation; when they unfavorite, it removes the connection.

## Handling Favorite and Unfavorite Requests

### POST Endpoint Implementation

The favorite creation handler lives in `apps/api/server/routes/api/articles/[slug]/favorite/index.post.ts`. It uses Prisma's **connect** operation to link the current user to the article:

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

  const { _count, ...article } = await usePrisma().article.update({
    where: { slug },
    data: { favoritedBy: { connect: { id: auth.id } } },
    include: {
      tagList: { select: { name: true } },
      author: { select: { username: true, bio: true, image: true, followedBy: true } },
      favoritedBy: true,
      _count: { select: { favoritedBy: true } },
    },
  });

  const result = {
    ...article,
    author: profileMapper(article.author, auth.id),
    tagList: article?.tagList.map(t => t.name),
    favorited: article.favoritedBy.some(f => f.id === auth.id),
    favoritesCount: _count?.favoritedBy,
  };

  return { article: result };
});

```

### DELETE Endpoint Implementation

The unfavorite handler in `apps/api/server/routes/api/articles/[slug]/favorite/index.delete.ts` performs the inverse operation using **disconnect**:

```typescript
data: { favoritedBy: { disconnect: { id: auth.id } } }

```

Both handlers request the `_count` aggregation to obtain the total number of favorites without loading the entire relation, optimizing database performance.

## Transforming the Response with articleMapper

After mutation, raw Prisma results require transformation to match the OpenAPI specification. The **`articleMapper`** utility in [`apps/api/server/utils/article.mapper.ts`](https://github.com/gothinkster/realworld/blob/main/apps/api/server/utils/article.mapper.ts) calculates the `favorited` boolean and `favoritesCount` integer:

```typescript
const articleMapper = (article: any, id?: number) => ({
  slug: article.slug,
  title: article.title,
  description: article.description,
  body: article.body,
  tagList: article.tagList.map((t: any) => t.name),
  createdAt: article.createdAt,
  updatedAt: article.updatedAt,
  favorited: article.favoritedBy.some((item: any) => item.id === id),
  favoritesCount: article.favoritedBy.length,
  author: authorMapper(article.author, id),
});

```

The `favorited` field indicates whether the requesting user appears in the `favoritedBy` array, while `favoritesCount` reflects the current total. For the author field, **`profileMapper`** from [`apps/api/server/utils/profile.utils.ts`](https://github.com/gothinkster/realworld/blob/main/apps/api/server/utils/profile.utils.ts) resolves the follow relationship between the article author and the requesting user.

## OpenAPI Specification

The API contract is defined in [`specs/api/openapi.yml`](https://github.com/gothinkster/realworld/blob/main/specs/api/openapi.yml) (lines 393-424). The specification requires endpoints to return a `SingleArticleResponse` object containing the updated article with current favorite state:

```yaml
/articles/{slug}/favorite:
  post:
    tags: [Favorites]
    summary: Favorite an article
    # ...

  delete:
    tags: [Favorites]
    summary: Unfavorite an article

```

## Client Integration Examples

To favorite an article programmatically, send an authenticated POST request:

```typescript
import fetch from "node-fetch";

async function favoriteArticle(slug: string, token: string) {
  const res = await fetch(`https://api.realworld.show/api/articles/${slug}/favorite`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "Authorization": `Token ${token}`,
    },
  });

  if (!res.ok) throw new Error(`Failed: ${res.status}`);
  const { article } = await res.json();
  console.log(`Favorited: ${article.title} (count: ${article.favoritesCount})`);
}

```

To unfavorite via command line:

```bash
curl -X DELETE "https://api.realworld.show/api/articles/awesome-article/favorite" \
  -H "Authorization: Token $TOKEN"

```

## Summary

- **Authentication**: Routes use `definePrivateEventHandler` from [`apps/api/server/utils/auth.ts`](https://github.com/gothinkster/realworld/blob/main/apps/api/server/utils/auth.ts) to enforce JWT validation.
- **Database Relations**: Prisma manages the many-to-many `UserFavorites` relation between `User` and `Article` models in `apps/api/prisma/schema.prisma`.
- **Mutations**: POST connects the user via `favoritedBy: { connect: { id: auth.id } }`; DELETE disconnects using the same pattern.
- **Response Mapping**: The `articleMapper` utility calculates `favorited` and `favoritesCount` fields for the API response.
- **Specification**: OpenAPI definitions in [`specs/api/openapi.yml`](https://github.com/gothinkster/realworld/blob/main/specs/api/openapi.yml) standardize the request and response formats.

## Frequently Asked Questions

### What database relation type does RealWorld use for article favorites?

The implementation uses a **many-to-many relation** named `UserFavorites` defined in the Prisma schema. This creates an implicit join table between the `User` and `Article` models, allowing efficient connection and disconnection of users from articles without manual join table management.

### How does the backend determine if the current user has favorited an article?

The system checks the `favoritedBy` array returned by Prisma. In [`apps/api/server/utils/article.mapper.ts`](https://github.com/gothinkster/realworld/blob/main/apps/api/server/utils/article.mapper.ts), the code executes `article.favoritedBy.some((item: any) => item.id === id)` to return a boolean `favorited` field indicating the requesting user's presence in the relation.

### Which utility function protects the favorite endpoints from unauthorized access?

**`definePrivateEventHandler`** wraps the route handlers in `apps/api/server/routes/api/articles/[slug]/favorite/index.post.ts` and [`index.delete.ts`](https://github.com/gothinkster/realworld/blob/main/index.delete.ts). This utility extracts and validates the JWT token from the `Authorization` header, rejecting requests without valid authentication before reaching the database layer.

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

The **POST** endpoint connects the authenticated user to the article using Prisma's `connect` operation, incrementing the favorite count. The **DELETE** endpoint uses `disconnect` to remove the relation, decrementing the count. Both return the updated article object with recalculated `favorited` and `favoritesCount` fields according to the OpenAPI specification.