How to Implement Article Favoriting in a RealWorld Backend Service

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, 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:

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:

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:

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 calculates the favorited boolean and favoritesCount integer:

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 resolves the follow relationship between the article author and the requesting user.

OpenAPI Specification

The API contract is defined in specs/api/openapi.yml (lines 393-424). The specification requires endpoints to return a SingleArticleResponse object containing the updated article with current favorite state:

/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:

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:

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 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 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, 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. 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →