Heavy, Engine, Ear
HeavyEngineer
Published on

Generate types for NextJS from the Strapi GraphQL schema

Using code to generate more code is among my favourite things to do on a Tuesday. Sadly it's Monday. But hey ho. Here is an explainer to get GraphQL codegen working with NextJS and the Apollo client so you can talk to the GraphQL schema in Strapi.

First install the packages:

npm i -D graphql @graphql-codegen/cli @graphql-codegen/typescript-react-apollo

Then using the schema explorer in Strapi - probably at http://localhost:1337/graphl Create the query that you would like to use, for example select all the articles:

Create the graphql queries in the Strapi playground

You will notice that your queries will appear like this:

query{
  articles{
    data{
      attributes{
        Title
      }
    }
  }
}

Which is apparently an anonymous query. The Apollo client will want to use named queries. Here you can see I've named the query 'articles' by inserting a string after the query tag

query articles{
  articles{
    data{
    ...snip

Save this query into a new file here src/graphql/articles/articles.graphql. Use the directory structure to denote content types or particular query patterns. The codegen will use this graphql query as the basis for generating the hooks and types.

Now you can either run the wizard to create the config file npx graphql-code-generator init

Or create a codegen.ts file like this:

import type { CodegenConfig } from "@graphql-codegen/cli";

const config: CodegenConfig = {
  overwrite: true,
  schema: "http://localhost:1337/graphql",
  documents: "src/graphql/**/*.graphql",
  ignoreNoDocuments: true,
    generates: {
    "./src/gql/graphql.tsx": {
      plugins: [
        "typescript",
        "typescript-operations",
        "typescript-react-apollo",
      ],
    },
  },
};

export default config;

Items of note

This is where the articles graphql query file is stored. The '**' denotes any directory, so I recommend being verbose and packaging the queries, by content type, feature or other obvious pattern.

documents: "src/graphql/**/*.graphql",

This is the output file and will contain a single file graphql.tsx which contains the Types and React Hooks.

generates: {
    "./src/gql/graphql.tsx": {
      ...snip

If you use the cli wizard, be careful to check the .tsx extension for the output file as it may not be there by default and is required by the Apollo client. Also i found that the wizard added a setting preset: "client" this doesn't work with the Apollo client (or at least i couldn't get it to work) and will lead to a malfunctioning file with duplicate identifiers.

  generates: {
    "./src/gql/graphql.tsx": { // <- check this file/dir/extension
      preset: "client", // <- this one
      plugins: [
        "typescript",
        "typescript-operations",
        "typescript-react-apollo",
      ],
    },
  },

In graphql.tsx you should be able to see your types e.g.

shows types now defined in tsx code for nextjs
graphql queries as exported functions
generated react hooks for Apollo graphql client

Replacing REST calls with GraphQL

Previously i've written about using getStaticProps with NextJS to render dynamic content. However the REST api is a bit meh, and the graphql API should allow us to do more with less and have just a bit more code generation! This is the code i'm going to replace:

// src/pages/article/[Slug].tsx
export const getStaticProps: GetStaticProps = async (context) => {
  const { Slug } = context.params as IParams;
  const queryURL =
    "/articles?filters%5BSlug%5D=" +
    Slug +
    "&fields=Title%2CBody%2C%20LeadIn%2CupdatedAt&populate=OtherMedia%2CHeroMedia";

  const res = await fetch(process.env.NEXT_PUBLIC_API_SERVER_URL + queryURL);
  const article = (await res.json()) as Article;

  return {
    props: {
      article,
    },
  };
};

If you want to use the apollo client in react, then you can create a React component to wrap the entire app in:

// src/graphql/apollo.tsx
import { ApolloClient, ApolloProvider, InMemoryCache } from "@apollo/client";
import React, { PropsWithChildren } from "react";

const StrapiApolloProvider: React.FC<PropsWithChildren<{}>> = ({
  children,
}) => {
  const client = new ApolloClient({
    uri: process.env.NEXT_PUBLIC_API_SERVER + "/graphql",
    cache: new InMemoryCache(),
  });
  return <ApolloProvider client={client}>{children}</ApolloProvider>;
};

export default StrapiApolloProvider;

But this is not the best way to use it in our case, which is outside of the React Components, and in a server side function that doesn't have user interaction. So we're going to create an additional client just for this:

// src/graphql/staticApollo.ts
import { ApolloClient, InMemoryCache } from "@apollo/client";

const apollo = new ApolloClient({
  uri: process.env.NEXT_PUBLIC_GRAPHQL_SERVER_URL,
  cache: new InMemoryCache(),
});

export default apollo;

So to use the staticApollo client in your getStaticProps and getStaticPaths, you will need to: import apollo from "@/graphql/staticApollo";

The GraphQL query we want to run is below. Note that we are passing the 'Slug' (a shortened, unique, version of the title) as an identifier. This is due to the articles having a url structure like protocol://hostname/article/slug-shortened-title So when queries come in externally they will be calling the slug value so we need to return data based on that. You will also note that we're querying for data deep within the content type - 'OtherMedia' and 'HeroMedia' specifically.

// src/graphql/articles/getArticleBySlug.graphql
query GetArticleBySlug($slug: String!) {
  articles(filters: { Slug: { eq: $slug } }) {
    data {
      attributes {
        Title
        Body
        Leadin
        updatedAt
        OtherMedia {
          data {
            id
            attributes {
              url
              mime
              caption
            }
          }
        }
        HeroMedia {
          data {
            id
            attributes {
              url
              mime
              caption
            }
          }
        }
      }
    }
  }
}

To replace the REST call we need to copy the graphql from above into a file located at: src/graphql/articles/getArticleBySlug.graphql

The run npm run codegen

Then back into our [Slug].tsx file with the REST query and replace it with:

export const getStaticProps: GetStaticProps = async (context) => {
  const { Slug } = context.params as IParams;

 const { data, error } = await apollo.query<GetArticleBySlugQuery>({
    query: GetArticleBySlugDocument,
    variables: { slug: Slug },
  });

// you WILL want to know if something fails to render. For now, log the error and return
// empty props so the client gets a 404, but this should trigger a red flag in ci/cd
  if (error != null) {
    console.log(error.message);
    return {
      props: {},
    };
  }

  const article = data.articles;

  return {
    props: {
      article,
    },
  };
};

And we should still be in business, but retrieving our data from the GraphQL API, not the REST API. For the next step you'd need to replace getStaticPaths using the same technique.

Whilst you could use elements of the OpenAPI types combined with the GraphQL types, they aren't identical and may cause some headscratching if you have similarly named types from different sources.

When you're making requests for live data to mutate the state in React, then you'd use the React hooks, which will probably be in another blog post.

The APIs and Types will be compatible and where it makes sense you could use either interface, but I personally prefer the GraphQL syntax.

Props to artcoded for this video: https://www.youtube.com/watch?v=XOiTrpLLM3c which set me on the right path.

Some more good docs here: https://the-guild.dev/graphql/codegen/docs/guides/react-vue

And further in-depth Strapi docs here: https://strapi.io/blog/a-deep-dive-into-strapi-graph-ql