Mastodon hachyterm.io

How to wire up a Next.js TypeScript application with Apollo Client, using Server Side Rendering and Static Site Generation

In this blog post I’ll show you how I created a working Next.js TypeScript setup with Apollo Client.

You can fetch data from a GraphQL endpoint both on the Node.js server as well as on the Next.js client, utilizing the Apollo Cache.

You are also able to work with protected endpoints using cookie-based authentication.

With minor adjustments, this setup should be applicable for JWT (JSON Web Tokens), too.

Initial Situation

I have a GraphQL server that uses sessions for authentication (in the form of cookies). In production, the server will share the same root domain as the client (Next.js application).

For example, the server will be on backend.example.com and Next.js will be on frontend.example.com.

It’s important to have both on the same domain to prevent problems with SameSite. Newer browsers will prevent you to set third-party cookies if you don’t mark them as SameSite=None and Secure.
You will need to configure these settings on the back-end server. Depending on how you’ve created your GraphQL server, this won’t be possible.
For example, I am using Keystone.js (Next) which does not allow the developer to set SameSite=none.

Edit: It looks like Keystone.js (Next) now offers the configuration options to set the SameSite attribute to none and the secure setting.
That means that you can now deploy frontend and backend to completely different domains!

You can read more about cookies on Valentino Gagliardi’s post.

Next.js Setup

I’ve been using Poulin Trognon’s guide on how to setup Next.js with TypeScript.

The basics are clear-cut. Create a new Next.js application with their CLI (create-next-app), install TypeScript and create a tsconfig.json file.

Please follow the steps in the Next.js documentation for getting started and their documentation about using TypeScript.

Apollo Client

The following material originally is from Vercel’s Next.js example as well as the Next Advanced Starter by Nikita Borisowsky.

While these two resource were invaluable, they needed some minor tweaks for my setup and some searching in GitHub issues.

Create a file for the Apollo client:

import {
  ApolloClient,
  ApolloLink,
  InMemoryCache,
  NormalizedCacheObject,
} from '@apollo/client'
import { onError } from '@apollo/link-error'
import { createUploadLink } from 'apollo-upload-client'
import merge from 'deepmerge'
import { IncomingHttpHeaders } from 'http'
import fetch from 'isomorphic-unfetch'
import isEqual from 'lodash/isEqual'
import type { AppProps } from 'next/app'
import { useMemo } from 'react'
import { paginationField } from './paginationField'

const APOLLO_STATE_PROP_NAME = '__APOLLO_STATE__'

let apolloClient: ApolloClient<NormalizedCacheObject> | undefined

const createApolloClient = (headers: IncomingHttpHeaders | null = null) => {
  // isomorphic fetch for passing the cookies along with each GraphQL request
  const enhancedFetch = (url: RequestInfo, init: RequestInit) => {
    return fetch(url, {
      ...init,
      headers: {
        ...init.headers,
        'Access-Control-Allow-Origin': '*',
        // here we pass the cookie along for each request
        Cookie: headers?.cookie ?? '',
      },
    }).then((response) => response)
  }

  return new ApolloClient({
    // SSR only for Node.js
    ssrMode: typeof window === 'undefined',
    link: ApolloLink.from([
      onError(({ graphQLErrors, networkError }) => {
        if (graphQLErrors)
          graphQLErrors.forEach(({ message, locations, path }) =>
            console.log(
              `[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`
            )
          )
        if (networkError)
          console.log(
            `[Network error]: ${networkError}. Backend is unreachable. Is it running?`
          )
      }),
      // this uses apollo-link-http under the hood, so all the options here come from that package
      createUploadLink({
        uri: 'http://localhost:3000/api/graphql',
        // Make sure that CORS and cookies work
        fetchOptions: {
          mode: 'cors',
        },
        credentials: 'include',
        fetch: enhancedFetch,
      }),
    ]),
    cache: new InMemoryCache(),
  })
}

type InitialState = NormalizedCacheObject | undefined

interface IInitializeApollo {
  headers?: IncomingHttpHeaders | null
  initialState?: InitialState | null
}

export const initializeApollo = (
  { headers, initialState }: IInitializeApollo = {
    headers: null,
    initialState: null,
  }
) => {
  const _apolloClient = apolloClient ?? createApolloClient(headers)

  // If your page has Next.js data fetching methods that use Apollo Client, the initial state
  // get hydrated here
  if (initialState) {
    // Get existing cache, loaded during client side data fetching
    const existingCache = _apolloClient.extract()

    // Merge the existing cache into data passed from getStaticProps/getServerSideProps
    const data = merge(initialState, existingCache, {
      // combine arrays using object equality (like in sets)
      arrayMerge: (destinationArray, sourceArray) => [
        ...sourceArray,
        ...destinationArray.filter((d) =>
          sourceArray.every((s) => !isEqual(d, s))
        ),
      ],
    })

    // Restore the cache with the merged data
    _apolloClient.cache.restore(data)
  }

  // For SSG and SSR always create a new Apollo Client
  if (typeof window === 'undefined') return _apolloClient
  // Create the Apollo Client once in the client
  if (!apolloClient) apolloClient = _apolloClient

  return _apolloClient
}

export const addApolloState = (
  client: ApolloClient<NormalizedCacheObject>,
  pageProps: AppProps['pageProps']
) => {
  if (pageProps?.props) {
    pageProps.props[APOLLO_STATE_PROP_NAME] = client.cache.extract()
  }

  return pageProps
}

export function useApollo(pageProps: AppProps['pageProps']) {
  const state = pageProps[APOLLO_STATE_PROP_NAME]
  const store = useMemo(() => initializeApollo({ initialState: state }), [
    state,
  ])
  return store
}

Of course, you will need to install a few libraries:

yarn add @apollo/client @apollo/link-error @apollo/react-common @apollo/react-hooks deepmerge lodash graphql graphql-upload isomorphic-unfetch apollo-upload-client

The code above uses the apollo-upload-client as an alternative for the standard HttpLink. If you don’t plan on uploading files, you can replace the createUploadLink part above:

const httpLink = new HttpLink({
  uri: 'http://localhost:3000/api-graphql',
  credentials: 'include',
  fetch: enhancedFetch,
})

After you’ve created all the scaffolding, you will need to connect it to your Next.js application.

Next.js uses the App component to initialize pages. You need to create the component as ./pages/_app.tsx:

import { AppProps } from 'next/app'
import { ApolloProvider } from '@apollo/client'

import { useApollo } from '../lib/apollo'

const App = ({ Component, pageProps }: AppProps) => {
  const apolloClient = useApollo(pageProps)

  return (
    <ApolloProvider client={apolloClient}>
      <Component {...pageProps} />
    </ApolloProvider>
  )
}

export default App

Use Apollo for (Incremental) Static Site Generation

Let’s say that you have a list of products that you want to statically create at build time. The products don’t require authentication/authorization.

// first create an Apollo client for the server
const client = initializeApollo()

export const getStaticPaths = async () => {
  // here we use the Apollo client to retrieve all products
  const {
    data: { allProducts },
  } = await client.query<AllProductsQuery>({ query: ALL_PRODUCTS_QUERY })
  const ids = allProducts?.map((product) => product?.id)
  const paths = ids?.map((id) => ({ params: { id } }))

  return {
    paths,
    fallback: true,
  }
}

interface IStaticProps {
  params: { id: string | undefined }
}

export const getStaticProps = async ({ params: { id } }: IStaticProps) => {
  if (!id) {
    throw new Error('Parameter is invalid')
  }

  try {
    const {
      data: { Product: product },
    } = await client.query({
      query: PRODUCT_QUERY,
      variables: { id },
    })
    return {
      props: {
        id: product?.id,
        title: product?.name,
      },
      revalidate: 60,
    }
  } catch (err) {
    return {
      notFound: true,
    }
  }
}

The full example is available on GitHub.

Use Apollo for Server-Side Rendering

Let’s see an example for the orders page where authenticated users can see a list of their orders:

export const getServerSideProps = async (
  context: GetServerSidePropsContext
) => {
  // pass along the headers for authentication
  const client = initializeApollo({ headers: context?.req?.headers })
  try {
    await client.query<AllOrdersQuery>({
      query: ALL_ORDERS_QUERY,
    })

    return addApolloState(client, {
      props: {},
    })
  } catch {
    return {
      props: {},
      redirect: {
        destination: '/signin',
        permanent: false,
      },
    }
  }
}

You can also see the complete code on GitHub.

Running in Production

You can see the repository on GitHub.

Further Reading