DEV Community

Daanish2003
Daanish2003

Posted on

Email verification using better_auth, nextjs and resend

In this blog, we'll dive into setting up email verification in a modern web application. By leveraging a robust stack of tools and frameworks, you'll create a seamless and secure email verification system integrated into your full-stack application.

By the end of this guide, you'll have a functional system that validates users via email before granting access to their accounts.

Tech Stack

Here’s what we’ll use:

  • Better_Auth v1: A lightweight, extensible TypeScript authentication library.
  • Next.js: A React framework for building server-rendered applications.
  • Prisma: A modern ORM for efficient database interaction.
  • Shadcn: A utility-first component library for rapid UI development.
  • TailwindCSS: A popular utility-first CSS framework.

Prerequisites

Before proceeding, ensure you have the following ready:

  1. Node.js (LTS version) installed.
  2. A package manager like npm, yarn, or pnpm (we'll use pnpm in this guide).
  3. A PostgreSQL database instance (local or hosted, such as Supabase or PlanetScale).
    • If you're working locally, Docker is a great way to set this up.
  4. Familiarity with TypeScript, Next.js, and Prisma.

Note:

This blog is the continuation of Email and Password Auth If you want to follow along please first visit: Email and Password Auth using Better_Auth or you could clone the branch of the repository from the command below to follow along.

Clone Repo:

git clone -b feat-email-password-auth https://github.com/Daanish2003/better_auth_nextjs.git
Enter fullscreen mode Exit fullscreen mode

Install Dependencies:

pnpm install
Enter fullscreen mode Exit fullscreen mode

Run the docker if you have docker locally:

docker compose up -d
Enter fullscreen mode Exit fullscreen mode

Step 1: Setting up Resend service

Resend is a service for sending transactional emails like password resets and email verifications.

Install the Resend SDK

Add the Resend package to your project:

pnpm add resend
Enter fullscreen mode Exit fullscreen mode

Get Your Resend API Key

  1. Visit Resend.
  2. Log in or create an account.
  3. Navigate to API Keys and create a new key.
  4. Add the API key to your .env file:
RESEND_API_KEY=your-api-key-here
Enter fullscreen mode Exit fullscreen mode

Create a Helper for Resend

Organize your code by creating a reusable helper for Resend.

  1. Create a folder src/helpers/email/.
  2. Add a file named resend.ts:
import { Resend } from "resend";

export const resend = new Resend(process.env.RESEND_API_KEY);
Enter fullscreen mode Exit fullscreen mode

Step 2: Integrate Email verification with Better_Auth

Better_Auth provides native support for email verification.

Update the auth.ts File

Open src/lib/auth.ts and configure email verification:

// auth.ts
import prisma from "@/db";
import { resend } from "@/helpers/email/resend";
import { betterAuth } from "better-auth";
import { prismaAdapter } from "better-auth/adapters/prisma";

export const auth = betterAuth({
  appName: "better_auth_nextjs",
  database: prismaAdapter(prisma, {
    provider: "postgresql",
  }),
  emailAndPassword: {
    enabled: true,
    autoSignIn: true,
    minPasswordLength: 8,
    maxPasswordLength: 20,
    requireEmailVerification: true, //It does not allow user to login without email verification [!code highlight]
  },
emailVerification: {
    sendOnSignUp: true, // Automatically sends a verification email at signup
    autoSignInAfterVerification: true, // Automatically signIn the user after verification
    sendVerificationEmail: async ({ user, url }) => {
      await resend.emails.send({
        from: "Acme <onboarding@resend.dev>", // You could add your custom domain
        to: user.email, // email of the user to want to end
        subject: "Email Verification", // Main subject of the email
        html: `Click the link to verify your email: ${url}`, // Content of the email
        // you could also use "React:" option for sending the email template and there content to user
      });
    },
  },
// ignore this if your not adding OAuth
  socialProviders: {
    google: {
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
      redirectURI: process.env.BETTER_AUTH_URL + "/api/auth/callback/google",
    },
    github: {
      clientId: process.env.GITHUB_CLIENT_ID!,
      clientSecret: process.env.GITHUB_CLIENT_SECRET!,
      redirectURI: process.env.BETTER_AUTH_URL + "/api/auth/callback/github",
    },
  },
});

Enter fullscreen mode Exit fullscreen mode

Explanation:

  • sendVerificationEmail: This function is triggered when email verification starts. It accepts a data object with the following properties:
    • user: The user object containing the email address.
    • url: The verification URL the user must click to verify their email.

Step 3: Update UI Components

To handle the email verification workflow, we’ll update the sign-in and sign-up components.

Update the SignIn Component

Navigate to src/components/auth/sign-in.tsx and use this code:

// components/auth/sign-in.tsx
"use client"
import React from 'react'
import CardWrapper from '../card-wrapper'
import FormError from '../form-error'
import { FormSuccess } from '../form-success'
import { FcGoogle } from 'react-icons/fc'
import SocialButton from './social-button'
import { FaGithub } from 'react-icons/fa'
import { useAuthState } from '@/hooks/useAuthState'
import LoginSchema from '@/helpers/zod/login-schema'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '../ui/form'
import { Input } from '../ui/input'
import { Button } from '../ui/button'
import { signIn } from '@/lib/auth-client'
import { useRouter } from 'next/navigation'

const SignIn = () => {
    const router = useRouter()
    const { error, success, loading, setSuccess, setError, setLoading, resetState } = useAuthState();

    const form = useForm<z.infer<typeof LoginSchema>>({
        resolver: zodResolver(LoginSchema),
        defaultValues: {
            email: '',
            password: '',
        }
    })

    const onSubmit = async (values: z.infer<typeof LoginSchema>) => {
        try {
          await signIn.email({
            email: values.email,
            password: values.password
          }, {
            onResponse: () => {
              setLoading(false)
            },
            onRequest: () => {
              resetState()
              setLoading(true)
            },
            onSuccess: () => {
                setSuccess("LoggedIn successfully")
                router.replace('/')
            },
// changes were made on onError option
            onError: (ctx) => {
                /* Whenever user tried to signin but email is not verified it catches the error and display the error */
                if(ctx.error.status === 403) {
                    setError("Please verify your email address")
                }
                /* handles other error */
              setError(ctx.error.message);
            },
          });
        } catch (error) {
          console.log(error)
          setError("Something went wrong")
        }
      }

    return (
        <CardWrapper
            cardTitle='Sign In'
            cardDescription='Enter your email below to login to your account'
            cardFooterDescription="Don't have an account?"
            cardFooterLink='/signup'
            cardFooterLinkTitle='Sign up'
        >
            <Form {...form}>
                <form className='space-y-4' onSubmit={form.handleSubmit(onSubmit)}>
                    <FormField
                        control={form.control}
                        name="email"
                        render={({ field }) => (
                            <FormItem>
                                <FormLabel>Email</FormLabel>
                                <FormControl>
                                    <Input
                                        disabled={loading}
                                        type="email"
                                        placeholder='example@gmail.com'
                                        {...field}
                                    />
                                </FormControl>
                                <FormMessage />
                            </FormItem>
                        )}
                    />
                    <FormField
                        control={form.control}
                        name="password"
                        render={({ field }) => (
                            <FormItem>
                                <FormLabel>Password</FormLabel>
                                <FormControl>
                                    <Input
                                        disabled={loading}
                                        type="password"
                                        placeholder='********'
                                        {...field}
                                    />
                                </FormControl>
                                <FormMessage />
                            </FormItem>

                        )}
                    />
                    <FormError message={error} />
                    <FormSuccess message={success} />
                     // ignore this if your not adding OAuth into your app
                    <Button disabled={loading} type="submit" className='w-full'>Login</Button>
                    <div className='flex gap-x-2'>
                        <SocialButton provider="google" icon={<FcGoogle />} label="Sign in with Google" />
                        <SocialButton provider="github" icon={<FaGithub />} label="Sign in with GitHub" />
                    </div>
                </form>
            </Form>
        </CardWrapper>
    )
}

export default SignIn
Enter fullscreen mode Exit fullscreen mode

Update the SignUp Component

Similarly, update src/components/auth/sign-up.tsx:

// components/auth/sign-up.tsx
"use client"
import React from 'react'
import CardWrapper from '../card-wrapper'
import FormError from '../form-error'
import { FormSuccess } from '../form-success'
import { FcGoogle } from 'react-icons/fc'
import SocialButton from './social-button'
import { FaGithub } from 'react-icons/fa'
import { useAuthState } from '@/hooks/useAuthState'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '../ui/form'
import { Input } from '../ui/input'
import { Button } from '../ui/button'
import { SignupSchema } from '@/helpers/zod/signup-schema'
import { signUp } from '@/lib/auth-client'

const SignUp = () => {
    const { error, success, loading, setLoading, setError, setSuccess, resetState } = useAuthState();

    const form = useForm<z.infer<typeof SignupSchema>>({
        resolver: zodResolver(SignupSchema),
        defaultValues: {
            name: '',
            email: '',
            password: '',
        }
    })

    const onSubmit = async (values: z.infer<typeof SignupSchema>) => {
        try {
            await signUp.email({
                name: values.name,
                email: values.email,
                password: values.password,
                callbackURL:'/' // redirect the user after email is verified
            }, {
                onResponse: () => {
                    setLoading(false)
                },
                onRequest: () => {
                    resetState()
                    setLoading(true)
                },
                onSuccess: () => {
                    // setSuccess("User has been created")
                    // router.replace('/')
                    setSuccess("Verification link has been sent to your mail")
                },
                onError: (ctx) => {
                    setError(ctx.error.message);
                },
            });
        } catch (error) {
            console.error(error)
            setError("Something went wrong")
        }

    }

    return (
        <CardWrapper
        cardTitle='SignUp'
        cardDescription='Create an new account'
        cardFooterLink='/login'
        cardFooterDescription='Already have an account?'
        cardFooterLinkTitle='Login'
        >
            <Form {...form}>
                <form className='space-y-4' onSubmit={form.handleSubmit(onSubmit)}>
                <FormField
                        control={form.control}
                        name="name"
                        render={({ field }) => (
                            <FormItem>
                                <FormLabel>Name</FormLabel>
                                <FormControl>
                                    <Input
                                        disabled={loading}
                                        type="text"
                                        placeholder='john'
                                        {...field} />
                                </FormControl>
                                <FormMessage />
                            </FormItem>
                        )}
                    />
                    <FormField
                        control={form.control}
                        name="email"
                        render={({ field }) => (
                            <FormItem>
                                <FormLabel>Email</FormLabel>
                                <FormControl>
                                    <Input
                                        disabled={loading}
                                        type="email"
                                        placeholder='example@gmail.com'
                                        {...field}
                                    />
                                </FormControl>
                                <FormMessage />
                            </FormItem>
                        )}
                    />
                    <FormField
                        control={form.control}
                        name="password"
                        render={({ field }) => (
                            <FormItem>
                                <FormLabel>Password</FormLabel>
                                <FormControl>
                                    <Input
                                        disabled={loading}
                                        type="password"
                                        placeholder='********'
                                        {...field}
                                    />
                                </FormControl>
                                <FormMessage />
                            </FormItem>

                        )}
                    />
                    <FormError message={error} />
                    <FormSuccess message={success} />
                     //ignore this if your not adding Oauth
                    <Button disabled={loading} type="submit" className='w-full'>Submit</Button>
                    <div className='flex gap-x-2'>
                        <SocialButton provider="google" icon={<FcGoogle />} label="Sign in with Google" />
                        <SocialButton provider="github" icon={<FaGithub />} label="Sign in with GitHub" />
                    </div>
                </form>
            </Form>
        </CardWrapper>
    )
}

export default SignUp
Enter fullscreen mode Exit fullscreen mode

Step 4: Run Your Application

Start your development server:

pnpm dev

Enter fullscreen mode Exit fullscreen mode
  • Access your application at http://localhost:3000/signup to test sign-up.
  • Check your inbox for the verification email.
  • Log in at http://localhost:3000/signin once verified.

Image description

Final Thoughts

Congratulations! You have successfully integrated Email verification into your application, providing users with a seamless and secure authentication experience. 🎉

If you enjoyed this blog, please consider following and supporting me! Your encouragement motivates me to create more helpful content for you. 😊

Reference Links:

Email And Password with Better_Auth: https://dev.to/daanish2003/email-and-password-auth-using-betterauth-nextjs-prisma-shadcn-and-tailwindcss-hgc

OAuth Blog: https://dev.to/daanish2003/oauth-using-betterauth-nextjs-prisma-shadcn-and-tailwindcss-45bp

Better_Auth Docs: https://www.better-auth.com/

pnpm Docs: https://pnpm.io/

Docker Docs: https://docs.docker.com/

Prisma Docs: https://www.prisma.io/docs/getting-started

Shadcn Docs: https://ui.shadcn.com/

Next.js Docs: https://nextjs.org/

Tailwindcss Docs: https://tailwindcss.com/

Github repository: https://github.com/Daanish2003/better_auth_nextjs

Top comments (0)