Better Auth with Hono, Bun, TypeScript, React and Vite

You'll learn how to implement authentication with Better Auth in a client - server architecture, where the frontend is separate from the backend.

The backend uses Hono and Bun, whereas the frontend uses React with Vite. For brevity, this article will only include the relevant bits instead of taking you through the whole process of building the full-stack application from scratch.

You can find the link to the repository with the complete code at the end of this article.

Backend: Better Auth with Hono and Bun

We'll start by installing Better Auth:

bun add better-auth

After that, create an .env file with the following environment variables:

# Auth
BETTER_AUTH_SECRET=<generate-a-secret-key>
BETTER_AUTH_URL=<url-of-your-server> (e.g. http://localhost:1234)

Now you're ready to configure Better Auth.

Create the Better Auth Instance

In my case, I placed the Better Auth instance in the auth.ts file. My project has the following requirements:

  • it uses PostgreSQL for the database
  • it uses Prisma ORM
  • the app should allow users to log in/sign up with email and password
  • the app should allow users to log in/sign up with their Google or GitHub accounts
  • users can have different roles
import { Role } from "@prisma/client";
import { betterAuth } from "better-auth";
import { prismaAdapter } from "better-auth/adapters/prisma";

import prisma from "@/db/index";
import env from "@/env";

export const auth = betterAuth({
  database: prismaAdapter(prisma, {
    provider: "postgresql",
  }),
  // Allow requests from the frontend development server
  trustedOrigins: ["http://localhost:5173"],
  emailAndPassword: {
    enabled: true,
  },
  socialProviders: {
    github: {
      clientId: env.GITHUB_CLIENT_ID,
      clientSecret: env.GITHUB_CLIENT_SECRET,
    },
    google: {
      clientId: env.GOOGLE_CLIENT_ID,
      clientSecret: env.GOOGLE_CLIENT_SECRET,
    },
  },
  user: {
    additionalFields: {
      roles: {
        type: [Role.STUDENT, Role.CREATOR, Role.ADMIN],
        required: true,
        defaultValue: [Role.STUDENT],
      },
    },
  },
});

The above code does the following:

  1. It sets up the database to use Prisma ORM and PostgreSQL.
  2. It specifies the trusted origins, which are the origins allowed to make requests.
    1. In this case, it's the client (frontend). All the other origins that are not specified are automatically blocked.
  3. It enables email/password authentication and configures social login providers.
  4. Lastly, it extends the core user schema to add the additional field roles
    1. That means that each user can have multiple roles.

Once you're done with configuring your Better Auth instance, run the following command to generate the ORM schema:

bunx @better-auth/cli generate

It adds all the required models, fields, and relationships to the Prisma schema file.

Create the API Handler

The next step is to set up a route handler for the auth API requests.

The route below uses the handler provided by Better Auth to serve all POST and GET requests to the /api/auth endpoint.

import { auth } from "@/lib/auth";
import { createRouter } from "@/lib/create-app";

const router = createRouter();

router.on(["POST", "GET"], "/auth/**", (c) => {
  return auth.handler(c.req.raw);
});

export default router;

The above code lives in the routes/auth.ts file.

Lastly, you need to mount the route in the app.ts file.

....
import createApp from "@/lib/create-app";
import auth from "@/routes/auth";
....

const app = createApp();

const routes = [userSettings, creator, student, courses, auth] as const;

routes.forEach((route) => {
  app.basePath("/api").route("/", route);
});

....

export default app;

Once that's done, Better Auth on the backend is all set.

Frontend: Better Auth with React and Vite

The first step is to install Better Auth in the client directory:

bun add better-auth

Once that's installed, navigate to /src/lib/auth-client and add the following code:

import { createAuthClient } from "better-auth/react";
import { inferAdditionalFields } from "better-auth/client/plugins";

export const authClient = createAuthClient({
  plugins: [
    inferAdditionalFields({
      user: {
        roles: {
          type: "string[]",
        },
      },
    }),
  ],
});

export const {
  useSession,
  signIn,
  signUp,
  signOut,
  forgetPassword,
  resetPassword,
} = authClient;

export type Session = typeof authClient.$Infer.Session;
export type User = typeof authClient.$Infer.Session.user;

The client-side library allows you to interact with the Better Auth instance from the server.

The above code:

  • Calls the createAuthClient function to create the auth client.
  • Stores the result of the function call in a constant authClient.
    • It returns various hooks and functions related to authentication.
  • Uses the plugins property to extend the user object by adding the roles property.
  • Exports specific auth hooks and methods, so you don't have to import the entire authClient object to use them.
    • You can import and use them directly.
  • Lastly, it uses the $Infer property from Better Auth to export the "User" and "Sessions" types.

You're done and can start authenticating users.

Register Component

You need to import the signUp object from the /src/lib/auth-client file and pass the data provided by the user to its email method.

The method provides various callbacks, such as onRequest, onSuccess, and onError, allowing you to perform various actions at different stages of the registration process.

For example, the onRequest callback could be used to set a loading state until the registration resolves.

In this example, the user is redirected to the homepage if the registration is successful (redundant since that's the default Better-Auth behaviour, but its purpose is to showcase the onSuccess callback).

If there is an error during the registration process, it'll log the error (not recommended for production use).

...
  
  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();

    if (password !== confirmPassword) {
      alert("Passwords do not match");
      return;
    }

    await signUp.email(
      { email, password, name, roles: ["STUDENT"] },
      {
        onSuccess: () => {
          navigate({ to: "/" });
        },
        onError: (error) => {
          console.error(error);
        },
      }
    );
  };

...

Better Auth automatically redirects the user to the home page on successful registration. If you want to change the default behaviour, see how to do it here.

Login Component

The login functionality works similarly to the registration functionality.

Import the signIn object from the auth-client and call the appropriate authentication methods. In this case:

  • email to allow the users to sign in with email and password
  • social to allow the users to sign in via Google or GitHub
...

  const handleEmailLogin = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();

    await signIn.email(
      { email, password },
      {
        onSuccess: () => {
          navigate({ to: "/" });
        },
        onError: (error) => {
          console.error(error);
        },
      }
    );
  };

  const handleGoogleLogin = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();

    await signIn.social(
      { provider: "google" },
      {
        onSuccess: () => {
          navigate({ to: "/" });
        },
        onError: (error) => {
          console.error(error);
        },
      }
    );
  };

  const handleGithubLogin = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();

    await signIn.social(
      { provider: "github" },
      {
        onSuccess: () => {
          navigate({ to: "/" });
        },
        onError: (error) => {
          console.error(error);
        },
      }
    );
  };

  ...

Demo

The video below shows the following flows:

  • registers a user
  • signs out the user
  • signs in the user
0:00
/0:19
The complete code is available in this repository.

What's next?

  • Implement Resend to send emails
  • Implement Polar to accept payments

References:

Support this blog 🧡

If you like this content and it helped you, please consider supporting this blog. This helps me create more free content and keep this blog alive.

Become a supporter