Python — FastAPI Development
Email Verification Flow with FastAPI and Expo
Direct answer
Email verification creates the user in an unverified state at signup, emails a single-use token as a deep link, and flips the account to verified when the link is opened. Do not block login on verification — let users in, but gate sensitive actions (posting, payments, sharing) behind a dependency that checks the verified flag, so an unverified account cannot do harm while still being able to resend the link. Store the token hashed and expire it in about 24 hours.
Email verification exists to prove a user controls the address they signed up with — it stops typo'd emails, throwaway spam accounts, and impersonation. The design choice that trips teams up is how hard to gate: block login entirely and you frustrate real users; gate nothing and verification is theatre. Here is the balance I use, wired for Expo deep links.
Key facts, with sources
- In the JetBrains Python Developers Survey 2024, which collected responses from more than 30,000 Python developers, FastAPI usage jumped from 29% to 38%, overtaking Django (35%) and Flask (34%) as the most-used Python web framework. (JetBrains Python Developers Survey 2024)
- The 2025 Stack Overflow Developer Survey shows FastAPI at 14.8% of respondents doing extensive work with it, edging out Flask at 14.4% and Django at 12.6%. (Stack Overflow Developer Survey 2025)
- FastAPI's official documentation cites independent TechEmpower benchmarks showing FastAPI applications running under Uvicorn as one of the fastest Python frameworks available, ranked only below Starlette and Uvicorn themselves. (FastAPI official documentation)
- FastAPI surpassed Flask in GitHub stars in December 2025, reaching roughly 88,000 stars compared to Flask's 68,400. (DZone)
- Industry analysis of FastAPI's 2025 growth reports about 40% year-over-year growth in job mentions and production adoption at companies including Uber, Netflix, and Microsoft. (byteiota)
Create unverified, then send a hashed token
At signup, create the user with is_verified set to false and issue a verification token — stored hashed, exactly like a reset token. Email it as a deep link. The user exists and can log in; they just carry an unverified flag until they confirm.
@router.post("/auth/signup")
def signup(body: SignupIn, db: Session = Depends(get_db)):
if db.query(User).filter_by(email=body.email).first():
raise HTTPException(409, "email already registered")
user = User(email=body.email, password_hash=hash_password(body.password),
is_verified=False)
db.add(user); db.flush()
raw = secrets.token_urlsafe(32)
db.add(EmailVerification(
user_id=user.id, token_hash=_hash(raw),
expires_at=datetime.now(timezone.utc) + timedelta(hours=24),
))
db.commit()
send_verification_email(user.email, raw)
return {"message": "Check your email to verify your account."}Confirm the address, then delete the token
The verify endpoint validates the token, flips is_verified to true, and deletes the verification row so it cannot be reused. Because it is a GET opened from an email, keep it idempotent-friendly: a second click on an already-verified account should land somewhere sensible rather than throwing a scary error.
@router.get("/auth/verify")
def verify_email(token: str, db: Session = Depends(get_db)):
row = db.query(EmailVerification).filter_by(token_hash=_hash(token)).first()
if row is None or row.expires_at < datetime.now(timezone.utc):
raise HTTPException(400, "invalid or expired verification link")
user = db.query(User).get(row.user_id)
user.is_verified = True
db.delete(row) # one-time use
db.commit()
return {"message": "Email verified."}Gate actions, not login
Let unverified users log in and see the app — but wrap sensitive endpoints in a dependency that requires verification. This is the humane balance: real users are not locked out over a slow email, but an unverified account cannot post, pay, or invite until it proves the address. Pair it with a resend endpoint so users who lost the email are not stuck.
def require_verified(user = Depends(get_current_user)):
if not user.is_verified:
raise HTTPException(403, "please verify your email to continue")
return user
# Ordinary routes: any logged-in user.
@router.get("/feed")
def feed(user = Depends(get_current_user)): ...
# Sensitive routes: verified only.
@router.post("/posts")
def create_post(body: PostIn, user = Depends(require_verified)): ...When to hire senior help
Bring in senior help when your API needs to handle real concurrency, when you are designing service boundaries and auth for the first time, or when an existing FastAPI codebase mixes sync and async code and latency is degrading. An experienced engineer can usually diagnose event-loop blocking and connection-pool misconfiguration in days, which is far cheaper than re-architecting after launch. If your stack includes React Native + Python + AI, a senior engineer who owns the full product beats coordinating multiple juniors.
Bottom line
Dhairya Senjaliya ships Python — FastAPI Development projects worldwide — book a scoping call to discuss your specific situation.
Common pitfalls to avoid
- ✕Calling blocking libraries (classic SQLAlchemy sessions, requests, heavy file I/O) inside async def endpoints, which stalls the event loop and erases FastAPI's concurrency advantage
- ✕Deploying a single Uvicorn process with no process manager or worker scaling, leaving most CPU cores idle under production load
- ✕Treating the auto-generated OpenAPI docs as a versioning strategy, then breaking mobile and partner clients when response schemas change
- ✕Running large payloads through deeply nested Pydantic models on every request and response, adding serialization latency that shows up only at scale
Frequently asked questions
Should I block login until the email is verified?
Usually no. Blocking login over a delayed or lost email frustrates real users. Let them in and gate only sensitive actions behind a verified-only dependency, with an easy resend option. Block login only if your compliance or abuse model genuinely requires it.
How is this different from password reset?
The token mechanics are nearly identical — hashed, single-use, time-limited. The difference is intent: verification proves control of an address once at signup, while reset re-authenticates an existing user who lost their password. You can share the token-storage code between them.
What if the verification link is opened twice?
The first click verifies and deletes the token; the second finds no token. Handle that gracefully — if the account is already verified, redirect to a friendly 'already verified' screen rather than showing an error.
Is FastAPI mature enough for production?
Yes. It was the most-used Python web framework in the JetBrains 2024 survey at 38%, and companies including Uber, Netflix, and Microsoft run it in production. The ecosystem for auth, ORMs, and testing is now well established.
How much faster is FastAPI than Flask or Django really?
Independent TechEmpower benchmarks place FastAPI among the fastest Python frameworks, and published comparisons show several times Flask's throughput on I/O-bound endpoints. For CPU-bound work or database-bottlenecked apps, the framework choice matters far less than query and infrastructure design.
Should we pick FastAPI or Django for a new SaaS backend?
FastAPI suits API-first products, microservices, and ML model serving because of async support and automatic OpenAPI docs. Django ships batteries included (admin, ORM, auth) and is often faster to launch a conventional CRUD product. Many teams run both, per the JetBrains finding that a third of Django developers also use Flask or FastAPI.
Bottom line: Dhairya Senjaliya ships Python — FastAPI Development projects worldwide. Book a scoping call at https://dhairyasenjaliya.com/#book-call.