DEV Community

linou518
linou518

Posted on

Making Shop Auth Accept Phone OR Email — A zod + TypeScript Fix

Users wanted to register with just a phone number, but the backend required an email. A small bug with real impact.

Background

Our LINE-integrated e-commerce site targets users who often do not have or do not want to enter an email address. The registration form said just enter your phone number, but submitting it returned 400 Bad Request.

The cause was simple: the frontend was sending only phone, but the backend validation had email: required.

What We Changed

Frontend (Vue + Vite)

Changed the email field to optional:

<input
  v-model="form.email"
  type="email"
  placeholder="Email (optional)"
/>
Enter fullscreen mode Exit fullscreen mode

Removed the email-required check from client-side validation.

Backend (Node.js / ts-node)

Fixed the /api/auth/register endpoint validation:

// Before
const schema = z.object({
  phone: z.string(),
  email: z.string().email(),  // The problem
  password: z.string().min(6),
});

// After
const schema = z.object({
  phone: z.string().optional(),
  email: z.string().email().optional(),
  password: z.string().min(6),
}).refine(data => data.phone || data.email, {
  message: 'Either phone or email is required',
});
Enter fullscreen mode Exit fullscreen mode

Login was updated similarly:

const user = await User.findOne({
  where: phone ? { phone } : { email },
});
Enter fullscreen mode Exit fullscreen mode

The Gotcha

During testing, we hit a phone number already registered error. A test admin account had a real user phone number attached. A reminder of the risks of developing against production data.

Fix: guide those users to the login flow instead, with a clear already registered message.

Takeaways

Required depends on user context. Developers naturally think of email as the primary identifier, but for many real-world users, phone numbers are far more familiar. In BtoC services, auth UX directly impacts drop-off rates.

zod .refine() is great for cross-field validation. The either A or B must be present pattern cannot be expressed with individual field validators. .refine() lets you add object-level checks cleanly.

Top comments (0)