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

> Learn how to implement article publishing in a Node.js and Prisma backend. Discover the workflow for validation, slug generation, duplicate checking, and persistence with relational connections.

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

---

**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`](https://github.com/gothinkster/realworld/blob/main/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:

```typescript
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`](https://github.com/gothinkster/realworld/blob/main/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:

```typescript
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`](https://github.com/gothinkster/realworld/blob/main/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:

```typescript
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:

```prisma
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`](https://github.com/gothinkster/realworld/blob/main/apps/api/server/utils/article.mapper.ts) transforms the raw Prisma record to include these computed and relational fields.