Working with GraphQL and Next.js 13 React Server Components

Next.js recently announced experimental support for a new app directory. While users can continue to use the pages directory for building applications, those curious on the future of Next.js can experiment with new data fetching hooks, server components, and long-awaited layout support.

This guide assumes you have a Next.js 13 app setup and using the new experimental app directory. If you haven't, you will need to add the following to your next.config.js to get going:

/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  swcMinify: true,
  experimental: { appDir: true }
}

module.exports = nextConfig

Then inside of the root of your project create the file app/page.tsx and add the following:

const Page = async () => <h1>Grafbase is awesome!</h1>

export default Page

If you now run the Next.js dev server (npm run dev) then you will see the h1 we created above! You will also notice that if you haven't already got the file app/layout.tsx then one will be created for you at this point.

const RootLayout = async ({ children }: { children: React.ReactNode }) => {
  return (
    <html lang="en">
      <head></head>
      <body></body>
    </html>
  )
}

export default RootLayout

If you've worked with a custom pages/_document.tsx before then this will feel very familiar.

If you've a Grafbase backend already setup then you can skip this step. For those curious, you can initialize a new Grafbase project by using a single command:

npx grafbase init

This will create the file grafbase/schema.graphql in the root of your project.

You will want to open this and replace the contents with the following schema to follow along with the rest of this guide:

type Post @model {
  id: ID!
  title: String!
  slug: String! @unique
  views: Int @default(value: 0)
}

While we could use plain old fetch to make a request to our backend, we'll use the graphql-request package to abstract a few things around parsing JSON, errors, and the arguments we'd pass to fetch.

npm install graphql-request graphql

Next we'll create the file lib/grafbase.ts in the root of our project. Inside here you will want to import GraphQLClient from graphql-request and instantiate a new client for your Grafbase backend.

It's recommended you use environment variables here so that this will work seamlessly with the Grafbase CLI, and when you deploy.

import { GraphQLClient } from 'graphql-request'

export { gql } from 'graphql-request'

export const grafbase = new GraphQLClient(
  process.env.GRAFBASE_API_URL as string,
  {
    headers: {
      'x-api-key': process.env.GRAFBASE_API_KEY as string
    }
  }
)

Then inside .env for running locally you will want to run the following:

GRAFBASE_API_URL=http://localhost:4000/graphql
GRAFBASE_API_KEY=

We can use the grafbase client we exported from lib/grafbase.ts inside of our app layouts and pages.

The files app/layout.tsx and app/page.tsx run on the server. We can inside of these files import our grafbase client and make a request to our backend.

import { gql, grafbase } from '../../lib/grafbase'

const GetAllPostsQuery = gql`
  query GetAllPosts($first: Int!) {
    postCollection(first: $first) {
      edges {
        node {
          id
          title
          slug
        }
      }
    }
  }
`

const RootLayout = async ({ children }: { children: React.ReactNode }) => {
  const { postCollection } = await grafbase.request(GetAllPostsQuery, {
    first: 10
  })

  return (
    <html lang="en">
      <head></head>
      <body>
        <main>{children}</main>
      </body>
    </html>
  )
}

export default RootLayout

You'll notice in your browser if you visit the page that nothing changed. You'll see no requests in the network activity because this happened on the server.

Next we'll render a list of links to our posts:

import Link from 'next/link'
import { gql, grafbase } from '../../lib/grafbase'

const GetAllPostsQuery = gql`
  query GetAllPosts($first: Int!) {
    postCollection(first: $first) {
      edges {
        node {
          id
          title
          slug
        }
      }
    }
  }
`

const RootLayout = async ({ children }: { children: React.ReactNode }) => {
  const { postCollection } = await grafbase.request(GetAllPostsQuery, {
    first: 10
  })

  return (
    <html lang="en">
      <head></head>
      <body>
        <nav>
          <ul>
            {postCollection?.edges?.map(edge =>
              edge?.node ? (
                <li key={edge.node.id}>
                  <Link href={`/posts/${edge.node.slug}`}>
                    {edge.node.title}
                  </Link>
                </li>
              ) : null
            )}
          </ul>
        </nav>
        <main>{children}</main>
      </body>
    </html>
  )
}

export default RootLayout

The changes above will now render a list of links to each of your post pages.

We will now finish by creating the individual pages for posts and the post slug as a dynamic param in the URL.

Create the file app/posts/[slug]/page.tsx and add the following:

const Page = async ({ params }: { params: { slug: string } }) => {
  return (
    <>
      <h1>Post title</h1>
    </>
  )
}

export default Page

To fetch data from the Grafbase backend on the server we will import the grafbase client and execute a query like we did for the app/layout.tsx.

import { gql, grafbase } from '../../../lib/grafbase'

const GetPostBySlugQuery = gql`
  query GetPostBySlug($slug: String!) {
    post(by: { slug: $slug }) {
      id
      title
      slug
    }
  }
`

const Page = async ({ params }: { params: { slug: string } }) => {
  const { post } = await grafbase.request(GetPostBySlugQuery, {
    slug: params.slug
  })

  return (
    <>
      <h1>{post.title}</h1>
      <pre>{JSON.stringify(post, null, 2)}</pre>
    </>
  )
}

export default Page

You may want to show the default Next.js 404 page by invoking notFound() or rendering something custom:

const Page = async ({ params }: { params: { slug: string } }) => {
  const { post } = await grafbase.request(GetPostBySlugQuery, {
    slug: params.slug
  })

  if (!post) {
    // optionally import { notFound } from 'next/navigation'
    // notFound()
    return <h1>404: Not Found</h1>
  }

  return (
    <>
      <h1>{post.title}</h1>
      <pre>{JSON.stringify(post, null, 2)}</pre>
    </>
  )
}

That's it! You have successfully fetched data on the server with Next.js 13 and the new app directory!

Next.js extends the native fetch API to give users greater caching control of requests. Here we can make the same query we did above to our backend but instead using the built-in fetch library:

const Page = async ({ params }: { params: { slug: string } }) => {
  const { data } = await fetch(process.env.GRAFBASE_API_URL, {
    method: 'POST',
    headers: {
      'content-type': 'application/json',
      'x-api-key': process.env.GRAFBASE_API_KEY
    },
    body: JSON.stringify({
      query: `
        query GetPostBySlug($slug: String!) {
          post(by: { slug: $slug }) {
            id
            title
            slug
          }
        }
      `,
      variables: { slug: params.slug }
    })
  })

  if (!data?.post) {
    return <h1>404: Not Found</h1>
  }

  return (
    <>
      <h1>{data.post.title}</h1>
      <pre>{JSON.stringify(data, null, 2)}</pre>
    </>
  )
}

export default Page

To get the most out of using a database at the Edge, it makes sense to deploy your app also to the Edge to benefit from instant cold boots and lowest latency.

You can configure this on a per page basis, or globally using the following next.config.js configuration:

/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  swcMinify: true,
  experimental: {
    appDir: true,
    runtime: 'experimental-edge'
  }
}

module.exports = nextConfig

In addition to updating next.config.js we will also need to update lib/grafbase.ts to polyfill fetch with the built-in fetch provided by Next.js:

import { GraphQLClient } from 'graphql-request'

export { gql } from 'graphql-request'

export const grafbase = new GraphQLClient(
  process.env.GRAFBASE_API_URL as string,
  {
    headers: {
      'x-api-key': process.env.GRAFBASE_API_KEY as string
    }
  },
  fetch
)

There's a lot more to learn with Next.js 13 but hopefully this has shown how you can benefit from the lower latency by using a edge database with your edge powered React application!

Build your Next.js 13 backend with Grafbase

Get early access to the Grafbase beta.