Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature-request: Add Nest js support #171

Open
danyalutsevich opened this issue Oct 14, 2024 · 28 comments
Open

feature-request: Add Nest js support #171

danyalutsevich opened this issue Oct 14, 2024 · 28 comments

Comments

@danyalutsevich
Copy link

This is feature request to add nest js support

@Bekacru Bekacru added this to the v1.0 (Stable Version) milestone Oct 14, 2024
@NoHaxito
Copy link
Contributor

NoHaxito commented Oct 15, 2024

@Bekacru i have an idea on how to "implement" without modifying the current code (only docs)
Is it okay if i do a PR with a
"temporary guide"? (If you plan to do a "custom" integration as is the case with Next.js, Svelte, etc.)

The only thing is to create a controller and module and get the req and res parameters from the decorators.

@Bekacru
Copy link
Contributor

Bekacru commented Oct 15, 2024

is it okay if i do a PR with a "temporary guide"? (If you plan to do a "custom" integration as is the case with Next.js, Svelte, etc.)

yes feel free to open a pr. and we're not going to have integration we only need the docs.

@NoHaxito
Copy link
Contributor

is it okay if i do a PR with a "temporary guide"? (If you plan to do a "custom" integration as is the case with Next.js, Svelte, etc.)

yes feel free to open a pr. and we're not going to have integration we only need the docs.

Ok! I will try to have the nestjs docs soon 🙌🏼🚀

@ismoiliy98
Copy link

Any progress on this?

@danyalutsevich
Copy link
Author

@ismoiliy98 join us in discord
we have couple of problems with module system in nest and better-auth
feels like we are about to finish)

@BayBreezy
Copy link
Contributor

I tried adding better auth to nest but the ESM nature of better auth is nor playing nicey with NestJS. @danyalutsevich which discord do i need to join to see the discusiion?

@danyalutsevich
Copy link
Author

danyalutsevich commented Nov 2, 2024

@BayBreezy you can check the repo from this #359 issue
you can use dynamic import as a workaround

discord: https://discord.com/invite/GYC3W7tZzb
if it tells you that invite is expired copy the link and paste it to join server window
Screenshot 2024-11-02 at 15 09 36

@danyalutsevich
Copy link
Author

I dont know if it's possible to merge issues on github
we have duplicate issues

#406 #359

@subenksaha
Copy link

I think the problem is from nanoid module. I am getting the following error:
Screenshot 2024-11-13 at 9 57 55 PM

if anybody can fix this that would be great help. I could do it by myself but I am getting DTS error when I build the project.

@aemara
Copy link

aemara commented Nov 15, 2024

@subenksaha

I think the problem is from nanoid module. I am getting the following error: Screenshot 2024-11-13 at 9 57 55 PM

if anybody can fix this that would be great help. I could do it by myself but I am getting DTS error when I build the project.

I remember running into this problem with nanoid. I was using version 5.0.7, and when I used an older one (3.3.7) the problem went away. I tried to use dynamic import as a workaround but for some reason it didn't work.

@subenksaha
Copy link

@aemara yes, that can be workaround but not permanent fix. I created a PR #550 replacing nanoid with @paralleldrive/cuid2 that might solve the issue.

@Bekacru Bekacru removed this from the v1.0 (Stable Version) milestone Nov 25, 2024
@Innei
Copy link

Innei commented Dec 3, 2024

I've tried to get the migration done and working in nest js.
I use a way to compile to cjs and then use it directly in nestjs.

https://github.com/mx-space/core/blob/49cc5b628fd6e4b8cd5c2adf35c40bf982621b28/apps/core/src/modules/auth/auth.implement.ts

@subenksaha
Copy link

@Innei , so you compiled inside your code? or compiled to another package? What is the way?

@Innei
Copy link

Innei commented Dec 3, 2024

complied sometimes has to be done, in writing nodejs server a few used pkg does not provide cjs export, so need to be converted.

Create a separate subpackage dedicated to compiling these packages using a monorepo.

https://github.com/mx-space/core/blob/6e50bee8dafbd7e56742b711d01a167c70f96f9a/packages/complied/tsup.config.ts

@marcomuser
Copy link

marcomuser commented Dec 5, 2024

Are there plans on the nest.js side to move to esm? That would seem like the more healthy fix since the whole ecosystem is moving that direction. In particular since also typescript is starting to think about deprecating and removing features nest.js relies on, e.g. useDefineForClassFields: false and probably later on experimentalDecorators: true. See: microsoft/TypeScript#45995 (comment). If nest.js doesn't want to be left back in the dark there is gonna have to be some movement soon anyway towards esm and standard decorators.

@ismoiliy98
Copy link

Are there plans on the nest.js side to move to esm?

@marcomuser Unfortunately, no 😞

nestjs/nest#13319
nestjs/nest#13817

@ddedic
Copy link

ddedic commented Dec 19, 2024

Any updates on this? Is there rough eta for nestjs support?

@BayBreezy
Copy link
Contributor

We out here waiting patiently @ddedic

@niraj-khatiwada
Copy link

niraj-khatiwada commented Dec 28, 2024

Oh man I was so excited to integrate better-auth but was disappointed that it does not support Nest.js.
Maybe in the future then.

@Bekacru
Copy link
Contributor

Bekacru commented Jan 13, 2025

could anyone please confirm me if there are still issue with nest js support?

@BayBreezy
Copy link
Contributor

BayBreezy commented Jan 13, 2025

@Bekacru I am still investigating.

I was able to get most endpoints working by adding this middleware

import { Injectable, NestMiddleware } from "@nestjs/common";
import { Request, Response, NextFunction } from "express";
import * as express from "express";

@Injectable()
export class RawBodyMiddleware implements NestMiddleware {
  constructor() {
    console.log("RawBodyMiddleware initialized");
  }

  use(req: Request, res: Response, next: NextFunction) {
    // Check if the route matches the desired pattern
    if (req.baseUrl.startsWith("/api/auth")) {
      // Skip JSON and URL-encoded body parsing for these routes
      console.log("Skipping body parsing for:", req.baseUrl);
      next();
      return;
    }

    // Otherwise, parse the body as usual
    express.json()(req, res, (err) => {
      if (err) {
        next(err); // Pass any errors to the error-handling middleware
        return;
      }
      express.urlencoded({ extended: true })(req, res, next);
    });
  }
}

And then registering it like so

export class AppModule {
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(RawBodyMiddleware).forRoutes("*");
  }
}

This is my auth config

import { betterAuth } from "better-auth";
import { prismaAdapter } from "better-auth/adapters/prisma";
import { PrismaClient } from "@prisma/client";
import { openAPI, bearer, admin } from "better-auth/plugins";

const prisma = new PrismaClient();
export const auth = betterAuth({
  database: prismaAdapter(prisma, {
    provider: "sqlite", // or "mysql", "postgresql", ...etc
  }),
  // @ts-expect-error - TS shinanigans
  plugins: [openAPI(), admin(), bearer()],
  emailAndPassword: {
    enabled: true,
    autoSignIn: true,
  },
});

This is the controller

@Controller()
export class AppController {
  @All("api/auth/*")
  async auth(@Req() req: Request, @Res() res: Response) {
    return toNodeHandler(auth)(req, res);
  }
}

In the main.ts file, disable global bodyParser

async function bootstrap() {
  const app = await NestFactory.create<NestExpressApplication>(AppModule, {
    bodyParser: false,
  });
  app.enableCors();
  await app.listen(process.env.PORT);
  Logger.log(`Server running on ${process.env.PUBLIC_URL}`, "Bootstrap");
  Logger.log(`Better Auth API Spec on: ${process.env.PUBLIC_URL}/api/auth/reference`, "Bootstrap");
}
bootstrap();

I will report back if i run into any trouble. For now most things work. I did notice that the bearer & admin plugins were causing all types of type errors. Not sure what that is about

The TS Errors
[5:39:15 AM] Starting compilation in watch mode...

src/lib/auth.ts:11:24 - error TS2322: Type '{ id: "admin"; init(ctx: AuthContext): { options: { databaseHooks: { user: { create: { before(user: { id: string; email: string; emailVerified: boolean; name: string; createdAt: Date; updatedAt: Date; image?: string; }): Promise<...>; }; }; session: { ...; }; }; }; }; hooks: { ...; }; endpoints: { ...; }; $ERROR_COD...' is not assignable to type 'BetterAuthPlugin'.
  The types of 'hooks.after' are incompatible between these types.
    Type '{ matcher(context: HookEndpointContext<{ returned: Response | Record<string, any> | APIError; endpoint: Endpoint; }>): boolean; handler: Endpoint<...>; }[]' is not assignable to type '{ matcher: (context: HookEndpointContext<{ returned: Response | Record<string, any> | APIError; endpoint: Endpoint; }>) => boolean; handler: HookAfterHandler; }[]'.
      Type '{ matcher(context: HookEndpointContext<{ returned: Response | Record<string, any> | APIError; endpoint: Endpoint; }>): boolean; handler: Endpoint<...>; }' is not assignable to type '{ matcher: (context: HookEndpointContext<{ returned: Response | Record<string, any> | APIError; endpoint: Endpoint; }>) => boolean; handler: HookAfterHandler; }'.
        Types of property 'handler' are incompatible.
          Type 'Endpoint<Handler<string, EndpointOptions, { response: { body: any; status: number; statusText: string; headers: Record<string, string>; }; body: SessionWithImpersonatedBy[]; _flag: "json"; }>, EndpointOptions>' is not assignable to type 'HookAfterHandler'.
            Types of parameters 'ctx' and 'context' are incompatible.
              Type 'HookEndpointContext<{}>' is not assignable to type '{ body: { [x: string]: any; }; params?: Record<string, string>; method?: "GET"; headers: Headers; request: Request; query: any; _flag?: "json" | "router" | "default"; ... 10 more ...; responseHeader: Headers; } | ... 26 more ... | { ...; }'.
                Type 'HookEndpointContext<{}>' is not assignable to type '{ body: { [x: string]: any; }; params?: Record<string, string>; method: Method; headers?: Headers; request?: Request; query: any; _flag?: "json" | "router" | "default"; ... 10 more ...; responseHeader: Headers; }'.
                  Property 'method' is optional in type 'HookEndpointContext<{}>' but required in type '{ body: { [x: string]: any; }; params?: Record<string, string>; method: Method; headers?: Headers; request?: Request; query: any; _flag?: "json" | "router" | "default"; ... 10 more ...; responseHeader: Headers; }'.

11   plugins: [openAPI(), admin(), bearer()],
                          ~~~~~~~

src/lib/auth.ts:11:33 - error TS2322: Type '{ id: "bearer"; hooks: { before: { matcher(context: HookEndpointContext): boolean; handler: (c: HookEndpointContext) => Promise<{ context: HookEndpointContext; }>; }[]; after: { ...; }[]; }; }' is not assignable to type 'BetterAuthPlugin'.
  The types of 'hooks.after' are incompatible between these types.
    Type '{ matcher(context: HookEndpointContext<{ returned: Response | Record<string, any> | APIError; endpoint: Endpoint; }>): boolean; handler: Endpoint<...>; }[]' is not assignable to type '{ matcher: (context: HookEndpointContext<{ returned: Response | Record<string, any> | APIError; endpoint: Endpoint; }>) => boolean; handler: HookAfterHandler; }[]'.
      Type '{ matcher(context: HookEndpointContext<{ returned: Response | Record<string, any> | APIError; endpoint: Endpoint; }>): boolean; handler: Endpoint<...>; }' is not assignable to type '{ matcher: (context: HookEndpointContext<{ returned: Response | Record<string, any> | APIError; endpoint: Endpoint; }>) => boolean; handler: HookAfterHandler; }'.
        Types of property 'handler' are incompatible.
          Type 'Endpoint<Handler<string, EndpointOptions, { responseHeader: Headers; }>, EndpointOptions>' is not assignable to type 'HookAfterHandler'.
            Types of parameters 'ctx' and 'context' are incompatible.
              Type 'HookEndpointContext<{}>' is not assignable to type '{ body: { [x: string]: any; }; params?: Record<string, string>; method?: "GET"; headers: Headers; request: Request; query: any; _flag?: "json" | "router" | "default"; ... 10 more ...; responseHeader: Headers; } | ... 26 more ... | { ...; }'.
                Type 'HookEndpointContext<{}>' is not assignable to type '{ body: { [x: string]: any; }; params?: Record<string, string>; method: Method; headers?: Headers; request?: Request; query: any; _flag?: "json" | "router" | "default"; ... 10 more ...; responseHeader: Headers; }'.
                  Property 'method' is optional in type 'HookEndpointContext<{}>' but required in type '{ body: { [x: string]: any; }; params?: Record<string, string>; method: Method; headers?: Headers; request?: Request; query: any; _flag?: "json" | "router" | "default"; ... 10 more ...; responseHeader: Headers; }'.

11   plugins: [openAPI(), admin(), bearer()],
                                   ~~~~~~~~

[5:39:23 AM] Found 2 errors. Watching for file changes.

@BayBreezy
Copy link
Contributor

Anyone else here able to test out nest?
@wh5938316 ? I tried it and it seems to work with a few changes.
I must say because of how deep the types are in better-auth, it slows vscode to a crawl in a nestjs project. is there a way around that?

@Oupsla
Copy link

Oupsla commented Jan 16, 2025

@BayBreezy I have tried your solution with the RawBodyMiddleware and it is working great.

I have gone a step further and put the creation of the auth object into a service, like that I can use the nestjs DI (to inject my database for example).

I didn't found a simple type from better auth to type the "auth" object, so I have left any at the moment

The module

import { Module } from "@nestjs/common";
import { DatabaseModule } from "src/services/database/database.module";
import { DatabaseService } from "src/services/database/database.service";
import { AuthController } from "./auth.controller";
import { AuthService } from "./auth.service";
import { ConfigModule, ConfigService } from "@nestjs/config";

const authServiceProvider = {
  provide: AuthService,
  useFactory: (
    databaseService: DatabaseService,
    configService: ConfigService
  ) => {
    const baseURL = configService.get("BETTER_AUTH_URL");
    const secret = configService.get("BETTER_AUTH_SECRET");
    const googleClientId = configService.get("GOOGLE_CLIENT_ID");
    const googleClientSecret = configService.get("GOOGLE_CLIENT_SECRET");

    return new AuthService(
      databaseService,
      googleClientId,
      googleClientSecret,
      baseURL,
      secret
    );
  },
  inject: [DatabaseService, ConfigService],
};

@Module({
  imports: [DatabaseModule, ConfigModule],
  providers: [authServiceProvider],
  controllers: [AuthController],
})
export class AuthModule {}

The service

import { Injectable } from "@nestjs/common";
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { toNodeHandler } from "better-auth/node";
import { openAPI } from "better-auth/plugins";
import { Request, Response } from "express";
import { DatabaseService } from "src/services/database/database.service";

@Injectable()
export class AuthService {
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  private auth: any;

  constructor(
    private readonly database: DatabaseService,
    private readonly googleClientId: string,
    private readonly googleClientSecret: string,
    private readonly baseURL: string,
    private readonly secret: string
  ) {
    this.auth = betterAuth({
      secret: this.secret,
      baseURL: this.baseURL,
      database: drizzleAdapter(this.database.getDatabase(), {
        provider: "pg",
        usePlural: true,
      }),
      emailAndPassword: {
        enabled: true,
        autoSignIn: true,
      },
      socialProviders: {
        google: {
          clientId: this.googleClientId,
          clientSecret: this.googleClientSecret,
        },
      },
      plugins: [openAPI()],
    });
  }

  async handler(req: Request, res: Response): Promise<void> {
    return toNodeHandler(this.auth)(req, res);
  }
}

The controller

import { All, Controller, Req, Res } from "@nestjs/common";
import { ApiOperation, ApiTags } from "@nestjs/swagger";
import type { Request, Response } from "express";
import { AuthService } from "./auth.service";

@ApiTags("auth")
@Controller()
export class AuthController {
  constructor(private readonly authService: AuthService) {}

  @All("api/auth/*")
  @ApiOperation({
    summary: "Auth handler",
  })
  async auth(@Req() req: Request, @Res() res: Response) {
    return this.authService.handler(req, res);
  }
}

And I have used your example for the app.module and the middleware.
For people coming on this post, use the middleware and not the app.use(json({ limit: "5mb" })); from express to avoid the following error Response body object should not be disturbed or locked

@BayBreezy
Copy link
Contributor

Ty @Oupsla !
The reason why i did not go with the service setup was because of the generate command. I am assuming that it did not pickup your configuration and you had to create the tables manually?

Yes, I struggled with the Response body object should not be disturbed or locked error initially.

I was thinking about duplicating the auth config - create a service for Nest DI & one for the better-auth cli. Thoughts?

@Oupsla
Copy link

Oupsla commented Jan 17, 2025

I am assuming that it did not pickup your configuration and you had to create the tables manually?

Yes

I was thinking about duplicating the auth config - create a service for Nest DI & one for the better-auth cli. Thoughts?

Yes I think you should have to use the Nest DI, because if not you will also have to duplicate every services that you use in your auth config (db service for example, but also services used by this one, etc...)

Maybe a good solution will be to do the same thing that drizzle, having a drizzle.config.ts at the root of your project for the CLI, that will contains info about tables configurations, etc...
And then you will have your auth object that will pick up this base configuration and add things like the db connector, secrets, etc...

Maybe this will require some changes in the current state of better-auth lib

@Syarx
Copy link

Syarx commented Jan 28, 2025

@Oupsla

you can use

  private auth: ReturnType<typeof betterAuth>;

@ThallesP
Copy link

Hey @Bekacru is a nestjs integration not wanted? i wouldn't mind contribuing an integration for nestjs
i've thought of something like this:

import { Module } from '@nestjs/common';
import { AuthModule } from 'better-auth/nestjs';

@Module({
  imports: [
    AuthModule.forRoot({ // everything needed for auth!
      emailAndPassword: {
        enabled: true,
      },
    }),
  ],
})
export class AppModule {}

I might also include decorators for hooks:

import { Injectable } from "@nestjs/common";
import { BeforeHook, AuthContext } from "better-auth/nest";
import { SignUpService } from "./sign-up.service";

@Injectable()
export class SignUpHook {
    constructor(private readonly signUpService: SignUpService) {}

    @BeforeHook('/sign-up/email')
    async handle(ctx: AuthContext) {
        // custom logic like enforcing email domain registration
        // might throw APIError if something is wrong
        await this.signUpService.execute(ctx);
    }
}

And also, I'm figuring out an easy way to integrate adapters and DI (for example a PrismaService and prismaAdapter) but I might consider just using a nestjs factory instead... if anyone from the community has a better idea lmk!

@BayBreezy
Copy link
Contributor

Hey guys,

Just reporting back with an example app I created with NestJS. the code can be found here: https://github.com/BayBreezy/LearnReact/tree/main/login-system/api

I am learning React now(I think they have more jobs than Vue/Nuxt).

Here is the frontend to test it out: https://login-system.learn-react.behonbaker.com/

Feedback appreciated 🙏🏽

P.S: @Oupsla I did not go with the approach you mentioned(injecting auth as a provider). Thanks @Bekacru for updating the types. Not getting the same errors with the plugins anymore.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
No open projects
Status: No status
Development

No branches or pull requests