Introduction: A Comprehensive Walkthrough of Grafbase Integration with Next.js 13 for Database and Authentication-NextAuth

Rocky EsselRocky Essel
28 min read

Get ready to embark on a journey that's part coding wizardry and part digital detective work, as you and I unravel the mysteries of connecting Grafbase with Next.js 13 and dancing hand-in-hand with Authentication-NextAuth. By the time you're done, you'll be slinging queries and authenticating like a tech-savvy Sherlock Holmes, and you might find yourself giving your database a standing ovation. So, grab your magnifying glass and let's dive into this delightful tech tango! πŸ•΅οΈβ€β™‚οΈπŸŽ©πŸ“Š

That's by the way.

So in this walkthrough, I'll take you from creating and deploying your Grafbase project from the CLI, and then integrate NextAuth within our Next.js 13 project while using Grafbase Graphql API. And in case you're worried if you don't understand GraphQL and how it works, then I'm here for you, I promise to explain thoroughly. So relax and follow along as I take you through this walkthrough.

Before you start, let me talk about what Grafbase is capable of, and how amazing it is.

What really is Grafbase?

Grafbase is a platform that allows and gives power to developers to build GraphQL API fast and effectively and also allows developers to connect any type of database using resolvers and connectors. It also allows developers to set authentication and permission rules to an endpoint.

So in a nutshell, Grafbase is a backend for your mobile application or web application that's all the complex stuff for you and then lets you handle the easy part, but that's just the surface level, its capabilities goes beyond what I described.
So at least you have a fair of what Grafbase is about.

To start your journey, first, you either sign up or sign in to grafbase in order to use their services.

After you have completed the registration or login, you'll be redirected to your dashboard, then from there, move to your terminal.

Now I'll show how easy it is to create, link, and deploy a project. Continuing, create a directory called grafbase-auth

Project Setup

To set up our project, let's first initial a Next.js project, by entering the command below in the terminal.

npx create-next-app ./

Say yes to all the options that will be given on the terminal. And it should look like this below:

√ Would you like to use TypeScript with this project? ... No / Yes
√ Would you like to use ESLint with this project? ... No / Yes
√ Would you like to use `src/` directory with this project? ... No / Yes
√ Would you like to use experimental `app/` directory with this project? ... No / Yes
√ What import alias would you like configured? ... @/*
Creating a new Next.js app in C:\Users\essel_r\desktop\grafbase-auth.

Using npm.

Installing dependencies:
- react
- react-dom
- next
- @next/font
- typescript
- @types/react
- @types/node
- @types/react-dom
- eslint
- eslint-config-next

After that, you need to install the packages below to use our GraphQL seamlessly in our walkthrough.

npm install @apollo/client graphql-request

You'll be using NextAuth for authenticating your application in this walkthrough and might need other packages like bcrypt for password hashing and JWT for our custom token, so install that below.

npm install next-auth jsonwebtoken @types/jsonwebtoken bcrypt @types/bcrypt axios react-icons encoding

After the above installation is done, let's move on to the main dish of this walkthrough 😁.

Now to install Grafbase globally in your terminal, run this command on your terminal.

npm install -g grafbase

After this step, you can now run Grafbase(Your Backend), with only a few commands, init, create, link, deploy, dev

You'll be using the above commands frequently, so let's explain what each command does.

For npx grafbase init : Is used for setting up the current or existing project in your directory, or for new projects. Where you will be asked to choose whether to have your project in GraphQL or TypeScript.

npx grafbase init

So now, run the following command in your terminal, then run ls in your parent directory, a directory called grafbase is created and inside of it is your schema.graph. You'll then get an output like so:

$ npx grafbase init
Grafbase CLI 0.30.0

> What configuration format would you like to use? GraphQL
✨ Your project was successfully set up for Grafbase!
Your new schema can be found at .\grafbase\schema.graphql

After you can run the project in the terminal run the command below npx grafbse dev and that's for the next step. We'll talk about the pathfinder later, and how to start query data, but if you want to check that before continuing then here is a link to the Grafbase doc

npx grafbase dev

The output for the above command should look like this:

$ npx grafbase dev
Grafbase CLI 0.30.0

πŸ“‘ Listening on port 4000

- Pathfinder: http://127.0.0.1:4000
- Endpoint:   http://127.0.0.1:4000/graphql

So now let's stop or kill the terminal, and then see what npx grafbase create allows you to do. The command below allows us to create a project with your existing project, meaning in order to create a project, you first need to initial a grafbase project which has either grafbase/grafbase.config.ts or grafbase/schema.graphql , or you will get an error saying: Error: could not find grafbase/grafbase.config.ts or grafbase/schema.graphql in the current or any parent directory.

npx grafbase create

After running this project you will be asked a few questions related to your project. Then your project will be created and deployed. The output:

$ npx grafbase create
Grafbase CLI 0.30.0

> What should your new project be called? grafbase-auth
> In which account should the project be created? Rocky Essel (rockyessel)
> In which region should your database be created? Frankfurt (eu-central-1)
> Please confirm the above to create and deploy your new project Yes

✨ grafbase-auth was successfully created!

Endpoints:
- https://grafbase-auth.....grafbase.app/graphql
- https://grafbase-auth-.....grafbase.app/graphql

The next step is the npx grafbse deploy, it helps you deploy your existing or new projects. But in case your project is an existing project you clone from Github, then in order for you to deploy any changes to the GraphQL API endpoint, you have to link the existing project to the project in Grafbase.

npx grafbase link && npx grafbase deploy

So this explains the relationship between link and deploy.

npx grafbase login

With the login, this gives you the authorisation to perform the above operation explains. So now I think you're good to go!!!!

Now your project has been initialised, created and deployed. Let's start the fun.

Walkthrough - Part 1

Inside of our schema.graph

type Post @model @search {
  title: String!
  # Other field
}

type Comment @model @search {
  post: Post!
  # Other field
}

type User @model {
  name: String!
  # Other field
}

Let's go over the GraphQL schema but from the perspective of how JavaScript developers build their model using mongoose in Node.js

GraphQL Basic

Firstly, GraphQL is a query language for your API, and the schema you see above is like a contract that outlines the types of data that can be queried and manipulated using GraphQL operations. In contrast to RESTful APIs, where you typically have multiple endpoints for different resources, GraphQL uses a single endpoint (https://...../graph) and allows clients to request exactly the data they need, which can reduce over-fetching and under-fetching of data.

Here is the breakdown of the default schema generated by Grafbase:

  1. Post Type:

    • Represents a blog post.

    • @model indicates that this type is backed by some data source (like a database table), and likely operations (like CRUD) can be performed on it.

    • @search might indicate that there's search functionality integrated, possibly through some plugin or service.

    • title: A non-null string field representing the title of the post.

    • slug: A non-null unique string field, possibly for SEO-friendly URLs.

    • content: An optional string field for the content of the post.

    • publishedAt: A DateTime field indicating when the post was published.

    • comments: An array of Comment objects associated with this post.

    • likes: An integer field with a default value of 0, representing the number of likes on the post.

    • tags: An array of strings, possibly representing tags associated with the post.

    • author: A User an object representing the author of the post.

  2. Comment Type:

    • Represents a comment on a post.

    • post: A required reference to an Post object, indicating which post this comment belongs to.

    • body: The content of the comment.

    • likes: An integer field with a default value of 0, representing the number of likes on the comment.

    • author: A User object representing the author of the comment.

  3. User Type:

    • Represents a user.

    • name: A non-null string field representing the name of the user.

    • email: An Email scalar type (which seems to be a custom scalar) representing the email of the user.

    • posts: An array of Post objects authored by this user.

    • comments: An array of Comment objects authored by this user.

Remember, GraphQL can provide a more flexible and efficient way to fetch and manipulate data compared to traditional RESTful APIs, especially when dealing with complex data relationships and avoiding over-fetching or under-fetching data.

Setting up Authentication With NextAuth

Earlier in our walk-through, you install next-auth, for those that don't know what it is. next-auth is an authentication library is that designed to work with any OAuth services, to allow secured and flexible login with passwordless login or signups.

These OAuth services include Google, Facebook, Linkedin, Github, and Twitter, allowing users to authenticate using them. And that's next-auth

Below is how your next.js project directory should look. In the [...nextauth] directory you create a TypeScript file called router.ts.

src
└───app
    └───api
        └───auth
            └───[...nextauth]
πŸ’‘
Remember that the directory path is src/app/api/auth/[...nextauth] and not src/app/api/[...nextauth] or this src/app/api/auth/[...nextAuth], otherwise, you definitely get an error.

So this is how your router.ts code should be like

// route.ts
import NextAuth, { AuthOptions } from 'next-auth';
import GitHubProvider from 'next-auth/providers/github';

const authOptions: AuthOptions = {
  providers: [
    GitHubProvider({
      clientId: process.env.GITHUB_CLIENT_ID!,
      clientSecret: process.env.GITHUB_CLIENT_SECRET!,
    }),
  ],
  debug: process.env.NODE_ENV === 'development',
  session: {
    strategy: 'jwt',
  },
  secret: process.env.NEXTAUTH_SECRET!,
}

const handler = NextAuth(authOptions);

export {handler as GET, handler as POST};

Let's talk about the above code.

Imports
You first import the modules to use next-auth or integrate next-auth in our next.js project for authentication. And our case users will be authenticating with their GitHub accounts.

Options Configuration
The options part is the configuration process for our auth set-up so for the authOptions object keys and value and here is what the keys represent;

  • providers: An array of authentication providers. In this case, it's using the GitHubProvider with the GitHub client ID and client secret obtained from environment variables.

  • debug: Determines whether debugging information should be displayed. It's set to true in development mode and false in other environments.

  • session: Specifies the session strategy. Here, it's using JSON Web Tokens (JWT) for session management.

  • secret: The secret key used to sign and verify tokens. It's obtained from an environment variable.

Handler

const handler = NextAuth(authOptions);

This line of code initializes the authentication handler using the NextAuth function and the previously configured authOptions. The handler will be responsible for handling the authentication requests.

Exporting Handlers
This exports the handler function under different names (GET and POST). Exporting it as GET and POST allows to make both GET and POST request. So when you login in or sign up you're making a POST request, and then when you navigate between routes, it makes a GET request to retrieve your information.

And that's it for our authentication, now you're done, by doing it this way, we have left everything to handle in default, but since we want to store user information like username, name, image, password, and email. That means you need to customize the src/app/api/auth/[...nextauth]/route.ts so can save the data to our Grafbase GraphQL backend, and return the information to use in our application.

Believe me, I would love to explain everything from my thinking process to how you can customize the however you want to your needs, but that will be pretty long, so I will paste the code here can explain at a go.

Customizing the authentication process.

But let's refactor the directory as so;

//route.ts
import { authOptions } from '@/lib/auth';
import NextAuth from 'next-auth';

const handler = NextAuth(authOptions);

export { handler as GET, handler as POST };

What we did was export authOptions from a lib directory you should create the src directory, reason it was, exporting it in the route.ts would cause an error, and also you will so use it later in your code.

root dir
β”œβ”€β”€β”€app
β”‚   └───api
β”‚       └───auth
β”‚           └───[...nextauth]    
β”œβ”€β”€β”€lib
β”‚   β”œβ”€β”€β”€api
β”‚   β”‚   └───index.ts
β”‚   β”œβ”€β”€β”€auth
β”‚   β”‚   └───index.ts       
β”‚   └───providers  
└───styles

So in index.ts you have the code below, the code has been expanded to support Google, GitHub, and Credentials providers.

import { AuthOptions } from 'next-auth';
import GitHubProvider from 'next-auth/providers/github';
import GoogleProvider from 'next-auth/providers/google';
import CredentialsProvider from 'next-auth/providers/credentials';
import jsonwebtoken from 'jsonwebtoken';
import { JWT } from 'next-auth/jwt';
import bcrypt from 'bcrypt';
import { createUserUsingProvider, findUserByEmail } from '../api';

// Configuration options for authentication
export const authOptions: AuthOptions = {
  providers: [
    // GitHub authentication provider configuration
    GitHubProvider({
      clientId: process.env.GITHUB_CLIENT_ID!,
      clientSecret: process.env.GITHUB_CLIENT_SECRET!,
    }),
    // Google authentication provider configuration
    GoogleProvider({
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
    }),
    // Credentials authentication provider configuration
    CredentialsProvider({
      name: 'credentials',
      credentials: {
        email: { label: 'email', type: 'text' },
        password: { label: 'password', type: 'password' },
      },
      // Custom authorization logic for credentials-based authentication
      async authorize(credentials) {
        try {
          if (!credentials?.email || !credentials?.password) {
            throw new Error('Invalid credentials');
          }
          // Find user by email from GraphQL API
          const user = await findUserByEmail(credentials?.email);
          const isCorrectPassword = await bcrypt.compare(
            credentials?.password,
            user?.password
          );
          if (!isCorrectPassword) {
            throw new Error('Invalid credentials');
          }
          if (user && isCorrectPassword) {
            return user;
          } else {
            throw new Error('User does not exist');
          }
        } catch (error) {
          console.error('Error making GraphQL request:', error);
          throw error;
        }
      },
    }),
  ],

  // Configure JSON Web Token (JWT) encoding and decoding
  jwt: {
    // Encoding function: Create and sign a JWT token
    encode: ({ secret, token }) => {
      return jsonwebtoken.sign(
        {
          ...token,
          iss: 'https://grafbase.com', // Issuer of the token
          exp: Math.floor(Date.now() / 1000) + 60 * 60 * 60, // Expiration time (1 hour from now)
        },
        secret
      );
    },

    // Decoding function: Verify a JWT token and decode its data
    decode: async ({ secret, token }) => {
      return jsonwebtoken.verify(token!, secret) as JWT;
    },
  },

  // Configure authentication callbacks
  callbacks: {
    // JWT callback: Executed whenever a JWT is created or updated
    async jwt({ profile, token }) {
      let userId;
      if (token) {
        // Find user by email from GraphQL API
        const user = await findUserByEmail(token.email!);
        if (user) {
          userId = user.id;
          token.sub = userId;
        } else {
          // Create a custom user using token data
          let customUser = {
            username: '',
            name: token.name,
            email: token.email,
            image: token.picture,
          };
          // If GitHub profile information is available, use it
          if (profile && 'avatar_url' in profile && 'login' in profile) {
            customUser.image = profile.avatar_url as string;
            token.username = profile.login as string;
          }
          // Create the user and get the user's ID
          const createdUser = await createUserUsingProvider(customUser);
          userId = createdUser;
          token.sub = userId;
        }
      }
      return token;
    },

    // Session callback: Populate session data based on JWT information
    async session({ session, token }) {
      // Find user by email from GraphQL API
      const user = await findUserByEmail(token.email!);
      if (user) {
        const userId = user.id;
        const customUser = {
          username: token.username as string | null | undefined,
          name: token.name,
          email: token.email,
          image: token.picture,
          id: userId as string | null | undefined,
        };
        session.user = customUser; // Set custom user data in the session
      }
      return session;
    },
  },
};
πŸ’‘
We will talk about the function createUserUsingProvider & findUserByEmail later after setting up our next-auth provider. But note that are they custom functions you will write later.

Explanation:
When users login or sign up on a website, the system needs to ensure they are genuine users and grant them access to the website's features. To achieve this, the system needs to know if these users are new or returning. In other words, it needs to determine if they already have an account or if they need to be registered as new users.

For instance, consider a situation where users can log in using their email and password. The system should check whether the provided email already exists in its database. If the email exists, it's likely that the user already has an account. If not, the system needs to create a new account for them.

This decision-making process is facilitated by a callback function. Think of a callback function as a piece of code that gets triggered at a specific point in the authentication process. In this case, it's invoked when a JSON Web Token (JWT) is being created for the user.

The reason for using a callback function is twofold:

  1. Customization of Token Information: The callback function gives developers the flexibility to customize the information that's included in the JWT token. This customization is important because the token's contents are used for creating a secure digital signature. This signature ensures that the token hasn't been tampered with.

  2. User Information Enrichment: Developers can use this callback to enrich the token with additional user data. For example, the token could contain the user's name, email, profile picture, and any other information that helps the system recognize and authenticate the user.

The sequence of events goes like this:

  1. A user tries to log in or sign up.

  2. The system checks if the provided email exists in its records.

  3. Before the system signs and issues the JWT token, it passes the token and other relevant information to the callback function.

  4. Inside the callback function, the system can make decisions based on whether the user's email is already known or not. It can create new user records if needed.

  5. The callback function customizes the token's contents and returns it to the system.

  6. The system finally signs the token, making it ready for use.

In essence, the callback function acts as a bridge between the user's authentication and the token creation process. It helps ensure that user data is accurately reflected in the token and that the token's integrity is maintained for secure authentication and authorization.

πŸ’‘
I wanted to share a use case for why you want to customize your token, this is unrelated, I just wanted those who are curious to understand more. So you can definitely skip this part.

Token Customization use case (Unrelated you can skip this part).

So Imagine you're building a web application using Next.js, and you're using the next-auth library to handle user authentication. This library offers a lot of flexibility, especially in how it creates and manages authentication tokens.

Now, let's say you have another part of your application that's running on a separate Node.js server. This server is responsible for handling certain protected parts of your application that require user authorization. It's like a security checkpoint that only allows users with the right credentials to access specific areas.

In order to perform this authorization check, your Node.js server needs to see if the user's request is backed by a valid "token." A token is like a special pass that Next.js creates when a user logs in. It's a way of saying, "Hey, this user is allowed in."

However, to make this system work, your Node.js server needs to understand and trust these tokens. This is where the "signature" comes in. Think of it as a digital lock on the token. The lock's pattern is created using a secret that only your application knows. If your Node.js server can unlock the token using the same pattern, it knows the token is genuine.

So, both your Next.js application and your Node.js server need to be on the same page regarding how they create these signatures. If they're not in sync, it's like trying to unlock a door with the wrong key.

When a user logs in on your Next.js app, they get this token. When they try to access a protected area on your Node.js server, they send this token along as proof of their identity. The server then uses the secret pattern to unlock the token and confirm the user's authenticity.

To make this process smooth, you need to ensure that the way tokens are created in your Next.js app matches the way they're verified in your Node.js server. This consistency ensures that everything works seamlessly. You avoid any confusion or errors that could arise if the two parts of your application had different ways of handling tokens.

To actually send the token from Next.js to your Node.js server, you can include it in the headers of the user's request or in cookies. Alternatively, you can set up a special route in your Next.js app that your server can request to get the token.

So, by keeping the token process consistent between your Next.js app and your Node.js server, you're ensuring a secure and reliable way to let users access protected parts of your application.

Setting Up Next-Auth Provider

Configuring your authentication provider with next-auth is straightforward. Just navigate to src/lib/providers/next-auth.tsx in your project directory. In this file, you'll include the following code. The purpose of this code is to ensure that any HTML element with the children prop is accessible within the scope of the next-auth provider. This allows these elements to utilize the provider's methods and functions seamlessly, without encountering any errors.

// next-auth.tsx

'use client';
import { SessionProvider } from 'next-auth/react';

type Props = {
  children?: React.ReactNode;
};

export const NextAuthProvider = (props: Props) => {
  return <SessionProvider>{props.children}</SessionProvider>;
};

In this code, the NextAuthProvider component is defined. It takes in a props object, where the children prop represents the HTML elements enclosed within this provider. The purpose of SessionProvider from next-auth/react is to manage the authentication session and make its methods and functions available to the enclosed elements.

By wrapping your application's relevant components with this NextAuthProvider, you ensure that they are within the scope of the next-auth provider. This allows them to interact with authentication features without any hiccups. The SessionProvider is responsible for seamlessly handling the session-related functionality and sharing it with your app's components.

You're about to embark on the login and registration project. To get started, navigate to your /src/app directory. Once there, you'll spot two key files: layout.tsx and page.tsx. In the layout.tsx file, we're going to envelop the next-auth provider within the <body> element like this:

// layout.tsx
import '@/styles/global.css';
import type { Metadata } from 'next';
import { LayoutRootProps } from '@/interface';
import { NextAuthProvider } from '@/lib/providers/next-auth';

export const metadata: Metadata = {
  title: 'A Comprehensive Walkthrough of Grafbase Integration with Next.js 13 for Database and Authentication-NextAuth',
  description: `So in this walkthrough, I'll take you from creating and deploying your Grafbase project from the CLI, and then integrate NextAuth within our Next.js 13 project while using Grafbase Graphql API. And in case you're worried if you don't understand GraphQL and how it works, then I'm here for you, I promise to explain thoroughly. So relax and follow along as I take you through this walkthrough.`,
};

export default function RootLayout(props: LayoutRootProps) {
  return (
    <html lang='en'>
        <NextAuthProvider>
          <body>
            <main>{props.children}</main>
          </body>
        </NextAuthProvider>
    </html>
  );
}

This strategic placement ensures that authentication-related functionality is available throughout the application.

Creating Custom Function with Grafbase GraphQL Query & Mutation - Users

During the authentication set-up, there were two functions that we used to create a user and find an existing user. They were createUserUsingProvider, and findUserByEmail

To create these functions, we need to query and mutate how graphql then return the necessary value you need. But before we continue, let's explore Grafbase Pathfinder.

Modify user modal

type User @model {
  email: Email! @unique
  name: String
  username: String!
  password: String
  image: String
}

Run your endpoint locally:

npx grafbase dev

Then head to Pathfinder: http://127.0.0.1:4000 there you should see the same screen as me:

User - Mutation

In the video, I explained the query and mutate user objects using Grafbse Pathfinder.
Here is a link for now https://youtu.be/BZ2USK9Dn_k or view it here below.

In the videos, you saw how one can create, update, delete, and query the user. But how do you do that in your Next.js application, well, it is simple, go back your Pathfinder and copy the query for the user, and it should be like this:

Query

query User {
  user(by: {email:"rockyessell@gmail.com"}) {
    name
    username
    password
    email
    id
  }
}

So to query this, let's first create a GraphQL directory, and create a file called query.ts is file will contain all our query data. And this is how the file should look like.

// graphql/query.tsx
import { gql } from '@apollo/client';

export const queryUserByEmail = gql`
  query User($email: Email!) {
    user(by: { email: $email }) {
      id
      email
      name
      username
      password
      image
    }
  }
`;


export const queryUserById = gql`
  query User($id: ID!) {
    user(by: { id: $id }) {
      id
      email
      name
      username
      password
      image
    }
  }
`;

The $email parameter in the query definition is used to make the query dynamic, allowing you to pass different email values when executing the query.

Mutation

// mutation.ts
import { gql } from '@apollo/client';

export const createUserByProviders = gql`
  mutation UserCreate(
    $email: Email!
    $username: String
    $image: String
    $name: String
  ) {
    userCreate(
      input: {
        email: $email
        username: $username
        image: $image
        name: $name
      }
    ) {
      user {
        id
      }
    }
  }
`;

export const createUserByCredential = gql`
  mutation UserCreate($email: Email!, $password: String) {
    userCreate(input: { email: $email, password: $password }) {
      user {
        id
        email
        username
        name
        image
      }
    }
  }
`;

This is how your folder structure should look like.

β”œβ”€β”€β”€app
β”‚   β”‚
β”‚   └───api
β”‚       └───auth
β”‚           └───[...nextauth]    
β”œβ”€β”€β”€graphql                     // Create i query.ts & mutation.ts
β”‚   β”œβ”€β”€β”€query.ts                // Create this file
β”‚   └───mutation.ts             // Create this file
β”œβ”€β”€β”€interface                    
└───lib
    β”œβ”€β”€β”€api                     // Here we handle function here
    β”‚   └───index.ts            // Create this file
    β”œβ”€β”€β”€auth                    // AuthOptions
    └───providers               // NextAuth Provider

With src/lib/api/index.ts created, you have your functions look like this.

import { createUserByProviders } from '@/graphql/mutation';
import { queryUserByEmail } from '@/graphql/query';
import { DocumentNode } from '@apollo/client';
import { GraphQLClient, Variables } from 'graphql-request';

export const authenticatedQuery = async (query: DocumentNode | string, variables: Variables) => {
  const headers = {
    Accept: 'application/json',
    'Content-Type': 'application/json',
    'x-api-key': process.env.GRAFBASE_API_KEY!,
  };
  const client = new GraphQLClient('http://127.0.0.1:4000/graphql', {
    headers,
  });
  const data = await client.request(query, variables);
  return data;
};

export const findUserByEmail = async (email: string) => {
  const { user } = (await authenticatedQuery(queryUserByEmail, { email })) as any;
  return user;
};

export const createUserUsingProvider = async (variables: Variables): Promise<string> => {
  const data = (await authenticatedQuery(createUserByProviders, variables)) as any;
  return data.userCreate.user.id as string;
};

// I'll leave this one for you to figure it out.
export const findUserById = async (id: string) => {};

Overall, this code provides utility functions for querying and interacting with a GraphQL API server. It handles authentication headers, makes GraphQL requests, and extracts relevant data from query and mutation responses. It seems to be part of a broader application that interacts with a GraphQL backend.

Let's break down each part of the code and understand its purpose:

  1. Import Statements:

    • It imports DocumentNode from @apollo/client, which represents a GraphQL query or mutation document.

    • It imports GraphQLClient and Variables from graphql-request library, which is used to make GraphQL requests to the server.

  2. authenticatedQuery Function:

    • This function is used to make authenticated GraphQL queries to the server.

    • It takes a query parameter (which can be a GraphQL query document or a string) and variables to be passed to the query.

    • It constructs the necessary headers, including the x-api-key header with the API key from the environment.

    • It creates a GraphQLClient instance and uses it to send a request to the specified GraphQL endpoint with the provided query and variables.

    • It returns the response data from the request.

  3. findUserByEmail Function:

    • This function is designed to retrieve user data by querying for a user using their email.

    • It uses the authenticatedQuery function to execute the queryUserByEmail query, passing the email variable.

    • It extracts and returns the user data from the query result.

  4. createUserUsingProvider Function:

    • This function is intended to create a user using provider-specific information.

    • It uses the authenticatedQuery function to execute the createUserByProviders mutation with the provided variables.

    • It extracts the user ID from the mutation result and returns it as a string.

User Authentication - Almost Done

Before you start implementing the authentication, let's handle env. Create an .env file in your parent directory, then paste the following data:

GITHUB_CLIENT_ID=""
GITHUB_CLIENT_SECRET=""

GOOGLE_CLIENT_ID=""
GOOGLE_CLIENT_SECRET=""

NEXTAUTH_SECRET="it-can-be-anything"
NEXTAUTH_URL=""

NEXT_PUBLIC_GRAFBASE_API_URL=""
GRAFBASE_API_KEY=""

Find the above information yourself. Now let's head to page.tsx in app directory.

πŸ’‘
Just a few finishing touches left! I want to emphasize that the focus was solely on authentication using Grafbase as the backend. If you take a look at the GitHub repository, you might notice some additional components, but rest assured, they aren't complex at all.

src/app/page.tsx

In your page.tsx, you will only have to create a components folder, and inside of it is a user.tsx, add the following code, I would love to explain the code below, but it will only make the article length than it already is, but I added a comment to explain, so that makes up for it.

'use client';
import React from 'react';
import Link from 'next/link';
import Image from 'next/image';
import { UserProps } from '@/interface'; // Import UserProps interface
import { signOut, useSession } from 'next-auth/react'; // Import signOut and useSession functions

const User = () => {
  // Create a ref for the user dropdown
  const userDropdownRef = React.useRef<HTMLDivElement | null>(null);
  // State to track whether user dropdown should be shown
  const [showUserDropdown, setShowUserDropdown] = React.useState(false);
  // Get user session data and status using useSession hook
  const { data: session, status } = useSession();
  // Extract user information from the session
  const user = { ...session?.user } as UserProps;
  // Toggle user dropdown visibility
  const toggleUserDropdown = () => {
    setShowUserDropdown(!showUserDropdown);
  };

  // Effect to handle clicks outside the user dropdown
  React.useEffect(() => {
    const handleOutsideClick = (event: any) => {
      // If dropdown is shown and click is outside dropdown, hide it
      if (
        showUserDropdown &&
        userDropdownRef.current &&
        !userDropdownRef.current.contains(event.target)
      ) {
        setShowUserDropdown(false);
      }
    };
    // Add click event listener for outside clicks
    document.addEventListener('click', handleOutsideClick);
    // Clean up the event listener when component unmounts
    return () => {
      document.removeEventListener('click', handleOutsideClick);
    };
  }, [showUserDropdown]);

  return (
    <>
      {status === 'authenticated' ? (
        <div className='relative'>
          <button
            onClick={toggleUserDropdown}
            type='button'
            className='z-20 rounded-full flex mx-3 text-sm bg-gray-800  md:mr-0 focus:ring-4 focus:ring-gray-300'
          >
            <span className='sr-only'>Open user menu</span>

            {/* Display user image */}
            {user.image ? (
              <Image
                src={user.image}
                width={50}
                height={50}
                className='w-full h-full object-cover object-center  rounded-full'
                alt={user.name}
                priority
              />
            ) : (
              <Image
                src={'/248387.jpg'}
                width={50}
                height={50}
                className='w-12 h-12 object-cover object-center rounded-full'
                alt={'248387'}
                priority
              />
            )}
          </button>
          {/* Show user dropdown if enabled */}
          {showUserDropdown && (
            <div
              ref={userDropdownRef}
              className='absolute top-[3.6rem] left-12 right-0 mt-3 mr-2 w-56 text-base list-none bg-white rounded divide-y divide-gray-100 shadow'
            >
              {/* Display user name and email */}
              <div className='py-3 px-4 text-gray-500'>
                <span className='block text-sm font-semibold'>
                  {user?.name}
                </span>
                <span className='block text-sm font-light truncate '>
                  {user?.email}
                </span>
              </div>
              {/* Display sign out option */}
              <ul className='py-1 font-light text-gray-500 cursor-pointer'>
                <li
                  onClick={() => signOut()}
                  className='block py-2 px-4 text-sm'
                >
                  Sign out
                </li>
              </ul>
            </div>
          )}
        </div>
      ) : (
        <div className='relative'>
          <button
            onClick={toggleUserDropdown}
            type='button'
            className='z-20 rounded-full border-2 border-black flex mx-3 text-sm bg-gray-800  md:mr-0 focus:ring-4 focus:ring-gray-300'
          >
            <span className='sr-only'>Open user menu</span>
            {/* Display default avatar */}
            <Image
              src={'/avator.png'}
              width={50}
              height={50}
              className='w-full h-full object-cover object-center rounded-full'
              alt={'Avator'}
              priority
            />
          </button>
          {/* Show user dropdown if enabled */}
          {showUserDropdown && (
            <div
              ref={userDropdownRef}
              className='absolute top-[3.6rem] left-12 right-0 mt-3 mr-2 w-56 text-base list-none bg-white rounded divide-y divide-gray-100 shadow'
            >
              {/* Display link to authentication */}
              <ul className='py-1 font-light text-gray-500 cursor-pointer'>
                <li className='block py-2 px-4 text-sm'>
                  <Link href={`/account`}>Authenticate</Link>
                </li>
              </ul>
            </div>
          )}
        </div>
      )}
    </>
  );
};

export default User;

Now it is left with one more component, and that's the sign page. In the above, you can see we are redirecting the user to `/account` , so create an account folder in the app directory, with a file called page.tsx then paste the following code here.

'use client';
import React from 'react';
import axios from 'axios';
import Image from 'next/image';
import { signIn } from 'next-auth/react';
import { useRouter } from 'next/navigation';
import { useSession } from 'next-auth/react';
import { FaGithub, FaGoogle } from 'react-icons/fa';

// Initial form values
const initialFormValue = { email: '', password: '' };

const Account = () => {
  const router = useRouter(); // Initialize the useRouter hook
  const { status } = useSession(); // Retrieve the authentication status using the useSession hook

  // React effect to redirect after authentication status changes
  React.useEffect(() => {
    // Check if the authentication status is 'authenticated'
    if (status === 'authenticated') {
      // Redirect to the homepage when the user is authenticated
      router.push('/');
    }
  }, [router, status]);

  // State to hold form values
  const [form, setForm] =
    React.useState<typeof initialFormValue>(initialFormValue);

  // Handler for form input changes
  const handleFormChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    const { target } = event;
    setForm((initialValue) => ({
      ...initialValue,
      [target.name]: target.value,
    }));
  };

  // Handler for form submission
  const handleSubmission = async (event: React.SyntheticEvent) => {
    event.preventDefault();

    if (form) {
      // Make a POST request to register user
      await axios.post('/api/register', { ...form });

      // Sign in using credentials and bypass redirection
      await signIn('credentials', {
        ...form,
        redirect: false,
      });
    }
  };

  // Component rendering
  return (
    <main className='flex min-h-screen flex-col items-center justify-between p-24'>
      <section className='flex flex-col'>
        <div className='flex items-center justify-center'>
          <div className='min-w-fit flex-col border bg-white px-6 py-14 shadow-md rounded-[4px] '>
            <div className='mb-8 flex flex-col items-center justify-center'>
              <Image
                src='/grafbase.svg'
                width={50}
                height={50}
                alt='Grafbase'
              />
              <p className='font-rathetta'>Grafbase</p>
            </div>
            <form onSubmit={handleSubmission}>
              <fieldset className='flex flex-col text-sm rounded-md'>
                {/* Email input */}
                <input
                  className='mb-5 rounded-[4px] border p-3 hover:outline-none focus:outline-none hover:border-yellow-500 '
                  type='text'
                  placeholder='Username or Email id'
                  value={form.email}
                  onChange={handleFormChange}
                  name='email'
                />
                {/* Password input */}
                <input
                  className='border rounded-[4px] p-3 hover:outline-none focus:outline-none hover:border-yellow-500'
                  type='password'
                  placeholder='Password'
                  value={form.password}
                  onChange={handleFormChange}
                  name='password'
                />
              </fieldset>
              {/* Sign-in button */}
              <button
                className='mt-5 w-full border p-2 bg-gradient-to-r from-gray-800 bg-gray-500 text-white rounded-[4px] hover:bg-slate-400 scale-105 duration-300'
                type='submit'
              >
                Sign in
              </button>
            </form>
            {/* Forgot password and sign-up links */}
            <div className='mt-5 flex justify-between text-sm text-gray-600'>
              <a href='#'>Forgot password?</a>
              <a href='#'>Sign up</a>
            </div>
            {/* Social login options */}
            <div className='flex justify-center mt-5 text-sm'>
              <div className='inline-flex items-center justify-center w-full'>
                <hr className='w-64 h-px my-8 bg-gray-200 border-0 dark:bg-gray-700' />
                <span className='absolute px-3 font-medium -translate-x-1/2 bg-white left-1/2 dark:text-white dark:bg-gray-900'>
                  or
                </span>
              </div>
            </div>
            <div className='mt-2.5 flex justify-center gap-3    '>
              {/* GitHub login */}
              <FaGithub
                onClick={() => signIn('github')}
                className='w-10 h-10 p-1 rounded-md bg-gray-500 text-white text-2xl'
              />
              {/* Google login */}
              <FaGoogle
                onClick={() => signIn('google')}
                className='w-10 h-10 p-1 rounded-md bg-gray-500 text-white text-2xl'
              />
            </div>
            {/* Privacy and terms */}
            <div className='mt-5 flex text-center text-sm text-gray-400'>
              <p>
                This site is protected by reCAPTCHA and the Google <br />
                <a className='underline' href=''>
                  Privacy Policy
                </a>
                and
                <a className='underline' href=''>
                  Terms of Service
                </a>
                apply.
              </p>
            </div>
          </div>
        </div>
      </section>
    </main>
  );
};

export default Account;

The above code is a page users can authenticate with, then get redirected to the homepage, where their profile is displayed.

Finally in page.tsx in the app directory, you just need to add your User components and you're done. So here is how page.tsx should be:

import React from 'react';
import Image from 'next/image';
import User from '@/components/user';

export default function Home() {
  return (
    <main className='flex min-h-screen flex-col items-center justify-between p-24'>
      <div className='z-10 max-w-5xl w-full items-center justify-between font-mono text-sm lg:flex'>
        <div className='inline-flex items-center gap-1'>
          <User /> // added your hUser components herer.
          <p className='fixed left-0 top-0 flex w-full justify-center border-b border-gray-300 bg-gradient-to-b from-zinc-200 pb-6 pt-8 backdrop-blur-2xl dark:border-neutral-800 dark:bg-zinc-800/30 dark:from-inherit lg:static lg:w-auto  lg:rounded-xl lg:border lg:bg-gray-200 lg:p-4 lg:dark:bg-zinc-800/30'>
            Get started by editing&nbsp;
            <code className='font-mono font-bold'>src/app/page.tsx</code>
            ........
// Other code                
    </main>
  );
}

Done!!!!!!

Conclusion

The overarching message of the walkthrough encourages developers to embark on this journey with a sense of assurance. By elucidating the potential of Grafbase and demonstrating its practical implementation, the walkthrough fosters an environment of learning and exploration. As the software development landscape continues to evolve, platforms like Grafbase play a pivotal role in equipping developers with the tools they need to build sophisticated applications effectively and efficiently.

πŸ’‘
Here is the GitHub link to the source code. Demo

#grafbasehackathon #grafbase

15
Subscribe to my newsletter

Read articles from Rocky Essel directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Rocky Essel
Rocky Essel

πŸ‘©πŸΎβ€πŸ’»