Skip to main content

Command Palette

Search for a command to run...

(Flexibble) Full Stack Next.js application built using Grafbase and NextAuth.js

Published
•16 min read
(Flexibble) Full Stack Next.js application built using Grafbase and NextAuth.js

About Flexibble

The main idea behind Flexibble is to build a platform for developers to showcase their remarkable work in the community, but also to uncover a wellspring of inspiration from fellow projects and get hired.

Content

In this blog post, I will try to cover the major parts on how to set up Grafbase in your own project, use Authentication with Grafbase and NextAuth.js, and show how Grafbase makes our lives easy after initial setup and spins up all the queries and mutation (which we can explore using Grafbase Pathfinder).

📢
I will be only touching up the code which involves Grafbase setup, Authentication with Grafbase and NextAuth.js, and queries and mutations using GraphQL.
📢
I will be not going through the folder structure, initial setup of the Next.js project along with its features, components, styles, or any other things in this blog post.

Functionalities of Project

  • Login & Signup using Google which is provided by NextAuth.js

  • CRUD (Create, Read, Update, Delete) operations on the project

  • Functionality to view a particular User's project or profile

Technologies Used

  1. Next.js

  2. Grafbase

  3. NextAuth.js

The thought process of building up the Project (just the Grafbase & NextAuth.js part)

  • Part-1: Setting up the Grafbase account, database, and the major configurations

  • Part-2: The whole setup of Authentication with NextAuth.js and Grafbase

  • Part-3: Creating the main operation functionalities of the project using Grafbase PathFinder


Setting up Grafbase ( Part-1)

  1. Go to Grafbase's official website (https://grafbase.com/) and signup if you don't have an account or log in if you already have one.

  2. Initially, you will see an empty dashboard with no projects. That's the first step in setting up the whole thing.

  3. Now install the Grafbase SDK into your project by going into the terminal and typing the command npm i @grafbase/sdk , this will install the Grafbase SDK into the project as a Dev Dependency.

  4. Go to your local project and in the terminal use the following command npx grafbase init --config-format typescript to set up Grafbase for your project.

  5. You will see a new folder being created named grafbase in your project folder structure.

  6. In the grafbase folder you will have a .env file and a grafbase.config.ts file .

  7. You will have to open up the grafbase.config.ts file and make the configurations there, By default it will have a boilerplate code for configurations, you can modify the code according to your choice.

    You can refer to these resources to do configurations according to your needs:

    ---> https://grafbase.com/docs/database
    ---> https://www.npmjs.com/package/@grafbase/sdk

  8. We can set up all the Models, Schemas, and Authentication in the grafbase.config.ts file and Grafbase will take care of everything, and it will even provide suggestions for queries and mutations which can be used in functionalities. We will see it in the later part of this blog.

  9. If we try to condense our whole project and identify the models, then it has two models which are mainly "User" Model to take care of users who signup and the "Project" Model to store information about the projects, here is how my grafbase.config.ts file looks

    Link: https://github.com/reddymahendra52/Flexibble-Grafbase/blob/main/grafbase/grafbase.config.ts

  10. import { g, auth, config } from "@grafbase/sdk";
    
    // @ts-ignore
    const User = g
      .model("User", {
        name: g.string().length({ min: 2, max: 20 }),
        email: g.string().unique(),
        avatarUrl: g.url(),
        description: g.string().optional(),
        githubUrl: g.url().optional(),
        linkedinUrl: g.url().optional(),
        projects: g
          .relation(() => Project)
          .list()
          .optional(),
      })
      .auth((rules) => {
        rules.public().read();
      });
    
    // @ts-ignore
    const Project = g
      .model("Project", {
        title: g.string().length({ min: 3 }),
        description: g.string(),
        image: g.url(),
        liveSiteUrl: g.url(),
        githubUrl: g.url(),
        category: g.string().search(),
        createdBy: g.relation(() => User),
      })
      .auth((rules) => {
        rules.public().read(), rules.private().create().delete().update();
      });
    
    const jwt = auth.JWT({
      issuer: "grafbase",
      secret: g.env("NEXTAUTH_SECRET"),
    });
    
    export default config({
      schema: g,
      auth: {
        providers: [jwt],
        rules: (rules) => rules.private(),
      },
    });
    

    'g' is a schema generator using which we are creating two Models which are "User" and "Project", the fields show the attributes of the models with validators chained along them are self-explanatory.

  11. One thing here we can take note is of the .relation() which binds the two models:
    a) g.relation(() => Project).list().optional() in the "User" Model tells that the "User" Model has a relation with "Project" Model ( that is user creates projects in the DB), which is a list of projects and it is optional as initially, a new user sign's up, projects the user has to show is null.

    b) g.relation(() => User) in the "Project" Model simply means that a oroject is binded by the user who creates the project.

  12. We will look at the auth part in Part-2

  13. After following the above steps the basic setup is done locally for the Grafbase next, we have to create a new GitHub repository and push the code with the setup done till now to the repository.

  14. Now, go to the Grafbase website, and go to your dashboard. As of now it should be empty. You can a new project by clicking on the create project. After that import the repository from GitHub to Grafbase.

  15. After importing the project from the repository, the project is connected to the Grafbase and you can see it on the dashboard. Click on the connect button which would be visible on the top-right. On clicking it you can see the Grafbase API_Endpoint and API_KEY which you can save for now on the notepad, we will use these to connect to the database hosted on the cloud when the project is live and hosted.


NextAuth and Grafbase (Part-2)

Authentication is the process that ensures only authorized individuals gain access to sensitive systems and data, shielding against unauthorized entry.

So let's start...
Before we move forward let me show you the layout.tsx file of the project. The whole Authentication part is set up in the Navbar for my project.

Link: https://github.com/reddymahendra52/Flexibble-Grafbase/blob/main/app/layout.tsx

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        <Navbar />
        <main>{children}</main>
        <Footer />
      </body>
    </html>
  );
}
  1. I am using NextAuth.js (https://next-auth.js.org/) for authentication and the terms used from now on can be related to the library. So from now on if you don't see a familiar term please visit the following links for more information:

    ---> https://next-auth.js.org/getting-started/introduction
    ---> https://www.npmjs.com/package/next-auth

  2. Now install the next-auth library into your project by going into the terminal and typing the command npm install next-auth .

  3. As for the folks who have used Next.js previously know that using <Navbar/> in the layout.tsx file makes it common for all the routes and pages. So in the <Navbar/> we are looking for a session which we get from the auth providers when the user is logged in. To get that session we call the getServerSession. If the session is not available it means the user is not signed in. If the user is not signed we can show the list of authentication providers (in our case which would be Google )

  4. A snippet from <Navbar/> components showing the above functionality

    Link-1: https://github.com/reddymahendra52/Flexibble-Grafbase/blob/main/components/Navbar.tsx

    Link-2: https://github.com/reddymahendra52/Flexibble-Grafbase/blob/main/lib/session.ts

const session = await getCurrentUser();

<div className="flexCenter gap-4">
        {session?.user ? (
          <>
            <ProfileMenu session={session} />
            {console.log(session)}
            <Link href="/create-project">Share Work</Link>
          </>
        ) : (
          <AuthProviders />
        )}
</div>
export async function getCurrentUser() {
  const session = (await getServerSession(authOptions)) as SessionInterface;

  return session;
}
  1. Here the <AuthProviders/> component is nothing but a component that just iterates over the list of auth providers that we are providing and shows it to the user.

  2. Before we start using the session and functionality we have to set up a few things.

    a) in the app/api/auth/[...nextauth]/route.ts we have to write the following:

    Link: https://github.com/reddymahendra52/Flexibble-Grafbase/blob/main/app/api/auth/%5B...nextauth%5D/route.ts

     import NextAuth from "next-auth/next";
     import { authOptions } from "@/lib/session";
    
     const handler = NextAuth(authOptions);
    
     export { handler as GET, handler as POST };
    

    Here we are importing NextAuth and passing in all the authOptions which we create in the session.ts file which I will discuss below.

    b) in the app/api/auth/token/route.ts we have to write the following:

    Link:https://github.com/reddymahendra52/Flexibble-Grafbase/blob/main/app/api/auth/token/route.ts

     import { getToken } from "next-auth/jwt";
     import { NextRequest, NextResponse } from "next/server";
    
     const secret = process.env.NEXTAUTH_SECRET;
    
     export async function GET(req: NextRequest) {
       const token = await getToken({ req, secret, raw: true });
    
       return NextResponse.json({ token }, { status: 200 });
     }
    

    Here we are looking for the token along with the secret to encode and decode in the process of authentication, the NEXTAUTH_SECRET can be created by following the steps mentioned here in this link:

    ---> https://next-auth.js.org/configuration/options#nextauth_secret

    You can use the following tool to execute the command mentioned in the reference link above:

    ---> https://www.cryptool.org/en/cto/openssl

  3. Now after all the nitty-gritty, we can discuss the important session.ts file where all the magic happens.

    Link: https://github.com/reddymahendra52/Flexibble-Grafbase/blob/main/lib/session.ts

     import { getServerSession } from "next-auth/next";
     import { NextAuthOptions, User } from "next-auth";
     import GoogleProvider from "next-auth/providers/google";
     import jsonwebtoken from "jsonwebtoken";
     import { JWT } from "next-auth/jwt";
     import { SessionInterface, UserProfile } from "@/commons.types";
     import { createUser, getUser } from "./actions";
    
     export const authOptions: NextAuthOptions = {
       providers: [
         GoogleProvider({
           clientId: process.env.GOOGLE_CLIENT_ID!,
           clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
         }),
       ],
       jwt: {
         encode: ({ secret, token }) => {
           const encodedToken = jsonwebtoken.sign(
             {
               ...token,
               iss: "grafbase",
               exp: Math.floor(Date.now() / 1000) + 60 * 60,
             },
             secret
           );
    
           return encodedToken;
         },
         decode: async ({ secret, token }) => {
           const decodedToken = jsonwebtoken.verify(token!, secret) as JWT;
    
           return decodedToken;
         },
       },
       theme: {
         colorScheme: "light",
         logo: "/logo.png",
       },
       callbacks: {
         async session({ session }) {
           const email = session?.user?.email as string;
    
           try {
             const data = (await getUser(email)) as { user?: UserProfile };
    
             console.log("this is the session where new one is created");
             console.log(data);
    
             const newSession = {
               ...session,
               user: {
                 ...session.user,
                 ...data?.user,
               },
             };
    
             return newSession;
           } catch (error) {
             console.log("Error retrieving user data", error);
             return session;
           }
         },
         async signIn({ user }: { user: User }) {
           try {
             // get the user if they exist
             const userExists = (await getUser(user?.email as string)) as {
               user?: UserProfile;
             };
    
             // if don't exist create them
             if (!userExists.user) {
               await createUser(
                 user.name as string,
                 user.email as string,
                 user.image as string
               );
    
               console.log("user successfully created");
             }
    
             return true;
           } catch (error: any) {
             console.log(error);
             return false;
           }
         },
       },
     };
    
     // asking for the session
     export async function getCurrentUser() {
       const session = (await getServerSession(authOptions)) as SessionInterface;
    
       return session;
     }
    
  4. The getCurrentUser() is created here and passed to the <Navbar/> component to be used. Here as we are using Google Provider, we have to pass in GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET for the Google Provider to work.

    You can acquire them by going through this guide:
    https://www.balbooa.com/gridbox-documentation/how-to-get-google-client-id-and-client-secret

    That is for providers, as we are passing GoogleProvider. You can pass in multiple providers. You can learn all about them and how to set them up by looking at these docs:
    https://next-auth.js.org/configuration/providers/oauth

  5. After that, we are working on jwt, where we are doing the encode and decode process. And in the process, we are using a package called jsonwebtoken you can install it using the command npm install jsonwebtoken .

    While doing so we are also informing that our issuer is Grafbase.
    We connect this to our Grafbase config setup by this snippet.

    Link: https://github.com/reddymahendra52/Flexibble-Grafbase/blob/main/grafbase/grafbase.config.ts

     const jwt = auth.JWT({
       issuer: "grafbase",
       secret: g.env("NEXTAUTH_SECRET"),
     });
    

    Which can be found above. Here also the NEXTAUTH_SECRET used is the same as the one created above.

    It is also advised to include it in the .env file inside the grafbase folder which is created at the start so that it has access.

  6. After jwt the most important option is the callbacks inside which we have session and sigIn. This is the same session that is available everywhere. Before we touch sessions let's cover the signIn.

  7. In the signIn we can see that, we are following a pattern where if a user exists, get that user from the Grafbase database using getUser query, and if the user doesn't exists create a new user using createUser mutation to the Grafbase database. If an error occurs log it.

  8. the getUser query and createUser mutation would be explained in Part-3 and how Grafbase makes it so easy for us

  9. From the session provided by the provider which is Google Provider we can extract the email and from the email, we can query the Grafbase database. If the user exists or not then get the details from the Grafbase database append them to the current session and return newsession.

  10. This is the whole process of authentication where the major behind-the-scenes work is done by the next-auth, and provides us with an easy abstraction with all the features

  11. The whole auth setup is done.
    If you could recall the initial config where we set up some auth using .auth() that's a feature provided by Grafbaase to make our lives easy telling us what part is accessible (public) and what part is not-accessible (private).

    Link: https://github.com/reddymahendra52/Flexibble-Grafbase/blob/main/grafbase/grafbase.config.ts

    a) .auth((rules) => { rules.public().read(); }); on Users means that the profiles are visible and users can read them if they are not authenticated also.

    b) .auth((rules) => { rules.public().read(), rules.private().create().delete().update(); }); on Project means that the projects are read-only if not authenticated, and if authenticated then the user can create, delete and update.

  12. With just a few lines of code and minimum files, we were able to set up the full authentication for the project using NextAuth.js and Grafbase. The combination of NextAuth.js & Grafbase is very powerful for full-stack projects


Creating Queries & Mutations (Part-3)

As of now, we have done all the hard work of setting up Grafbase on our project and secured our project with authentication using both Grafbase and NextAuth.js. Now it's time to reap the rewards of all that initial setup.

We finally move into the part where we focus on the major functionalities of creating users and performing all the CRUD operations on the projects and also querying both users and projects.

So Let's dive in....

  1. Let's install the graphql-request library into your project by going into the terminal and typing the command npm i graphql-request
    Can read more about it here:
    https://www.npmjs.com/package/graphql-request

  2. So in the folder structure, I am storing all the raw GraphQL queries in the graphql folder and all the functions calling those queries in the actions.ts file

    Link-1: https://github.com/reddymahendra52/Flexibble-Grafbase/blob/main/graphql/index.ts

    Link-2: https://github.com/reddymahendra52/Flexibble-Grafbase/blob/main/lib/actions.ts

  3. Here is how the action.ts file looks

     import { ProjectForm } from "@/commons.types";
     import {
       createProjectMutation,
       createUserMutation,
       deleteProjectMutation,
       getProjectByIdQuery,
       getProjectsOfUserQuery,
       getUserQuery,
       projectsQuery,
       updateProjectMutation,
     } from "@/graphql";
     import { GraphQLClient } from "graphql-request";
    
     const isProduction = process.env.NODE_ENV === "production";
     const apiURL = isProduction
       ? process.env.NEXT_PUBLIC_GRAFBASE_API_URL || ""
       : "http://127.0.0.1:4000/graphql";
     const apiKey = isProduction
       ? process.env.NEXT_PUBLIC_GRAFBASE_API_KEY || ""
       : "letmein";
     const serverUrl = isProduction
       ? process.env.NEXT_PUBLIC_SERVER_URL
       : "http://localhost:3000";
    
     const client = new GraphQLClient(apiURL);
    
     const makeGraphQLRequest = async (query: string, variables = {}) => {
       try {
         return await client.request(query, variables);
       } catch (error) {
         throw error;
       }
     };
    
     export const getUser = (email: string) => {
       client.setHeader("x-api-key", apiKey);
       return makeGraphQLRequest(getUserQuery, { email });
     };
    
     export const createUser = (name: string, email: string, avatarUrl: string) => {
       client.setHeader("x-api-key", apiKey);
       const variables = {
         input: {
           name,
           email,
           avatarUrl,
         },
       };
    
       return makeGraphQLRequest(createUserMutation, variables);
     };
    
     export const fetchToken = async () => {
       try {
         const response = await fetch(`${serverUrl}/api/auth/token`);
         return response.json();
       } catch (err) {
         throw err;
       }
     };
    
     export const uploadImage = async (imagePath: string) => {
       try {
         const response = await fetch(`${serverUrl}/api/upload`, {
           method: "POST",
           body: JSON.stringify({
             path: imagePath,
           }),
         });
         return response.json();
       } catch (err) {
         throw err;
       }
     };
    
     export const createNewProject = async (
       form: ProjectForm,
       creatorId: string,
       token: string
     ) => {
       const imageUrl = await uploadImage(form.image);
    
       if (imageUrl.url) {
         client.setHeader("Authorization", `Bearer ${token}`);
    
         const variables = {
           input: {
             ...form,
             image: imageUrl.url,
             createdBy: {
               link: creatorId,
             },
           },
         };
    
         return makeGraphQLRequest(createProjectMutation, variables);
       }
     };
    
     export const fetchAllProjects = (category?: string, endcursor?: string) => {
       client.setHeader("x-api-key", apiKey);
    
       return makeGraphQLRequest(projectsQuery, { category, endcursor });
     };
    
     export const getProjectDetails = (id: string) => {
       client.setHeader("x-api-key", apiKey);
       return makeGraphQLRequest(getProjectByIdQuery, { id });
     };
    
     export const getUserProjects = (id: string, last?: number) => {
       client.setHeader("x-api-key", apiKey);
       return makeGraphQLRequest(getProjectsOfUserQuery, { id, last });
     };
    
     export const deleteProject = (id: string, token: string) => {
       client.setHeader("Authorization", `Bearer ${token}`);
       return makeGraphQLRequest(deleteProjectMutation, { id });
     };
    
     export const updateProject = async (
       form: ProjectForm,
       projectId: string,
       token: string
     ) => {
       // to check if the user updated the image or not
       // or only updated the information
       function isBase64DataURL(value: string) {
         const base64Regex = /^data:image\/[a-z]+;base64,/;
         return base64Regex.test(value);
       }
    
       let updatedForm = { ...form };
    
       const isUploadingNewImage = isBase64DataURL(form.image);
    
       // upload to cloudinary
       if (isUploadingNewImage) {
         const imageUrl = await uploadImage(form.image);
    
         if (imageUrl.url) {
           updatedForm = { ...updatedForm, image: imageUrl.url };
         }
       }
    
       client.setHeader("Authorization", `Bearer ${token}`);
    
       const variables = {
         id: projectId,
         input: updatedForm,
       };
    
       return makeGraphQLRequest(updateProjectMutation, variables);
     };
    

    At the top, we can see I am importing all the raw queries which will be discussed immediately below. Initially, we can see all the environment variables which needs to be used, we make a set of variables to use for production and other to use locally, it is one of the best practices followed in the industry.

  4. makeGraphQLRequest() is a generalized function that we will reuse again and again in the below functions, it takes in two parameters which are the query and the other one is variables (the input values which we pass), the query is imported and passed.

  5. All the functions are self-explanatory. We can see the function names attributed to what it does and the respected query and variables being passed, they show standard CRUD operations.

  6. As I mentioned earlier, you might be thinking what does Grafbase do here? So let me explain:

    Grafbase has Pathfinder which can be found locally when you run the following command npx grafbase dev on http://localhost:4000 or on the project dashboard.

  7. The Pathfinder feature takes a look at our config file and based on our schema and models automatically populates all the possible queries and mutations which we can create.

    Which might look like this as shown below:

    As we can see these are probably all the possible mutations and queries we can find, and on clicking on each Query on PathFinder, you can create the document of what you want with filters and lot of other options.

    Using this you can very easily write Queries without breaking your head.

    Here is an example:

  8. Using Grafbase and with the help of Pathfinder all the queries and mutations are written for the project, which is as follows:

     export const createProjectMutation = `
         mutation CreateProject($input: ProjectCreateInput!) {
             projectCreate(input: $input) {
                 project {
                     id
                     title
                     description
                     createdBy {
                         email
                         name
                     }
                 }
             }
         }
     `;
    
     export const getUserQuery = `
         query GetUser($email: String!){
             user(by: {email: $email}){
                 id
                 name
                 email
                 avatarUrl
                 description
                 githubUrl
                 linkedinUrl
             }
         }
     `;
    
     export const createUserMutation = `
         mutation CreateUser($input: UserCreateInput!){
             userCreate(input: $input){
                 user{
                     name
                     email
                     avatarUrl
                     description
                     githubUrl
                     linkedinUrl
                     id
                 }
             }
         }
     `;
    
     export const projectsQuery = `
       query getProjects() {
         projectSearch(first:10) {
           pageInfo {
             hasNextPage
             hasPreviousPage
             startCursor
             endCursor
           }
           edges {
             node {
               title
               githubUrl
               description
               liveSiteUrl
               id
               image
               category
               createdBy {
                 id
                 email
                 name
                 avatarUrl
               }
             }
           }
         }
       }
     `;
    
     export const getProjectByIdQuery = `
       query GetProjectById($id: ID!) {
         project(by: { id: $id }) {
           id
           title
           description
           image
           liveSiteUrl
           githubUrl
           category
           createdBy {
             id
             name
             email
             avatarUrl
           }
         }
       }
     `;
    
     export const getProjectsOfUserQuery = `
       query getUserProjects($id: ID!, $last: Int = 4) {
         user(by: { id: $id }) {
           id
           name
           email
           description
           avatarUrl
           githubUrl
           linkedinUrl
           projects(last: $last) {
             edges {
               node {
                 id
                 title
                 image
               }
             }
           }
         }
       }
     `;
    
     export const deleteProjectMutation = `
       mutation DeleteProject($id: ID!) {
         projectDelete(by: { id: $id }) {
           deletedId
         }
       }
     `;
    
     export const updateProjectMutation = `
         mutation UpdateProject($id: ID!, $input: ProjectUpdateInput!) {
             projectUpdate(by: { id: $id }, input: $input) {
                 project {
                     id
                     title
                     description
                     createdBy {
                         email
                         name
                     }
                 }
             }
         }
     `;
    

That's a wrap for this blog post, hope I was able to cover at least a few things regarding the above topic. I tried my best to put into words what I wanted to convey, if the readers can even take away a little bit of knowledge from this blog, I would be more than happy. Thank You!!

  • Thanks for taking out your time and reading. 😄

📢
A huge shoutout to JavaScript Mastery and Adrian Hajdin for such amazing content on his youtube channel JavaScipt Mastery, from which I have taken reference to build this one.