Implementing GraphQL with Type Safety in Next.js 14 App Router
Recently, I was working on a Next.js project where I tried to integrate Strapi’s GraphQL API with a Next.js 14 app. I was frustrated because there wasn’t any specific official documentation to help with integrating GraphQL into the Next.js 14 app router. So, I researched a few methods that can be used to achieve this integration.
Since the release of Next.js 13, the complexities of the app router have reached their peak, with Next.js declaring every component as a server component by default. This means we can’t use contexts, useEffect, useRef, or useState in server components. The Apollo Client, however, relies on the principle of context.
import React from 'react';
import * as ReactDOM from 'react-dom/client';
import { ApolloClient, InMemoryCache, ApolloProvider } from '@apollo/client';
import App from './App';
const client = new ApolloClient({
uri: process.env.API,
cache: new InMemoryCache(),
});
// Supported in React 18+
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<ApolloProvider client={client}>
<App />
</ApolloProvider>,
);
This isn’t actually possible in Next.js server components, as it will result in an error. With the Next.js 13 app router, data fetching has completely changed, and caching and revalidation can be quite confusing. To address these challenges, I explored different options and found one that works well.
So, to get started, let’s create a fresh Next.js app using create-next-app
.
npx create-next-app@latest
After creating the app, you need to install the following packages:
npm install @apollo/client @apollo/experimental-nextjs-app-support graphql
After installing these packages, you need to create the configuration for GraphQL.
// config/api.ts
import { HttpLink, InMemoryCache, ApolloClient } from "@apollo/client";
import { registerApolloClient } from "@apollo/experimental-nextjs-app-support/rsc";
export const { getClient } = registerApolloClient(() => {
return new ApolloClient({
cache: new InMemoryCache(),
link: new HttpLink({
uri: process.env.API,
}),
});
});
Although the package @apollo/experimental-nextjs-app-support
is experimental and may be removed later, it is working fine currently.
For now, let’s use the dummy GraphQL API provided by Apollo.
//.env
API = "https://countries.trevorblades.com/graphql"
Now you can use getClient
directly in any of your server components in the following way:
// app/page.tsx
import { getClient } from "@/config/api";
import { gql } from "@apollo/client";
export default async function Home() {
const { data } = await getClient().query({
query: CountriesQuery,
context: {
fetchOptions: {
next: { revlidte: 10 },
},
},
});
return (
<main className="">
{data?.countries?.map((country: any, index: number) => {
return (
<div key={index} className="border-white border-b-2">
<ul>
<li>{country?.name}</li>
<li>{country?.code}</li>
</ul>
</div>
);
})}
</main>
);
}
The best part of this code is that it allows you to revalidate the cache in the same way you would with the rest API data fetching in Next.js app router.
I think this is sufficient for JavaScript users, but the real challenge begins for TypeScript users here! 😂 If you’ve figured out ‘Type Safe GraphQL,’ you probably know that most TypeScript users rely on graphql-codegen
to generate types. GraphQL Code Generator automates the creation of TypeScript types, queries, resolvers, and custom React hooks for fetching data from your GraphQL schema. While this is a great option, it’s not ideal for server components.
I recently heard about gql.tada
and researched it. I found it to be an incredible tool for ensuring type safety with GraphQL. It's currently around version 1, and the team is constantly working to make it more efficient.
To get started with gql.tada
, you need to install it using the following command:
npm install gql.tada
After installing you need to add the TypeScrpt plugin to your tsconfig.json
to set it up with TypeScript’s language server. Setting up gql.tada/ts-plugin
will start up a “TypeScript Language Service Plugin” when TypeScript is analyzing a file in your IDE or editor. This provides editor hints, such as diagnostics, auto-completions, and type hovers for GraphQL.
"plugins": [
{
"name": "next"
},
{
"name": "gql.tada/ts-plugin",
"schema": "https://countries.trevorblades.com/graphql",
"tadaOutputLocation": "./graphql-env.d.ts"
}
],
To configure the schema, you have several options. The schema can be provided as a .graphql
file containing the schema definition in GraphQL SDL format or a .json
file with the schema's introspection query data. You can refer to the gql.tada
official documentation for more details.
For most development scenarios, you’ll need to generate types from your GraphQL server. After setting up your schema, you should specify your tadaOutputLocation
. This option defines the output path for the typings file, which gql.tada
uses to infer GraphQL types within the TypeScript type system. The tadaOutputLocation
supports two formats based on the file path you provide: .d.ts
and .ts
. The documentation suggests using .d.ts
for better performance, so we'll use .d.ts
for now.
Next, you need to generate the output file that contains all your types. To do this, use the following CLI command:
npx gql.tada generate output
However, I was unable to generate the .d.ts
file initially, so I had to install gql.tada
globally on my device using the following command:
npm i -g gql.tada
Now use the following command to generate .d.ts
file
gql.tada generate output
After installing gql.tada
globally, I was able to generate the .d.ts
file, which looks similar to the following:
declare const introspection: {
__schema: { /*...*/ };
};
import * as gqlTada from 'gql.tada';
declare module 'gql.tada' {
interface setupSchema {
introspection: typeof introspection;
}
}
Now that the setup is complete, note that VSCode may not load your workspace’s TypeScript installation by default and might instead use a global installation, which can prevent the plugin from being loaded. To resolve this, add the following two lines to your settings.json
configuration in VSCode:
{
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true
}
Now you’re all set! Let’s see what changes are needed in page.tsx
.
You need to update your query import from @apollo/client
to gql.tada
. Here's what your final code should look like:
//page.tsx
import { getClient } from "@/config/api";
import { graphql } from "gql.tada";
const CountriesQuery = graphql(`
query Countries {
countries {
name
capital
code
continent {
code
name
}
currency
}
}
`);
export default async function Home() {
const { data } = await getClient().query({
query: CountriesQuery,
context: {
fetchOptions: {
next: { revlidte: 10 },
},
},
});
return (
<main className="">
{data?.countries?.map((country: any, index: number) => {
return (
<div key={index} className="border-white border-b-2">
<ul>
<li>{country?.name}</li>
<li>{country?.code}</li>
</ul>
</div>
);
})}
</main>
);
}
With these changes, your code should now benefit from type safety in responses from GraphQL. I hope this approach resolves your issues and enhances your development experience. Feel free to let me know how it works out for you!
References
https://www.npmjs.com/package/@apollo/experimental-nextjs-app-support