Skip to content

laakal/nestjs-better-auth-template

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

14 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

NestJS with Better Auth Example

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!


πŸ“‘ Table of Contents


🧰 Project Information

  • Framework: NestJS v11
  • HTTP Framework: Express v5 (integrated in NestJS)
  • Database Adapter: MongoDB

βœ… Prerequisites

Ensure the following are installed:

  • Node.js (compatible with NestJS v11)
  • npm (comes with Node.js)
  • MongoDB (running instance required)

βš™οΈ Project Setup

  1. Clone the repository

  2. Install dependencies

    npm install
  3. Create and configure the .env file

    Create a .env file 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.


πŸš€ Running the Application

  • Development Mode

    npm run start:dev
  • Production Mode

    npm run start

πŸ” Authentication Configuration

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:

  • Google
  • GitHub
  • Email/Password

Add more providers via better-auth/providers/*.


πŸ—„οΈ Database Migrations

If you use SQL databases (Postgres, MySQL, SQLite) instead of MongoDB, use the Better Auth CLI to create/apply the schema.

  • Step 1: Move your Better Auth config options into a standalone file (no NestJS bootstrap), for example src/auth/auth-config.ts.
    • Keep only the Better Auth configuration object. Avoid path aliases; use relative imports so the CLI can resolve modules.
  • Step 2 (Kysely/built-in adapter): Apply the schema directly:
    • npx @better-auth/cli@latest migrate --config src/auth/auth-config.ts
    • Add --yes to skip prompts.
  • Step 2 (Prisma/Drizzle): Generate the schema, then run your ORM migrations:
    • npx @better-auth/cli@latest generate --config src/auth/auth-config.ts
    • Prisma: npx prisma migrate dev -n better_auth_init
    • Drizzle: run your drizzle-kit generate/push flow to apply the generated schema.

Notes

  • Ensure your database connection env (e.g., DATABASE_URL) points to your Postgres/MySQL/SQLite instance.
  • If the CLI can’t resolve imports, switch to relative paths in your config file (per docs).

Docs: https://www.better-auth.com/docs/concepts/cli


🌐 Cross-Domain Session Configuration

The application supports cross-domain cookies for production environments, allowing session sharing across subdomains. This is configured in src/auth/auth.module.ts:

...(isProd
  ? {
      advanced: {
        crossSubDomainCookies: {
          enabled: true,
          domain: process.env.CROSS_DOMAIN_ORIGIN, // Domain with a leading period
        },
        defaultCookieAttributes: {
          secure: true,
          httpOnly: true,
          sameSite: 'none', // Allows CORS-based cookie sharing across subdomains
          partitioned: true, // New browser standards will mandate this for foreign cookies
        },
      },
    }
  : {})

To enable cross-domain session sharing:

  1. Add to your .env file:

    CROSS_DOMAIN_ORIGIN=".yourdomain.com"  # Include the leading period
  2. Ensure your frontend and API are on subdomains of the same parent domain:

    • API: api.yourdomain.com
    • Frontend: app.yourdomain.com

This configuration enables secure sharing of authentication cookies between your subdomains, maintaining session continuity for users navigating between different parts of your application.

For more details, refer to Better Auth Cookies Documentation.


πŸ›‘οΈ Protecting Routes

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() };
  }
}

🌐 Global API Prefix

Set in src/main.ts:

async function bootstrap() {
  const app = await NestFactory.create<NestExpressApplication>(AppModule, {
    bodyParser: false,
  });
  app.set('query parser', 'extended');

  const trustedOrigins = (process.env.TRUSTED_ORIGINS as string).split(',');

  app.enableCors({
    origin: trustedOrigins,
    credentials: true,
  });

  app.setGlobalPrefix('api', { exclude: ['/api/auth/{*path}'] });

  await app.listen(process.env.PORT ?? 3000);
}

🧩 Frontend Integration (Next.js)

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", // API base URL
  plugins: [
    inferAdditionalFields({
      user: {
        surname: { type: 'string' },
        role: { type: 'string', nullable: true },
      },
    }),
  ],
});

Do not include /api in the baseURL.


πŸ” Redirect After Sign-in

const { error, data } = await signIn.email({
  email,
  password,
  rememberMe,
  callbackURL: "http://localhost:3001/dashboard", // Frontend URL, not API
});

callbackURL should point to the frontend URL where you want to redirect after sign-in.


πŸ”’ Session Handling in Next.js

Middleware (middleware.ts)

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();
}

Server-side Rendering (page.tsx)

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 from Protected API

export default async function Page() {
  const res = await fetch("http://localhost:3000/api/foo", {
    headers: {
      cookie: (await headers()).get('cookie') || '', // Include cookies for session
    },
    cache: 'no-store',
  });

  const data = await res.json();

  return (
    <div>
      <h1>Protected Page</h1>
      <pre>{JSON.stringify(data, null, 2)}</pre>
    </div>
  );
}

Client Component Example

'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', // Include cookies for session
      });
      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>
  );
}

πŸ§ͺ Tips

  • Use .env variables 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.

About

This project demonstrates how to integrate Better Auth into a NestJS application

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published