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
- A practical, Complete Tutorial on HTTP cookies by Valentino Gagliardi
- Start a clean Next.js project with TypeScript, ESLint and Prettier from scratch by Poulin Trognon
- Next.js Advanced Apollo Starter w/ Apollo Client 3, TypeScript, I18n, Docker and more… by Nikita Borisowsky
- Wes Bos: Advanced React