How RealWorld Prevents Duplicate Registrations for Usernames and Emails
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.
// 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 (lines 12-30) optimizes for efficiency by performing a single query with an OR clause to detect either username or email collisions simultaneously.
// 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 (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.
// 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:
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
errorsobject identifying specific fields that failed uniqueness validation
// 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.prismaenforce uniqueness via@uniqueattributes 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.tsuses a single PrismafindUniquequery with an OR clause to detect duplicates efficiently, returning generic 422 errors. - Legacy validation in
apps/api/server/routes/api/users/index.post.tsleverages thecheckUserUniquenesshelper 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) 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) 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 (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.
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 →