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 {
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!