Apollo server set cookie to be accessed in next js app

Viewed 4377

I have a Next.js app and I am using apollo-server-micro and apollo-client. I am unable to set a cookie session when the user hits the login mutation. Here's my server.js setup (npm start)

const express = require('express');
const next = require('next');
const { apolloServer } = require('pages/api/graphql');
const cookieSession = require('cookie-session');

const dev = process.env.NODE_ENV !== 'production';
const app = nextApp({ dev });
const handle = app.getRequestHandler();

app.prepare().then(() => {
  const server = express();

  server.use(express.static('./public'));

  server.use(
        '/api',
        cookieSession({
            httpOnly: false,
            keys: ['super-secret-key'],
            maxAge: 24 * 60 * 60 * 1000,
            name: 'xyz',
        })
    );

  server.get('*', handle);

  server.listen(8000, () => {
    console.log('Server running on port 8000');
  })
})

Within pages/api/graphql.js, I have the following piece of code (straight from Next.js documentation)

import { ApolloServer } from 'apollo-server-micro'
import { schema } from '../../apollo/schema'

export const apolloServer = new ApolloServer({
  schema,
  context: (ctx) => {
    return ctx.req;
  }
})

export const config = {
  api: {
    bodyParser: false
  }
}

export default apolloServer.createHandler({ path: '/api/graphql' })

Here's the mutation for logging in

const jobs = require('../data/jobs');
const users = require('../data/users');

const getUserByEmail = email => users.find(user => user.email === email);

export const resolvers = {
  Query: {
    viewer (_parent, _args, _context, _info) {
      return { id: 1, name: 'John Smith', status: 'cached' }
    },
    jobs: (parent, args, context, info) => {
      return jobs;
    }
  },

  Mutation: {
    login: (parent, args, context, info) => {
      const { email } = args;
      const user = getUserByEmail(email);
      console.log(context.headers);
      if (user) {
        context.req = {
          ...context.req,
          session: user
        }
        return user;
      }
    }
  }
}

There's no encryption and all for now, I just want to get this up and running for now. How would I set the session (so that the cookie appears in Chrome and is available for future incoming requests)

Update: Apollo Client Configuration

import React from 'react'
import Head from 'next/head'
import { ApolloProvider } from '@apollo/react-hooks'
import { ApolloClient } from 'apollo-client'
import { InMemoryCache } from 'apollo-cache-inmemory'

let apolloClient = null

/**
 * Creates and provides the apolloContext
 * to a next.js PageTree. Use it by wrapping
 * your PageComponent via HOC pattern.
 * @param {Function|Class} PageComponent
 * @param {Object} [config]
 * @param {Boolean} [config.ssr=true]
 */
export function withApollo (PageComponent, { ssr = true } = {}) {
  const WithApollo = ({ apolloClient, apolloState, ...pageProps }) => {
    const client = apolloClient || initApolloClient(apolloState)
    return (
      <ApolloProvider client={client}>
        <PageComponent {...pageProps} />
      </ApolloProvider>
    )
  }

  // Set the correct displayName in development
  if (process.env.NODE_ENV !== 'production') {
    const displayName =
      PageComponent.displayName || PageComponent.name || 'Component'

    if (displayName === 'App') {
      console.warn('This withApollo HOC only works with PageComponents.')
    }

    WithApollo.displayName = `withApollo(${displayName})`
  }

  if (ssr || PageComponent.getInitialProps) {
    WithApollo.getInitialProps = async ctx => {
      const { AppTree } = ctx

      // Initialize ApolloClient, add it to the ctx object so
      // we can use it in `PageComponent.getInitialProp`.
      const apolloClient = (ctx.apolloClient = initApolloClient())

      // Run wrapped getInitialProps methods
      let pageProps = {}
      if (PageComponent.getInitialProps) {
        pageProps = await PageComponent.getInitialProps(ctx)
      }

      // Only on the server:
      if (typeof window === 'undefined') {
        // When redirecting, the response is finished.
        // No point in continuing to render
        if (ctx.res && ctx.res.finished) {
          return pageProps
        }

        // Only if ssr is enabled
        if (ssr) {
          try {
            // Run all GraphQL queries
            const { getDataFromTree } = await import('@apollo/react-ssr')
            await getDataFromTree(
              <AppTree
                pageProps={{
                  ...pageProps,
                  apolloClient
                }}
              />
            )
          } catch (error) {
            // Prevent Apollo Client GraphQL errors from crashing SSR.
            // Handle them in components via the data.error prop:
            // https://www.apollographql.com/docs/react/api/react-apollo.html#graphql-query-data-error
            console.error('Error while running `getDataFromTree`', error)
          }

          // getDataFromTree does not call componentWillUnmount
          // head side effect therefore need to be cleared manually
          Head.rewind()
        }
      }

      // Extract query data from the Apollo store
      const apolloState = apolloClient.cache.extract()

      return {
        ...pageProps,
        apolloState
      }
    }
  }

  return WithApollo
}

/**
 * Always creates a new apollo client on the server
 * Creates or reuses apollo client in the browser.
 * @param  {Object} initialState
 */
function initApolloClient (initialState) {
  // Make sure to create a new client for every server-side request so that data
  // isn't shared between connections (which would be bad)
  if (typeof window === 'undefined') {
    return createApolloClient(initialState)
  }

  // Reuse client on the client-side
  if (!apolloClient) {
    apolloClient = createApolloClient(initialState)
  }

  return apolloClient
}

/**
 * Creates and configures the ApolloClient
 * @param  {Object} [initialState={}]
 */
function createApolloClient (initialState = {}) {
  const ssrMode = typeof window === 'undefined'
  const cache = new InMemoryCache().restore(initialState)

  // Check out https://github.com/zeit/next.js/pull/4611 if you want to use the AWSAppSyncClient
  return new ApolloClient({
    ssrMode,
    link: createIsomorphLink(),
    cache
  })
}

function createIsomorphLink () {
  if (typeof window === 'undefined') {
    const { SchemaLink } = require('apollo-link-schema')
    const { schema } = require('./schema')
    return new SchemaLink({ schema })
  } else {
    const { HttpLink } = require('apollo-link-http')
    return new HttpLink({
      uri: '/apii/graphql',
      credentials: 'same-origin'
    })
  }
}

Updated: Apollo-server config using apollo micro

import { ApolloServer } from 'apollo-server-micro'
import { schema } from '../../apollo/schema'

export const apolloServer = new ApolloServer({
  schema,
  context: (ctx) => {
    return ctx.req;
  }
})

export const config = {
  api: {
    bodyParser: false
  }
}

export default apolloServer.createHandler({ path: '/api/graphql' })
2 Answers

If you're using SchemaLink, I think you can just get the req object from the PageContext and use that to reconstruct your GraphQL context object.

const { AppTree, req } = ctx
// elsewhere
new SchemaLink({ schema, context: { req } })

Here's an example that doesn't use SchemaLink that you can adopt to your project. The key is to extract the cookie value from the PageContext. You can then use that to create a custom fetch implementation that populates the Cookie header, and then pass this fetch function to your HttpLink constructor.

let apolloClient: ApolloClient<any>

export const getClient = (
  initialState: NormalizedCacheObject = {},
  cookie?: string,
): ApolloClient<any> => {
  const fetch: WindowOrWorkerGlobalScope['fetch'] = async (url, init = {}) => {
    const headers = { ...init.headers } as Record<string, string>
    if (cookie) {
      headers.Cookie = cookie
    }

    const response = await unfetch(url, {
      ...init,
      headers,
    })
    return response
  }

  return new ApolloClient<any>({
    ssrMode: typeof window === 'undefined',
    link: new HttpLink({
      uri: 'http://localhost:3000/graphql',
      credentials: 'same-origin',
      fetch,
    }),
    cache: new InMemoryCache().restore(initialState),
  })
}

export const initApollo = (
  initialState?: NormalizedCacheObject,
  cookie?: string,
): ApolloClient<any> => {
  if (typeof window === 'undefined') {
    return getClient(initialState, cookie)
  }
  if (!apolloClient) {
    apolloClient = getClient(initialState, cookie)
  }

  return apolloClient
}

export const withApollo = (PageComponent: NextPage, { ssr = true } = {}) => {
  const WithApollo = ({ apolloState, ...pageProps }: WithApolloProps) => {
    const client = apolloClient || initApollo(apolloState)

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

  if (ssr || PageComponent.getInitialProps) {
    WithApollo.getInitialProps = async (ctx: PageContext) => {
      const { AppTree } = ctx
      const cookie = (ctx.req && ctx.req.headers.cookie) || undefined
      const apolloClient = (ctx.apolloClient = initApollo({}, cookie))

      let pageProps = {}
      if (PageComponent.getInitialProps) {
        pageProps = await PageComponent.getInitialProps(ctx)
      }

      if (typeof window === 'undefined') {
        if (ctx.res && ctx.res.finished) {
          return pageProps
        }

        if (ssr) {
          try {
            const { getDataFromTree } = await import('@apollo/react-ssr')
            await getDataFromTree(
                <AppTree
                    pageProps={{
                      ...pageProps,
                      apolloClient,
                    }}
                />,
            )
          } catch (error) {
            console.error('Error while running `getDataFromTree`', error)
          }

          Head.rewind()
        }
      }

      const apolloState = apolloClient.cache.extract()

      return {
        ...pageProps,
        apolloState,
      }
    }
  }

  return WithApollo
}

As an aside, you want to make sure you're setting the context correctly when initializing Apollo Server. Don't just return the req object, as that will set your context to the req object. Instead do:

context: ({ req }) => {
  return { req };
}

and then make sure you're correctly mutating the session inside your resolver:

context.req.session.user = user

As is, your code is overriding the session object with the user, which is not what you want to do. Setting the user property on the session means you'll be able to access it with context.req.session.user elsewhere in your code.

Here's a solution from this article https://www.rockyourcode.com/nextjs-with-apollo-ssr-cookies-and-typescript/

import {
  ApolloClient,
  InMemoryCache,
  NormalizedCacheObject,
} from "@apollo/client";
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 { AppProps } from "next/app";
import { useMemo } from "react";

const APOLLO_STATE_PROP_NAME = "__APOLLO_STATE__";

let apolloClient: ApolloClient<NormalizedCacheObject> | undefined;

const createApolloClient = (headers: IncomingHttpHeaders | null = null) => {
  const enhancedFetch = (url: RequestInfo, init: RequestInit) => {
    return fetch(url, {
      ...init,
      headers: {
        ...init.headers,
        "Access-Control-Allow-Origin": "*", // TODO: Check this out
        Cookie: headers?.cookie ?? "",
      },
    }).then((response) => response);
  };

  return new ApolloClient({
    ssrMode: typeof window === "undefined",
    link: createUploadLink({
      uri: "http://localhost:4000/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 (initialState) {
    const existingCache = _apolloClient.extract();

    const data = merge(initialState, existingCache, {
      arrayMerge: (destinationArray, sourceArray) => [
        ...sourceArray,
        ...destinationArray.filter((d) =>
          sourceArray.every((s) => !isEqual(d, s)),
        ),
      ],
    });

    _apolloClient.cache.restore(data);
  }

  if (typeof window === "undefined") return _apolloClient;

  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;
}
Related