In this blog post, we'll explore how to set up email and password authentication in a modern web application using a powerful stack that ensures efficiency, scalability, and developer productivity. By the end, you'll have a working email and password system integrated into a full-stack application.
Tech Stack:
- Better_Auth v1: A lightweight and extensible authentication library for TypeScript.
- Next.js: A React framework for building server-rendered web applications.
- Prisma: A modern ORM for database management.
- Shadcn: A utility-first component library.
- TailwindCSS: A popular utility-first CSS framework.
Prerequisites:
Before we begin, make sure you have the following tools installed:
- Node.js (LTS version)
- npm, yarn, or pnpm (package managers)
- A PostgreSQL database instance (local or hosted, such as Supabase or PlanetScale). I'll be using Docker for local development
- Basic knowledge of TypeScript, Next.js, and Prisma.
Step 1: Setting up your Next.js project
Create a New Project Directory:
Begin by creating a folder for your project and navigating into it via the terminal. Use the following commands:
mkdir better_auth_nextjs_oauth
cd better_auth_nextjs_oauth
Install Next.js in the Directory:
Initialize a new Next.js project within the folder using the command below. You can use your preferred package manager (e.g., npm, yarn, or pnpm). For this guide, we'll use pnpm.
pnpx create-next-app@latest .
💡Tip: If you don't have pnpm installed, visit pnpm.io and follow the instructions for your operating system.
Configure the Project Options:
Follow the prompts displayed in the terminal to configure your project. You will be asked to make selections such as enabling TypeScript, which is recommended for better type safety and development experience.
Use the arrow keys to navigate through the options and choose according to your preferences.
Step 2: Installing Dependencies
Install the necessary packages for the tech stack by running the following commands:
pnpm add better-auth@latest
pnpm add -D prisma
pnpm add @prisma/client
pnpm install react-icons --save
pnpm add oslo
pnpm add zod
To install shadcn-ui, a utility-first component library, run the following command:
pnpm dlx shadcn@latest init
This will set up shadcn-ui in your project, allowing you to start using its components right away.
When prompted, choose from the available options to customize your shadcn-ui setup. Follow the instructions in the terminal to select the components and configurations that best suit your project needs.
To add reusable components from shadcn-ui, you can use the following command:
pnpx shadcn@latest add button card form input label
Step 3: Setting Up the Database (Local or Cloud)
Option 1: Local Development with Docker:
💡Tip: If you're using a cloud-based database provider like Neon or PlanetScale, you can skip this step and proceed to configure your database connection directly through their provided URLs.
Create a docker-compose.yml
File
To set up a PostgreSQL database using Docker, create a docker-compose.yml
file in your project’s root directory. This file will define the configuration for your database container.
Here's an example configuration for the docker-compose.yml
file:
# docker-compose.yml
version: '3.9'
services:
db:
container_name: BETTER_AUTH_NEXTJS_OAUTH
image: postgres:17-alpine
restart: always
ports:
- 5432:5432
volumes:
- postgres-data:/var/lib/postgresql/data
environment:
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_DB=OAUTH
volumes:
postgres-data:
This configuration sets up a PostgreSQL container with a persistent volume and environment variables for your database credentials.
Set Up Environment Variables for the Database
Create a .env
file in the root of your project and define the necessary environment variables for your database connection. Here's an example configuration:
//.env
POSTGRES_PASSWORD="your_password_here"
POSTGRES_USER="your_username_here"
DATABASE_URL="postgresql://your_username_here:your_password_here@localhost:5432/mydb?schema=public"
- Replace
your_password_here
andyour_username_here
with your own PostgreSQL credentials. -
DATABASE_URL
should point to your local database or a cloud database if you are using a provider.
Run the Docker Container
Once you’ve configured the docker-compose.yml
file and set up the environment variables, you can start the Docker container by running the following command:
docker compose up -d
This command will start the PostgreSQL container in detached mode (-d
), ensuring your database is up and running in the background. You can now proceed with the rest of the setup in your Next.js application.
If you ever need to stop the container, you can use:
docker compose down
Option 2: Using a Hosted Database
If you're using a cloud database provider (such as Neon, PlanetScale, or others), you can skip the local Docker setup. Instead, simply copy the provided Database URL from your cloud provider and add it to your .env
file.
For example:
# .env
DATABASE_URL="postgresql://username:password@host:port/database_name?sslmode=require"
Ensure that the DATABASE_URL
contains the correct connection details such as your:
username
password
-
host
(database server URL) -
port
(usually5432
for PostgreSQL) database_name
Once you've added the correct URL, your application will be able to connect to the hosted database seamlessly.
Step 4: Setting Up Prisma
Initialize Prisma CLI
This will create a new prisma
folder in your project, containing the schema.prisma
file and configuration for Prisma.
pnpm dlx prisma init
This will create a new prisma
folder in your project, containing the schema.prisma
file and configuration for Prisma.
Configure the Database Connection:
Open the prisma/schema.prisma
file and ensure that the DATABASE_URL
points to your PostgreSQL database. It should look something like this:
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
Prisma will automatically read the DATABASE_URL
from your .env
file.
Create a Database Folder
Inside the src
directory of your project, create a folder named db
. This will house all database-related files.
Create a Database Client File
Inside the db
folder, create a file named index.ts
. Add the following code to configure and export a singleton instance of the Prisma Client:
// src/db/index.ts
import { PrismaClient } from '@prisma/client'
const prismaClientSingleton = () => {
return new PrismaClient()
}
declare const globalThis: {
prismaGlobal: ReturnType<typeof prismaClientSingleton>;
} & typeof global;
const prisma = globalThis.prismaGlobal ?? prismaClientSingleton()
export default prisma
if (process.env.NODE_ENV !== 'production') globalThis.prismaGlobal = prisma
Create Schema for auth
In this step, you'll define a database schema to manage authentication-related data, such as users, sessions, accounts, and verification tokens.
- Open the
prisma/schema.prisma
file in your project. - Replace its contents with the following schema:
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id String @id @default(cuid())
name String
email String
emailVerified Boolean @default(false)
image String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
twoFactorEnabled Boolean @default(false)
Session Session[]
Account Account[]
@@unique([email])
@@map("user")
}
model Session {
id String @id @default(cuid())
expiresAt DateTime
token String @unique
ipAddress String?
userAgent String?
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("session")
}
model Account {
id String @id @default(cuid())
accountId String
providerId String
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
accessToken String?
refreshToken String?
idToken String?
expiresAt DateTime?
password String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
accessTokenExpiresAt DateTime?
refreshTokenExpiresAt DateTime?
scope String?
@@map("account")
}
model Verification {
id String @id @default(cuid())
identifier String
value String
expiresAt DateTime
createdAt DateTime? @default(now())
updatedAt DateTime? @updatedAt
@@map("verification")
}
Explanation of the Schema
-
User: Contains basic user information and manages relationships to
Account
andSession
models. - Session: Tracks user sessions, including tokens, IP addresses, and user agents.
- Account: Manages linked OAuth accounts for third-party login providers such as Google or GitHub.
- Verification: Handles email or token-based verification (e.g., passwordless login or two-factor authentication).
Generate and Migrate the Prisma Schema
After defining your models in the schema.prisma
file, you need to generate the Prisma client and apply the database migrations.
-
Generate Prisma Client
First, generate the Prisma Client, which provides an API to interact with your database:
pnpx prisma generate
This will generate the necessary client files for accessing the database from your application.
- Run Migrations
Apply the migrations to your database by running the following command:
pnpx prisma migrate dev --name auth
This will apply the migration to your database and generate the required tables (e.g., for User
, Session
, Account
, etc.).
Once this setup is complete, Prisma will be ready to use for querying your database in your application.
Step-4: Integrate Better_Auth
Setting Up Environment Variables:
Add the following configuration to your .env
file to provide the necessary environment variables for authentication:
# General authentication settings
BETTER_AUTH_SECRET="your-secret-key" # Replace with your own secure key or generate one using "pnpm dlx auth secret"
BETTER_AUTH_URL="http://localhost:3000" # Base URL of your app during local development
NEXT_PUBLIC_APP_URL="http://localhost:3000" # Public-facing base URL of your app
Explanation:
-
BETTER_AUTH_SECRET: A secret key used by Better_Auth for signing JWTs. You can generate one using
pnpm dlx auth secret
. - BETTER_AUTH_URL: The base URL for your application (used for redirecting after authentication).
- NEXT_PUBLIC_APP_URL: The public URL of your app, which is used for client-side requests.
Setting Up the Authentication Configuration:
-
Create the
auth.ts
FileInside the
lib
folder of your project, create a file namedauth.ts
. This file will contain the configuration for Better_Auth. -
Add the Following Code
Copy and paste the code below into the
auth.ts
file:
// auth.ts import prisma from '@/db'; 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, }, });
Explanation of the Code:
-
betterAuth
: Initializes the authentication library with the following parameters: -
appName
: Specifies the name of your application. In this case, it is"better_auth_nextjs"
. This name might be used in logs or display contexts. -
database
: Connects the Prisma adapter to the PostgreSQL database configured in your Prisma setup. It uses theprismaAdapter
method, which linksbetter-auth
to your Prisma client (prisma
) and ensures it works seamlessly with the PostgreSQL database defined in your.env
file. -
emailAndPassword
: Configures email-and-password authentication with the following settings:-
enabled: true
: Enables email-and-password as an authentication method. -
autoSignIn: true
: Automatically login the user in after signup. -
minPasswordLength
: Sets the minimum password length to 8 characters for added security. -
maxPasswordLength
: Sets the maximum password length to 20 characters to avoid excessively long passwords.
-
Setting Up the Authentication Client
-
Create the
auth-client.ts
FileInside the
lib
folder, create a new file namedauth-client.ts
. This file will manage client-side authentication interactions. -
Add the Following Code
Paste the code below into the newly created file:
// src/lib/auth-client.ts
import {
createAuthClient
} from "better-auth/react";
export const authClient = createAuthClient({
baseURL: process.env.NEXT_PUBLIC_APP_URL,
})
export const {
signIn,
signOut,
signUp,
useSession
} = authClient;
-
createAuthClient
:- This function initializes the client-side authentication logic using Better_Auth.
- The
baseURL
is sourced from theNEXT_PUBLIC_APP_URL
environment variable, ensuring compatibility with both local and production environments.
-
Exported Methods:
-
signIn
: Initiates the sign-in process for users. -
signOut
: Logs the user out. -
signUp
: Handles user registration. -
useSession
: Retrieves and manages the current user's authentication session.
💡Why This File is Important
The
auth-client.ts
file abstracts client-side authentication functions, making them reusable and straightforward to integrate into React components. By centralizing the authentication logic, you maintain cleaner code and enhance the maintainability of your project. -
Creating an API Route Handler for Authentication
To handle authentication API requests, you'll need to set up a route handler in your application. This will process all requests directed to the /api/auth/*
endpoint (or any custom base path you configure).
-
Create the Route Handler
In your framework's designated directory for API routes (e.g.,
/app/api/auth
for Next.js), create a catch-all route handler to manage authentication requests. -
Add the Following Code
For a Next.js application, create a file named
route.ts
inside the/app/api/auth/[...all]
directory and paste the following code:
// /app/api/auth/[...all]/route.ts
import { auth } from "@/lib/auth"; // path to your auth file
import { toNextJsHandler } from "better-auth/next-js";
export const { POST, GET } = toNextJsHandler(auth);
Explanation:
-
toNextJsHandler
:- Converts the Better_Auth configuration into route handlers compatible with Next.js.
- Automatically sets up handlers for the HTTP methods (e.g.,
POST
andGET
) required for authentication.
-
Catch-All Route (
/api/auth/*
):- Ensures that all authentication-related requests (e.g., sign-in, callbacks) are routed through this handler.
- This centralizes your authentication logic for better maintainability.
💡Why This Setup is Necessary
By creating this route handler:
- You streamline the handling of authentication-related API requests.
- The
/api/auth/*
endpoint acts as a dedicated entry point for Better_Auth to manage user sessions, social logins, and other auth operations.
Step 5: Create Sign-In Component
Create a Reusable CardWrapper
Component
To improve modularity and reusability, let's create a CardWrapper
component. This component will wrap content inside a styled card layout, making it easy to display various types of content consistently throughout the app.
Here’s the improved version of the CardWrapper
component:
// src/components/card-wrapper.tsx
import React from 'react';
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from './ui/card';
import Link from 'next/link';
interface CardWrapperType {
children: React.ReactNode;
cardTitle: string;
cardDescription: string;
cardFooterLinkTitle?: string;
cardFooterDescription?: string;
cardFooterLink?: string;
className?: string;
}
const CardWrapper = ({
children,
cardTitle,
cardDescription,
cardFooterLinkTitle = 'Learn More', // Default value
cardFooterDescription = '',
cardFooterLink,
className = '',
}: CardWrapperType) => {
return (
<Card className={`w-[400px] relative ${className}`}>
<CardHeader>
<CardTitle>{cardTitle}</CardTitle>
<CardDescription>{cardDescription}</CardDescription>
</CardHeader>
<CardContent>{children}</CardContent>
{cardFooterLink && (
<CardFooter className="flex items-center justify-center gap-x-1">
{cardFooterDescription && <span>{cardFooterDescription}</span>}
<Link href={cardFooterLink} className="underline text-blue-500 hover:text-blue-700">
{cardFooterLinkTitle}
</Link>
</CardFooter>
)}
</Card>
);
};
export default CardWrapper;
Explanation:
-
Component Structure:
-
children
: This is the content that will be rendered inside the card. -
cardTitle
: The title of the card, which will be prominently displayed at the top. -
cardDescription
: A brief description under the title that provides more context for the card. -
cardFooterLink
: This is a link for navigating to different page -
cardFooterDescription
: A brief description of footer -
cardFooterLinkTitle
: Title for footer link
-
-
Styling:
- The
Card
container has utility classes likew-full
,max-w-md
,mx-auto
,bg-white
,rounded-lg
, andshadow-md
to control the width, margin, background color, border radius, and shadow of the card. This ensures the card looks well-aligned, centered, and visually distinct. - Inside the card header, the
CardTitle
andCardDescription
have additional styling for text size, weight, and color to improve readability and contrast.
- The
-
Modularity:
- By separating the card layout into its own reusable component (
CardWrapper
), it becomes easy to reuse this structure throughout the app wherever you need a card with a title and description. This promotes consistency in the design and reduces code duplication.
- By separating the card layout into its own reusable component (
Create FormError
and FormSuccess
Components:
To improve error and success message handling in forms, let's create reusable components for displaying error and success messages.
-
FormError
Component:The `FormError` component will display error messages with an icon and a styled container to alert the user.
// app/components/form-error.tsx
import React from 'react'
import { AiFillExclamationCircle } from "react-icons/ai";
type FormErrorProps = {
message?: string
}
const FormError = ({message}: FormErrorProps) => {
if (!message) return null
return (
<div className="bg-destructive/15 p-3 rounded-md flex items-center gap-x-2 text-sm text-destructive">
<AiFillExclamationCircle className='w-4 h-4'/>
{message}
</div>
)
}
export default FormError
Explanation:
-
Props:
-
message
: A string containing the error message that will be displayed. If no message is provided, the component returnsnull
.
-
-
Styling:
-
bg-red-100
: Light red background for the error message. -
text-red-600
: Red text color for visibility and to emphasize the error. -
p-3
: Padding inside the message box. -
rounded-md
: Rounded corners for a smooth appearance. -
flex items-center gap-x-2
: A flex container with horizontal spacing between the icon and the message text. - The
AiFillExclamationCircle
icon is used to represent the error visually.
-
FormSuccess
Component:
The FormSuccess
component is similar to the FormError
, but it's used for success messages.
// app/components/form-success.tsx
import React from 'react'
import { AiFillExclamationCircle } from "react-icons/ai";
type FormErrorProps = {
message?: string
}
const FormError = ({message}: FormErrorProps) => {
if (!message) return null
return (
<div className="bg-destructive/15 p-3 rounded-md flex items-center gap-x-2 text-sm text-destructive">
<AiFillExclamationCircle className='w-4 h-4'/>
{message}
</div>
)
}
export default FormError
Explanation:
-
Props:
-
message
: A string containing the success message to be displayed. If no message is provided, the component returnsnull
.
-
-
Styling:
-
bg-green-100
: Light green background to indicate success. -
text-green-600
: Green text color to match the success color scheme. -
p-3
: Padding inside the message box for better spacing. -
rounded-md
: Rounded corners for a sleek and modern design. -
flex items-center gap-x-2
: Flexbox container with horizontal spacing between the success icon and message text. - The
AiFillCheckCircle
icon represents a successful action.
-
Create the useAuthState.ts
Hook file
Inside the src
folder, create a new folder named hooks
and inside hooks folder create a new file named useAuthState.ts
. This file contain the hooks which is responsible for handling error, success and loading state and message
import { useState } from 'react';
export const useAuthState = () => {
const [error, setError] = useState('');
const [success, setSuccess] = useState('');
const [loading, setLoading] = useState(false);
const resetState = () => {
setError('');
setSuccess('');
setLoading(false);
};
return { error, setError, success, setSuccess, loading, setLoading, resetState };
};
Explanation:
The useState
hook from React is used to manage the component's state variables. Each state variable (e.g., error
, success
, loading
) will be used to track a specific part of the authentication process.
-
error
: Stores error messages (if any) during the authentication process.- Initial Value:
''
(empty string), meaning no error by default. - Function:
setError
updates the error message.
- Initial Value:
-
success
: Stores success messages when an authentication process completes successfully.- Initial Value:
''
(empty string), meaning no success message by default. - Function:
setSuccess
updates the success message.
- Initial Value:
-
loading
: Indicates whether an authentication process (e.g., login, signup) is ongoing.- Initial Value:
false
, meaning the process is not loading by default. - Function:
setLoading
updates the loading state.
- Initial Value:
-
Purpose: Resets all three states (
error
,success
, andloading
) to their initial values.-
setError('')
: Clears the error message. -
setSuccess('')
: Clears the success message. -
setLoading(false)
: Sets the loading state tofalse
.
-
- Use Case: Typically used to clear all messages and reset the loading state when starting a new authentication flow or when leaving the authentication page.
Create zod scheme for signin
and signup
Inside the src
folder, create a new folders helpers/zod
and then create a new files named signup-schema.tsx
and login-schema.tsx
.This file is responsible for type check the input values given by the user. If user sends invalid field type it returns the error message.
// src/helpers/zod/signup-schema.ts
import { z } from "zod";
export const SignupSchema = z
.object({
name: z
.string()
.min(2, { message: "Minimum 2 characters are required" })
.max(20, { message: "Maximum of 20 characters are allowed" }),
email: z
.string()
.email({ message: "Invalid email address" })
.min(1, { message: "Email is required" }),
password: z
.string()
.min(8, { message: "Password must be at least 6 characters long" })
.max(20, { message: "Password must be at most 20 characters long" }),
})
// src/helpers/zod/login-schema.ts
import { z } from "zod";
const LoginSchema = z.object({
email: z
.string()
.email({message: "Invalid email"})
.min(1, {message: "Email is required"}),
password: z
.string()
.min(8, {message: "Password must be at least 8 characters long"})
.max(20, {message: "Password must be at most 20 characters long"}),
})
export default LoginSchema
Explanation:
This imports the z
object from the zod
library, which provides methods for defining and validating schemas.
-
name
Field -
Type: A
string
. Ensures thename
field is a string. -
Minimum Length:
min(2)
, meaning the name must have at least 2 characters.- Custom error message:
"Minimum 2 characters are required"
.
- Custom error message:
-
Maximum Length:
max(20)
, meaning the name can have at most 20 characters.- Custom error message:
"Maximum of 20 characters are allowed"
.
- Custom error message:
-
email
Field -
Type: A
string
. Ensures theemail
field is a string. -
Email Format:
.email()
, validates that the string is a properly formatted email address.- Custom error message:
"Invalid email address"
.
- Custom error message:
-
Non-Empty:
.min(1)
, ensures the email field is not empty.- Custom error message:
"Email is required"
.
- Custom error message:
-
password
Field -
Type: A
string
. Ensures thepassword
field is a string. -
Minimum Length:
min(6)
, enforces a minimum password length of 6 characters.- Custom error message:
"Password must be at least 6 characters long"
.
- Custom error message:
-
Maximum Length:
max(64)
, enforces a maximum password length of 64 characters.- Custom error message:
"Password must be at most 64 characters long"
.
- Custom error message:
Create the sign-in.tsx
file
Inside the components/auth
folder, create a new file named sign-in.tsx
. This file will contain the component responsible for rendering the sign-in UI and handling the authentication logic.
Here's how to structure your component:
// 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: (ctx) => {
setSuccess("LoggedIn successfully")
router.replace('/')
},
onError: (ctx) => {
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} />
<Button disabled={loading} type="submit" className='w-full'>Login</Button>
</form>
</Form>
</CardWrapper>
)
}
export default SignIn
Explanation:
-
State management: The component uses
useAuthState
for managing error, success, and loading states. -
Email and Password sign-in: The
onSubmit
functions handle authentication with Email and Password respectively. They use thesignIn.email
method from theauth-client.ts
file. - React Hook Form: It uses this library to handler the form check the types using zod
-
Form feedback:
FormError
andFormSuccess
components display any errors or success messages during the sign-in process. -
UI: The
Button
component is used to create sign-in buttons for GitHub and Google. The buttons are disabled while the sign-in process is in progress
This component will handle the authentication process and provide users with an interface to sign in using their Email and Password.
Create the sign-up.tsx
file
Inside the components/auth
folder, create a new file named sign-up.tsx
. This file will contain the component responsible for rendering the sign-up UI and handling the authentication logic.
Here's how to structure your component:
// 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'
import { useRouter } from 'next/navigation'
const SignUp = () => {
const router = useRouter()
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,
}, {
onResponse: () => {
setLoading(false)
},
onRequest: () => {
resetState()
setLoading(true)
},
onSuccess: () => {
setSuccess("User has been created")
router.replace('/')
},
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} />
<Button disabled={loading} type="submit" className='w-full'>Submit</Button>
</form>
</Form>
</CardWrapper>
)
}
export default SignUp
Explanation:
-
State management: The component uses
useAuthState
for managing error, success, and loading states. -
Email and Password sign-up: The
onSubmit
functions handle authentication with Email and Password respectively and creates new user in database. They use thesignIn.email
method from theauth-client.ts
file. - React Hook Form: It uses this library to handler the form check the types using zod
-
Form feedback:
FormError
andFormSuccess
components display any errors or success messages during the sign-in process. -
UI: The
Button
component is used to create sign-in buttons for GitHub and Google. The buttons are disabled while the sign-in process is in progress
This component will handle the authentication process and provide users with an interface to sign in using their Email and Password.
Create Signout
Button
Inside the src/components/auth
folder, create a new file named sign-out.tsx
.This file will contain the component responsible for rendering the sign-out user from the app.
"use client"
import React from 'react'
import { Button } from '../ui/button'
import { signOut } from '@/lib/auth-client'
import { useRouter } from 'next/navigation'
const SignOut = () => {
const router = useRouter()
return (
<Button
onClick={async() => {await signOut({
fetchOptions: {
onSuccess: () => {
router.push("/signin")
}
}
})}}
>Logout</Button>
)
}
export default SignOut
Explanation:
The SignOut
component provides a simple button for users to log out of their account. It uses an asynchronous function to handle the sign-out process and redirects users to the sign-in page upon successful logout.
Create page.tsx
and layout.tsx
for signup and signin page
Create (auth)
folder inside the src/app
folder and then create a layout.tsx
file , signup
folder and signin
folder, and then create a page.tsx
file inside the both the folders.
// src/app/(auth)/layout.tsx
import React from 'react'
const AuthLayout = ({children}: {children: React.ReactNode}) => {
return (
<div className='flex items-center justify-center w-screen h-screen'>{children}</div>
)
}
export default AuthLayout
-
Layout Component: The
AuthLayout
component wraps the entire sign-in page content (children
). This layout is useful for organizing the structure of the page and allows for consistent styling and placement of content.
// /app/(auth)/signin/page.tsx
import SignIn from '@/components/auth/sign-in'
import React from 'react'
const SignInPage = () => {
return (
<SignIn />
)
}
export default SignInPage
The SignIn
component from components/auth/sign-in
is imported and used to display the sign-in form.
// /app/(auth)/signup/page.tsx
import SignUp from '@/components/auth/sign-up'
import React from 'react'
const SignupPage = () => {
return (
<SignUp />
)
}
export default SignupPage
The SignUp
component from components/auth/sign-up
is imported and used to display the sign-in form.
// src/app/page.tsx
import SignOut from "@/components/auth/sign-out";
export default function Home() {
return (
<SignOut />
);
}
The SignOut
component from components/auth/sign-out
is imported and used to display the sign-in form.
Step 6 - Run the Application:
To start your development server and preview your application:
- Open your terminal and navigate to your project directory.
- Run the following command:
pnpm dev
This command will launch the development server and make your application accessible at http://localhost:3000
by default. You can now test and view your application in the browser.
Once the development server is running, visit the following URL in your browser to access the sign-in page:
http://localhost:3000/signin
This will load the sign-in page of your application, where you can test the OAuth authentication flow.
http://localhost:3000/signup
Congratulations! You have successfully integrated OAuth login 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:
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 (1)
The form-success.tsx will throw an error.
This fixed it:
and them change the import to