# How RealWorld Prevents Duplicate Registrations for Usernames and Emails

> Learn how RealWorld prevents duplicate registrations using Prisma database constraints and API validation checks for unique usernames and emails, ensuring data integrity.

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

---

**RealWorld enforces username and email uniqueness through a dual-layer defense combining Prisma database constraints at the schema level and explicit pre-flight validation checks in both modern and legacy API endpoints that return HTTP 422 errors when conflicts are detected.**

The gothinkster/realworld repository demonstrates production-grade handling of duplicate registration attempts across its Node.js/Nuxt backend. By leveraging database-level uniqueness guarantees alongside application-layer validation, the system prevents account creation collisions while providing clear error feedback to client applications.

## Database-Level Protection with Prisma Schema Constraints

The foundation of duplicate prevention starts in the Prisma data model. In `apps/api/prisma/schema.prisma`, both the **email** and **username** fields are decorated with the `@unique` attribute, ensuring the underlying SQLite database physically rejects any insert operations that would create duplicate values.

```prisma
// apps/api/prisma/schema.prisma (lines 44-47)
model User {
  id       Int    @id @default(autoincrement())
  email    String @unique
  username String @unique
  // ... additional fields
}

```

This schema-level constraint acts as the final safety net. Even if application logic were bypassed, the database itself raises a constraint violation error, maintaining data integrity regardless of which API endpoint is used.

## API-Level Validation Strategies

Before reaching the database, both the V2 and legacy registration endpoints perform explicit existence checks using Prisma's `findUnique` method. This prevents unnecessary database constraint errors and allows the API to return structured validation messages.

### V2 Signup Endpoint

The modern registration route in [`apps/api/server/routes/api/v2/auth/signup.post.ts`](https://github.com/gothinkster/realworld/blob/main/apps/api/server/routes/api/v2/auth/signup.post.ts) (lines 12-30) optimizes for efficiency by performing a single query with an **OR clause** to detect either username or email collisions simultaneously.

```typescript
// POST /api/v2/auth/signup
const { username, email, password } = await readValidatedBody(event, userSchema.parse);

const existingUser = await usePrisma().user.findUnique({
  where: { 
    OR: [{ email }, { username }] 
  },
  select: { id: true },
});

if (existingUser) {
  return createError({
    status: 422,
    statusMessage: 'Unprocessable Content',
    data: "User already exists",
  });
}

```

This approach minimizes database round-trips by consolidating the uniqueness check into one query, returning a generic "User already exists" message that avoids revealing which specific field (email or username) caused the conflict.

### Legacy Registration Endpoint

The legacy route in [`apps/api/server/routes/api/users/index.post.ts`](https://github.com/gothinkster/realworld/blob/main/apps/api/server/routes/api/users/index.post.ts) (lines 24-80) implements a more granular validation strategy using the `checkUserUniqueness` helper function. This method executes separate `findUnique` queries for each field to provide field-specific error messages.

```typescript
// POST /api/users
await checkUserUniqueness(email, username); // Throws 422 if duplicate found

const hashedPassword = await bcrypt.hash(password, 10);
const createdUser = await usePrisma().user.create({ /* ... */ });

```

The helper function defined in the same file performs two distinct lookups and constructs a detailed error payload:

```typescript
const checkUserUniqueness = async (email: string, username: string) => {
  const existingByEmail = await usePrisma().user.findUnique({ where: { email } });
  const existingByUsername = await usePrisma().user.findUnique({ where: { username } });

  if (existingByEmail || existingByUsername) {
    throw new HttpException(422, {
      errors: {
        ...(existingByEmail ? { email: ['has already been taken'] } : {}),
        ...(existingByUsername ? { username: ['has already been taken'] } : {}),
      },
    });
  }
};

```

## Error Handling and Response Format

Both endpoints utilize consistent HTTP **422 Unprocessable Content** status codes to signal validation failures, though their response structures differ:

- **V2 endpoint**: Returns a simplified error object with the message "User already exists"
- **Legacy endpoint**: Returns a structured `errors` object identifying specific fields that failed uniqueness validation

```json
// Legacy endpoint response format
{
  "errors": {
    "email": ["has already been taken"],
    "username": ["has already been taken"]
  }
}

```

This standardization allows frontend applications to parse validation errors predictably, whether using the modern Nuxt-based API or maintaining compatibility with the legacy Express-style endpoints.

## Summary

- **Database constraints** in `apps/api/prisma/schema.prisma` enforce uniqueness via `@unique` attributes on both email and username fields, providing hard integrity guarantees at the SQLite level.
- **V2 validation** in [`apps/api/server/routes/api/v2/auth/signup.post.ts`](https://github.com/gothinkster/realworld/blob/main/apps/api/server/routes/api/v2/auth/signup.post.ts) uses a single Prisma `findUnique` query with an OR clause to detect duplicates efficiently, returning generic 422 errors.
- **Legacy validation** in [`apps/api/server/routes/api/users/index.post.ts`](https://github.com/gothinkster/realworld/blob/main/apps/api/server/routes/api/users/index.post.ts) leverages the `checkUserUniqueness` helper to perform separate lookups and return field-specific error messages.
- **Consistent HTTP 422 status codes** across both endpoints ensure client applications can reliably handle duplicate registration attempts with appropriate user feedback.

## Frequently Asked Questions

### What HTTP status code does RealWorld return for duplicate registrations?

RealWorld returns **HTTP 422 Unprocessable Content** when detecting duplicate usernames or emails during registration. Both the V2 and legacy endpoints implement this status code through `createError` or `HttpException` helpers, signaling to clients that the request data violates uniqueness constraints without implying a server error or authentication failure.

### Does RealWorld check for duplicate usernames and emails separately or together?

The implementation varies by endpoint version. The **V2 endpoint** ([`signup.post.ts`](https://github.com/gothinkster/realworld/blob/main/signup.post.ts)) checks both fields simultaneously using a single Prisma `findUnique` query with an `OR` clause, optimizing for database efficiency. The **legacy endpoint** ([`users/index.post.ts`](https://github.com/gothinkster/realworld/blob/main/users/index.post.ts)) performs two separate `findUnique` queries through the `checkUserUniqueness` helper to determine which specific field caused the conflict and generate targeted error messages.

### Where are the uniqueness constraints defined in the RealWorld codebase?

The primary constraints reside in `apps/api/prisma/schema.prisma` at lines 44-47, where the `User` model declares both `email` and `username` fields with the `@unique` attribute. These Prisma schema definitions generate the underlying SQLite database constraints that enforce uniqueness at the storage layer, independent of application logic.

### How does the legacy endpoint provide field-specific error messages?

The legacy route utilizes the `checkUserUniqueness` helper function defined in [`apps/api/server/routes/api/users/index.post.ts`](https://github.com/gothinkster/realworld/blob/main/apps/api/server/routes/api/users/index.post.ts) (lines 24-80). This function executes separate `findUnique` queries for the email and username fields, then constructs a 422 error response containing an `errors` object that specifies exactly which fields are already taken, enabling precise frontend validation highlighting.