Implementing Article Publishing in a RealWorld Backend: Node.js and Prisma Workflow

Article publishing in the RealWorld backend validates the request payload against required fields, generates a unique URL slug from the title and author ID, queries for duplicates, persists the article with Prisma using relational connections for tags and authors, and maps the database record to the Conduit API format before returning the response.

The RealWorld demo application provides a full-stack Medium clone specification that backend frameworks must implement to demonstrate their capabilities. This guide examines the exact workflow for implementing article publishing in a RealWorld backend using the official Node.js and Prisma implementation from the gothinkster/realworld repository.

Validating the Article Payload

When a client sends a POST /api/articles request, the route handler in apps/api/server/routes/api/articles/index.post.ts reads the JSON body using readBody(event) and extracts the article object. The handler immediately validates that title, description, and body fields are present in the request.

If any required field is missing, the endpoint throws a 422 error to signal invalid input. This validation occurs in lines 7-22 of the route handler, ensuring only complete article data proceeds to the persistence layer.

Generating a Unique URL Slug

After validation, the backend constructs a URL-friendly slug to identify the article permanently. The implementation uses the slugify library combined with the authenticated user's ID to guarantee uniqueness across the entire platform:

const slug = `${slugify(title)}-${auth.id}`;

This approach ensures that two different users can publish articles with identical titles without collision, as the author ID appended to the slug guarantees uniqueness. The slug generation logic resides at line 24 of the same route file.

Preventing Duplicate Article Titles

Before persisting the new article, the handler queries the Article table to verify that no existing record already uses the generated slug. This duplicate check prevents users from republishing the same article twice or accidentally overwriting existing content.

If the query returns an existing article with that slug, the endpoint immediately returns a 422 error with the message indicating that the title must be unique. This safeguard appears in lines 26-36 of apps/api/server/routes/api/articles/index.post.ts.

Persisting the Article with Prisma

Once validation and uniqueness checks pass, the backend executes the primary database operation using Prisma's create method. The usePrisma().article.create call stores the article while simultaneously handling relational data through the following strategy:

  • Core fields: title, description, body, and slug are stored directly from the request data
  • Tag handling: The tagList relation uses connectOrCreate to either link existing tags or create new ones atomically
  • Author linkage: The author relation connects to the authenticated user via auth.id

The Prisma query also includes related data needed for the API response:

const {
  authorId,
  id: articleId,
  ...createdArticle
} = await usePrisma().article.create({
  data: {
    title,
    description,
    body,
    slug,
    tagList: {
      connectOrCreate: tags.map(tag => ({
        create: { name: tag },
        where: { name: tag },
      })),
    },
    author: { 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 } },
  },
});

This database transaction, found in lines 44-81 of the route handler, efficiently creates the article record while fetching the tag names, author profile, favorite status, and favorite count in a single query.

Mapping Database Records to the API Format

The raw Prisma record contains nested relational objects that do not match the Conduit API specification. The articleMapper utility function, located in apps/api/server/utils/article.mapper.ts, transforms the database structure into the expected response format.

The mapper adds computed fields including:

  • favorited: A boolean indicating whether the requesting user appears in the favoritedBy array
  • favoritesCount: The total number of users who favorited the article
  • author: A sub-object formatted by authorMapper containing profile information

The route returns the final payload using:

return { article: articleMapper(createdArticle, auth.id) };

Prisma Schema Support

The underlying data model enabling this workflow is defined in apps/api/prisma/schema.prisma. The Article model establishes the necessary relations and constraints:

model Article {
  id          Int      @id @default(autoincrement())
  slug        String   @unique
  title       String
  description String
  body        String
  createdAt   DateTime @default(now())
  updatedAt   DateTime @default(now())
  tagList     Tag[]
  author      User     @relation("UserArticles", fields: [authorId], onDelete: Cascade, references: [id])
  authorId    Int
  favoritedBy User[]   @relation("UserFavorites")
  comments    Comment[]
}

The schema enforces unique slugs at the database level through the @unique attribute on the slug field, while the onDelete: Cascade setting ensures that articles are removed when their author is deleted.

Summary

  • Request validation occurs immediately upon receiving POST /api/articles, requiring title, description, and body fields to be present or returning a 422 error
  • Slug generation combines slugify(title) with the author's ID to create unique, URL-friendly identifiers stored in the slug field
  • Duplicate prevention queries the Article table before creation, rejecting requests if the generated slug already exists
  • Prisma transactions use connectOrCreate for tag management and connect for author relations while including related data (tags, author profile, favorites) in a single query
  • Data transformation through articleMapper converts Prisma's relational objects into the Conduit API specification, adding computed fields like favorited status and favoritesCount

Frequently Asked Questions

How does the RealWorld backend ensure article slugs are unique?

The backend generates slugs by combining the slugified article title with the authenticated user's ID using the pattern ${slugify(title)}-${auth.id}. This composite key guarantees uniqueness across users because each author has a distinct ID, preventing collisions even when multiple users publish articles with identical titles.

What happens if a user tries to publish an article with a duplicate title?

The route handler queries the Article table to check for an existing record with the generated slug before creating a new entry. If a match exists, the endpoint returns a 422 error with a message indicating that the title must be unique, preventing duplicate publications without altering existing data.

How are article tags handled during the publishing workflow?

The Prisma create operation uses connectOrCreate logic within the tagList relation. For each tag provided in the request, the database either connects to an existing tag record or creates a new one atomically. This approach maintains referential integrity while allowing articles to reference multiple tags without requiring separate tag creation endpoints.

What data is included in the article creation response?

The response contains a complete article object following the Conduit API specification, including the slug, title, description, body, tagList, createdAt, updatedAt, favorited status, favoritesCount, and a nested author object with profile details. The articleMapper utility in apps/api/server/utils/article.mapper.ts transforms the raw Prisma record to include these computed and relational fields.

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 →