記事一覧

Real World GraphQL on Next.js SSR [EN]

tl;dr

  • Next.js has changed the handling of Initial Loading since v9.3.0
  • You can use Apollo on the client side, but if you want to set the Authorization Header, you need to make some changes
  • At SSR, it is better to make a simple API request by fetch

Today’s Web Frontend

I feel that in recent web development, not only do we need to have a basic knowledge of React and TypeScript, but frameworks such as Next.js (or Nuxt.js) that provide dynamic routing and build environments at high speed, while meeting the needs of both SPA and SSR, are coming in handy.

Redux is also good, but the combination of GraphQL, which can promote schema-driven development, and especially Hooks and Apollo client, which can provide uncommon state management, has given me quite a shocking developer experience after not having been a front-end developer for a while.

If you look at The State of JavaScript, you’ll see that these technologies are making their way up the mainstream.

Today’s Next.js

Well, I like React + TypeScript, so I chose Next.js, and adopted GraphQL because the experience was so good, and I think the development speed has increased, but I don’t have much know-how.

Especially, Next.js is recommended to use the function getStaticProps(getStaticPaths) or getServerSideProps instead of the function getInitialProps, which was used for data fetching before rendering, after v9.3.0, but there is almost no know-how about it.

Previously, getInitialProps was called on the server side during initial load, and on the client side during client-side routing with next/link.

If it becomes getStaticProps or getServerSideProps, the behavior is different: getStaticProps for SSG which generates static files including the result of API requests at build time, or getServerSideProps which is called at SSR on the server side for every request, **in other words, it is called at build time or on the server side for every request. **

**You don’t have to write code that assumes that the client and the server will be called by both, but you don’t know this much, and most of the code in official examples assume getInitialProps. **It’s not completely deprecated code, so it can’t be helped (and the great improvements are being made so fast that I’m not inclined to blame it at all).

How to use GraphQL on Next.js

Now, if I were to give my own answer to the question of how the GraphQL client should be used in Next.js after v9.3.0, the practical problem would be as follows.

  • On the client side, Apollo Client is used with Hooks assumption.
  • On the server side, use a plain fetch API to call the API.

GraphQL on client-side

Let’s look at it from the client side first. Here, we use Apollo in combination with React Hooks.

We’ll leave the detailed explanation of Apollo and React Hooks to others, but here we’ll focus on how to use GraphQL on the client side of Next.js.

You can read this document about Hooks in Apollo, but you can wrap the root component in the ApolloProvider component and use

class CustomApp extends App {
  render() {
    const { Component, pageProps } = this.props

    return (
      <ApolloProvider client={/* Insert an ApolloClient instance that was created somewhere */}>
        <Component {...pageProps} />
      </ApolloProvider>
    )
  }
}

By calling queries such as useQuery, useLazyQuery (lazy execution of a query at any given time), and useMutation in child components, developers can call GraphQL’s APIs without being aware of the DI-like work of the Apollo Client.

If you call these methods, they return three kinds of values: data of the response, loading of the loading state, and error of the error content, and you can render by passing them to the component.

Also, although the notation of React Hooks itself makes state management easier, ** especially in cases where you need to change the arguments to various methods of apollo-hooks, such as search and paging, it brings a strong developer experience improvement. **

For example, if you were to call up a list of blogs while searching and paging, it might look something like this. I’m sure there are other ways to write this, but in that case, please let me know your recommended design on Twitter or something like that.

const blogListPage = (props: IProps) => {
  const [activeIndex, setActiveIndex] = useState<number>(0)
  const defaultLimit: number = 10
  const [blogSearchInput, setBlogSearchInput] = useState<SearchInput>({
    query: '',
  })
  const initialBlogPageInput: PageInput = {
    first: defaultLimit,
    last: null,
    before: null,
    after: null,
  }
  const [blogPageInput, setBlogPageInput] = useState<PageInput>(initialBlogPageInput)
  const { data, loading, error } = useBlogListPageQuery({
    variables: {
      listBlogInput: {
        page: blogPageInput,
        search: blogSearchInput,
      },
    },
  })

  // Functions such as render, error handling and click event handlers follow

A little off the cuff, here we call useBlogListPageQuery. While we are not calling useQuery directly for the sake of the example, **you can also auto-generate a wrapper for the Query Hooks you call inside certain components like this one. **

I use them as a generator to make client-side GraphQL development easier. I honestly can’t imagine a development that doesn’t use these.

Back to the point, if you want to switch between search and paging content based on **choice or form input, all you have to do is update the value of blogSearchInput or blogPageInput, and the referring useQuery will automatically throw a request, update the contents of data, and re-render the referring component. **

I’m not going to dismiss jQuery, but it’s honestly much faster and easier than my previous development experience.

Client-side authentication

Now, so far, so peaceful, but *the problem is authentication, let’s assume that you use Firebase, Auth0, etc. to authenticate on the API side, for example using Authorization Header. **

In case of Apollo, it is possible to authenticate by adding the header information to the property link when creating a client instance. This is in the middle of the following code.

const httpLink = new HttpLink({
  uri: process.env.GRAPHQL_URL,
  credentials: 'same-origin',
  fetch,
})

const authLink = setContext((_, { headers }) => {
  // return the headers to the context so httpLink can read them
  return {
    headers: {
      ...headers,
      authorization: token ? `Bearer ${token}` : "",
    }
  }
})

const client = new ApolloClient({
  link: authLink.concat(httpLink),
  cache: new InMemoryCache()
});

The problem here, however, is when Authorization Token can be obtained.

I’m using Auth0, but you can get an access token by calling the get session information method after logging in.

**If it’s an SSR product, how do I get the token and initialize ApolloClient immediately after redirecting from the Auth0 authentication page? **

In conclusion, **I couldn’t find any solution other than “initialize Apollo Client with getInitialProps of App with a token as an argument”. **

const createApolloClient = (token: string) => {
  const httpLink = new HttpLink({
    uri: process.env.GRAPHQL_URL,
    credentials: 'same-origin',
    fetch,
  })

  const authLink = setContext((_, { headers }) => {
    // return the headers to the context so httpLink can read them
    return {
      headers: {
        ...headers,
        authorization: token ? `Bearer ${token}` : "",
      }
    }
  })

  const client = new ApolloClient({
    link: authLink.concat(httpLink),
    cache: new InMemoryCache()
  });

  return client
}

class CustomApp extends App<AppInitialProps & { token: string }>  {
  static async getInitialProps({ Component, ctx }) {
    const pageProps = Component.getInitialProps
      ? await Component.getInitialProps(ctx)
      : {};

    const session = await auth0.getSession(ctx.req)

    return { pageProps, token: session?.accessToken };
  }

  render() {
    const client = createApolloClient(this.props.token)
    const { Component, pageProps } = this.props

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

export default CustomApp

**Because this code runs API communication with Auth0 every time, it is bad for rendering cost, and we don’t want to call getInitialProps for the reason mentioned in the beginning. **

**However, I can’t think of a way to initialize Apollo Client with Next.js SSR product of the authentication premise other than this as a measure of hardship. Or rather, I can’t find it anywhere. It was really hard. In my dream, a 403 error was returned. **

*You may also want to temporarily store the token in localStorage. However, *that won’t be able to handle the strict session breakage managed by Auth0, and it will clog up localStorage because it can’t be initialized when doing logout handling with API Route in Next.js. **

Similarly, those using Auth0 in Next.js might use the nextjs-auth0 library, but the above issue is mentioned in Issue here.

Postscript

A few people pointed it out on Twitter, so I’ve fixed it, and here’s another way to implement it.

If you don’t need to set the token on the client side, you can insert the token on the SSR server or the BFF’s gateway server, and there is no need to write the clumsy process of getInitialProps.

I thought it would be easier if I could do it using the API Route feature without creating a BFF, so I decided to do it this time.

As an example, let’s make a request to the GraphQL API by proxying the path /api/graph created with the API Route function.

Auth0 is used as a sample. Except for getting the idToken from the session information and adding it to the header, I copy the API response as it is and pass it on to the client.

The following is based on gist here.

import auth0 from '../../lib/auth0';

const callAPI = async (path, body, headers) => {
  const res = await fetch(process.env.GRAPHQL_URL, {
    method: 'post',
    headers: {
      'content-type': 'application/json',
      ...headers,
    },
    body: JSON.stringify(body),
  });
  return {
    body: await res.text(),
    status: res.status,
    headers: res.headers,
  };
};

const forwardHeader = (res, apiRes, header) => {
  if (apiRes.headers.get(header)) {
    res.setHeader(header, apiRes.headers.get(header));
  }
};

const forwardResponse = (res, apiRes) => {
  forwardHeader(res, apiRes, 'content-type');
  forwardHeader(res, apiRes, 'www-authenticate');
  res.status(apiRes.status);
  res.send(apiRes.body);
};

export default async function graph(req, res) {
  try {
    const { idToken } = await auth0.getSession(req)
    const apiRes = await callAPI('graphql', req.body, {
      authorization: idToken ? `Bearer ${idToken}` : ""
    });
    forwardResponse(res, apiRes);
  } catch (error) {
    console.error(error);
    res.status(error.status || 400).end(error.message);
  }
}

Through the above proxy, the initialization process of the Apollo Client on the client side is simple as follows.

Of course, there is a trade-off between the load on the BFF gateway and SSR server and the quality of the code on the client side, but I think this is better because it doesn’t block processing by session fetching at rendering time.

class CustomApp extends App {
  render() {
    const client = new ApolloClient({
      link: createHttpLink({
        uri: "/api/graph",
        credentials: 'same-origin',
        fetch,
      }),
      cache: new InMemoryCache()
    })
    const { Component, pageProps } = this.props

    return (
      <ApolloProvider client={client}>
        <ThemeProvider theme={defaultTheme}>
          <Fragment>
            <BaseStyles />
            <Component {...pageProps} />
            <ToastContainer />
            <NextNProgress />
          </Fragment>
        </ThemeProvider>
      </ApolloProvider>
    )
  }
}

GraphQL on server-side

Fundamentally, the client-side development experience is dramatically enhanced by using Apollo. But not so much on the server side.

As mentioned above, it should be solved with a plain fetch API call, but why?

Even the official examples with-apollo can use Apollo when SSRing! You might think, “Oh, no.

**This is a big lie, as of now, if you use Apollo for SSR in Next.js, the rendering performance will drop like an idiot. **

See the official Discussion here.

This is because Apollo uses the function getDataFromTree to monitor which node in the component tree is referring to the query result via Manager class, and waits at Promise.all until all the results are retrieved.

So, you should implement a simple API call with fetch API, as you can see in comment here.

For a simple example, assuming that only the user’s basic information is registered with Mutation, the following API call method is defined and the

interface GQLQueryResponse {
  data: any
}

async function callGraphAPI(ctx: NextPageContext, query: string, { variables }: { variables: any } = {
  variables: {}
}) {
  const token = getCookie(ctx.req, TOKEN_COOKIE_NAME)
  const res = await fetch(process.env.GRAPHQL_URL, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${token}`,
    },
    body: JSON.stringify({
      query,
      variables,
    }),
  })

  const json = await res.json()
  if (json.errors) {
    throw new Error(json.errors.toString())
  }
  return json
}

export const createUser = async (ctx: NextPageContext, input: CreateUser): Promise<GQLQueryResponse> => {
  return await callGraphAPI(ctx, `
    mutation createUser($input: CreateUser!) {
  createUser(input: $input) {
    id
    email
    enabled
    createdAt
    updatedAt
  }
}`, {
    variables: {
      input
    }
  })
}

I think the callback page after login will do something like this. (I’m leaving out quite a bit of code.)

const callback = () => {
  return <Loading />
}

export async function getServerSideProps(ctx: NextPageContext) {
 const session = await auth0.getSession(ctx.req)

  try {
    await createUser(ctx, {
      id: session.user.sub,
      email: session.user.email,
    })
  } catch (error) {
    console.error(error)
    res.writeHead(500).end(error.message);
  }

  return {
    props: {}
  }
}

export default callback

As described above, on the SSR server side, if you try to use Apollo Client as is in the example of the example directory, you should implement fetch API or similarly simple API client because performance will decrease.

Also, since there is no need to put Apollo Client in NextPageContext in HOC, the Apollo Client used on the client side can be passed to the child component via the plain ApolloProvider as usual. This is a good point.

Summary

Next.js, GraphQL, Apollo, and all of them are great tools that will take the stress out of developers so far.

However, even in English-speaking countries, there is still a lack of know-how, and even if we refer to official examples, there are cases where the actual product does not turn out beautifully, or the performance goes down.

Even if it is far from the ideal, we have no choice but to implement it with enthusiasm, even if it is in an honest way.

The tool is already beyond the chasm of “it’s fine if it’s a personal product,” so I wanted to help you develop it more smoothly, so I wrote this article.

I hope I can help someone else.