DEV Community

ryanlee
ryanlee

Posted on

Type Signup Email Rules Once in React and Node

If your React form and Node API disagree on email rules, signup gets weird fast. A tiny shared contract keeps validation, UX, and testing aligned.

I keep seeing the same bug pattern on product teams: the React form accepts an address, the Node API rejects it, and support gets a screenshot with "but the button said success". It sounds small, but it slows launches more than people expect.

When the feature includes social onboarding, campaign signups, or a temp mail for facebook test path, the mismatch gets even noisier. One side trims spaces, the other side lowercases inconsistently, and a third bit of code quietly blocks a domain. Suddenly you are debugging user trust, not just a field.

Why React and Node drift on email rules

This drift usually happens becuase teams split the work in a reasonable way:

  • frontend wants fast feedback
  • backend wants stricter guarantees
  • product wants fewer blocked signups

All three goals are valid, but they create duplicate rule sets. I have seen this happen with plain regex checks, hand-written if blocks, and even decent schema libs when the browser and server use slightly different versions.

The fix is not a giant auth rewrite. Its a small contract that defines:

  • normalization
  • validation
  • allowed test scenarios
  • error messages safe for UI

That contract also makes related work easier, like email API checks under load and request tracing in signup APIs. Once the rules live in one place, the rest of the pipeline gets calmer.

A small shared contract that fixes most of it

For JavaScript and TypeScript teams, I like using one schema module that both React and Node import. Zod is fine, Valibot is fine too. The point is not the library, its that both layers read the same rule.

import { z } from "zod";

export const signupEmailSchema = z
  .string()
  .trim()
  .toLowerCase()
  .email("Enter a valid email address")
  .max(120, "Email is too long");

export function normalizeSignupEmail(input: string) {
  return signupEmailSchema.parse(input);
}
Enter fullscreen mode Exit fullscreen mode

This is boring code, which is why it works so well. The browser uses it before submit. The API uses the same function before account creation. If you need extra domain logic, add it beside the schema instead of scattering it around the app.

One helpful rule is separating validation from policy:

  • validation answers "is this shaped like an email?"
  • policy answers "do we allow this address for this flow?"

That split matters when you support QA paths like get temporary email checks. A test inbox may be valid syntactically, but maybe it should only pass in staging or in a flagged test project. Thats a policy decision, not a regex decision.

How I wire the React form and Node endpoint

On the React side, validate early but keep the message plain. Users dont care that your parser has 12 branches. They care that the field tells them what to fix.

const onSubmit = async (values: { email: string }) => {
  const email = normalizeSignupEmail(values.email);
  await api.post("/signup", { email });
};
Enter fullscreen mode Exit fullscreen mode

On the Node side, do the exact same normalization again. Yes, again. Never trust that a client shipped the latest bundle, and never assume a mobile webview is behaving nicely. Double validation is cheap, and it realy pays off.

app.post("/signup", async (req, res) => {
  const email = normalizeSignupEmail(req.body.email);

  const decision = evaluateSignupEmailPolicy({
    email,
    environment: process.env.NODE_ENV,
    source: req.body.source
  });

  if (!decision.allowed) {
    return res.status(422).json({ error: decision.message });
  }

  await createPendingUser({ email });
  return res.status(201).json({ ok: true });
});
Enter fullscreen mode Exit fullscreen mode

The product win here is subtle but important: analytics, rate limits, and verification mail flows now key off one normalized value. You stop getting duplicate rows for Name@Example.com and name@example.com, which sounds tiny until it breaks an experiment readout.

Where temporary inbox testing actually helps

I do not think temporary inboxes should drive production policy by default, but they are useful in development and QA. They help when you need to verify:

  • welcome email timing
  • OTP or verification copy
  • retry behavior
  • account cleanup after abandoned signup

If your team runs those checks often, document them as a test scenario instead of a hidden habit. For example, say "QA may use a temp mailid in staging for verification flow checks" and keep that rule out of the user-facing copy. This keeps engineers honest and avoids weird customer support scripts later on.

One thing I would not do: block every disposable domain in the frontend. That usually creates false confidence. If you need restrictions, enforce them in the API with logs and a reason code. Frontend-only blocking looks neat in demos, but in real systems it ages badly.

Quick Q&A before you ship

Should I share the exact same package between frontend and backend?

If your repo layout allows it, yes. In a monorepo this is usualy the cleanest option. In separate repos, publish a tiny internal package or generate from one schema source.

Do I still need backend validation if React already checks?

Yes. Always. Browsers, bots, stale clients, and partner integrations dont care about your nice form logic.

What about Facebook or other social signup flows?

Treat them as another source, not a special exception machine. If a temp mail for facebook scenario exists for QA, gate it behind environment or account-level policy so it does not leak into normal production behavior.

What should I measure after this change?

Track signup rejection rate, duplicate email records, and verification completion time. If those numbers move in the right direction, your contract is doing real work, not just making the codebase look tidy.

Small contracts are underrated. When React and Node agree on email behavior, teams ship faster, support has fewer strange tickets, and future auth work gets a lot less messy.

Top comments (0)