This project demonstrates how to integrate Better Auth into a NestJS application for streamlined and flexible authentication.
Special thanks to ThallesP for their work on Better Auth!
- Framework: NestJS v11
- HTTP Framework: Express v5 (integrated in NestJS)
- Database Adapter: MongoDB
Ensure the following are installed:
- Node.js (compatible with NestJS v11)
- npm (comes with Node.js)
- MongoDB (running instance required)
-
Clone the repository
-
Install dependencies
npm install
-
Create and configure the
.envfileCreate a
.envfile in the project root with:MONGODB_URI="YOUR_MONGODB_CONNECTION_STRING" GITHUB_CLIENT_ID="YOUR_GITHUB_APPLICATION_CLIENT_ID" GITHUB_CLIENT_SECRET="YOUR_GITHUB_APPLICATION_CLIENT_SECRET" GOOGLE_CLIENT_ID="YOUR_GOOGLE_APPLICATION_CLIENT_ID" GOOGLE_CLIENT_SECRET="YOUR_GOOGLE_APPLICATION_CLIENT_SECRET" TRUSTED_ORIGINS="http://localhost:3001"
Replace placeholders with your actual credentials and frontend origin.
-
Development Mode
npm run start:dev
-
Production Mode
npm run start
The core Better Auth config is in src/auth/auth.module.ts:
static forRoot(options: AuthModuleOptions = {}) {
const auth = betterAuth({
trustedOrigins,
database: mongodbAdapter(db),
emailAndPassword: {
enabled: true,
},
session: {
freshAge: 10,
modelName: 'sessions',
},
user: {
modelName: 'users',
additionalFields: {
role: {
type: 'string',
defaultValue: 'user',
},
},
},
account: {
modelName: 'accounts',
},
verification: {
modelName: 'verifications',
},
socialProviders: {
github: {
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
},
google: {
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
},
},
});
// ...
}Supported Providers:
- GitHub
- Email/Password
Add more providers via better-auth/providers/*.
See app.controller.ts for usage of the AuthGuard:
@Controller()
@UseGuards(AuthGuard)
export class AppController {
constructor(private readonly appService: AppService) {}
@Get('/cats')
getCats(@Session() session: UserSession, @UserId() userId: string, @Body() body: any) {
console.log({ session, userId, body });
return { message: this.appService.getCat() };
}
@Post('/cats')
@Public()
sayHello(@Session() session: UserSession, @UserId() userId: string, @Body() body: any) {
console.log({ session, userId, body });
return { message: this.appService.getCat() };
}
}Set in src/main.ts:
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule, {
bodyParser: false,
});
const trustedOrigins = process.env.TRUSTED_ORIGINS?.split(',') || [];
app.enableCors({
origin: trustedOrigins,
credentials: true,
});
app.setGlobalPrefix('api', { exclude: ['/api/auth/{*path}'] });
await app.listen(process.env.PORT ?? 3000);
}Use @better-auth/react to connect your frontend:
import { createAuthClient } from 'better-auth/react';
import { inferAdditionalFields } from 'better-auth/client/plugins';
export const { signIn, signUp, signOut, useSession } = createAuthClient({
baseURL: "http://localhost:3000",
plugins: [
inferAdditionalFields({
user: {
surname: { type: 'string' },
role: { type: 'string', nullable: true },
},
}),
],
});Do not include
/apiin the baseURL.
const { error, data } = await signIn.email({
email,
password,
rememberMe,
callbackURL: "http://localhost:3001/dashboard",
});Option 1: Check cookies
import { getSessionCookie } from 'better-auth/cookies';
export async function middleware(request: NextRequest) {
const cookies = getSessionCookie(request);
if (!cookies) {
return NextResponse.redirect(new URL('/sign-in', request.url));
}
return NextResponse.next();
}Option 2: Check session with API
export async function middleware(request: NextRequest) {
const res = await fetch("http://localhost:3000/api/auth/get-session", {
headers: {
cookie: request.headers.get('cookie') || '',
},
});
const session = await res.json();
if (!session) {
return NextResponse.redirect(new URL('/sign-in', request.url));
}
return NextResponse.next();
}Require Session
export default async function Page() {
const cookie = headers().get('cookie');
const res = await fetch("http://localhost:3000/api/auth/get-session", {
headers: { cookie: cookie || '' },
});
const session = await res.json();
if (!session) {
return redirect('/sign-in');
}
return (
<div>
<h1>Protected Page</h1>
</div>
);
}Fetch Data with Session
export default async function Page() {
const res = await fetch("http://localhost:3000/api/auth/foo", {
headers: await headers(),
cache: 'no-store',
});
const data = await res.json();
return (
<div>
<h1>Protected Page</h1>
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
);
}'use client';
import { useSession } from '@/lib/auth-client';
import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
export default function Page() {
const { data: session } = useSession();
const [data, setData] = useState(null);
const router = useRouter();
useEffect(() => {
if (!session) {
router.push('/sign-in');
}
}, [session, router]);
useEffect(() => {
async function fetchData() {
const res = await fetch('http://localhost:3000/api/auth/foo', {
credentials: 'include',
});
const result = await res.json();
setData(result);
}
if (session) {
fetchData();
}
}, [session]);
return (
<div>
<h1>Protected Page</h1>
{data && <pre>{JSON.stringify(data, null, 2)}</pre>}
</div>
);
}- Use
.envvariables for all sensitive data. - Ensure CORS is properly configured.
- Add the correct callback URLs in your OAuth provider settings.
- Donβt forget to include credentials when making authenticated fetch requests.