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
definePrivateEventHandlerfromapps/api/server/utils/auth.tsto enforce JWT validation. - Database Relations: Prisma manages the many-to-many
UserFavoritesrelation betweenUserandArticlemodels inapps/api/prisma/schema.prisma. - Mutations: POST connects the user via
favoritedBy: { connect: { id: auth.id } }; DELETE disconnects using the same pattern. - Response Mapping: The
articleMapperutility calculatesfavoritedandfavoritesCountfields for the API response. - Specification: OpenAPI definitions in
specs/api/openapi.ymlstandardize 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →