How to SSR with Next.js, Apollo Client, and MongoDB Atlas GraphQL API

178 Views Asked by At

From my Next.js app (Pages router) I'm using Apollo Client to connect to a my GraphQL API provided by MongoDB Atlas App Services. I'm following this guide in the MongoDB docs and am attempting to server-side render (SSR) the results from the API response.

Although I've followed the guide largely verbatim in a new project (only swapping out the GraphQL API used along with the App ID) I can't get SSR to work. I keep getting back a 401 (unauthorised response) saying the access token has expired in the console:

statusCode: 401,
result: {
  error: 'invalid session: access token expired',
  error_code: 'InvalidSession',
}

From the MongoDB logs I see the following error:

Error: unauthorized
Logs: ["token contains an invalid number of segments"]

In my browser console I can see that the cookie is created: enter image description here

My Next.js index.js page (with SSR):

import { useEffect } from 'react';
import * as Realm from 'realm-web';
import nookies from 'nookies';
import { ApolloClient, InMemoryCache, HttpLink, gql } from '@apollo/client';

// 2. Function to create GraphQL client
const createClient = (token) =>
    new ApolloClient({
        link: new HttpLink({
            ssrMode: true,
            uri: process.env.NEXT_PUBLIC_GRAPHQL_API_ENDPOINT,
            headers: {
                Authorization: `Bearer ${token}`,
            },
        }),
        cache: new InMemoryCache(),
    });

// 3. GraphQL Query used in SSR
const GET_PROPERTIES = gql`
    query GetProperties {
        properties {
            title
        }
    }
`;

// 4. Server-side logic to parse cookie and run query
export async function getServerSideProps(context) {
    const { accessToken } = nookies.get(context);
    const client = createClient(accessToken);

    // Deconstruct `data` from the query response
    const { data } = await client.query({
        query: GET_PROPERTIES,
    });

    return {
        props: { properties: data.properties },
    };
}

// Full page exported that gets the data from SSR
export default function ServerPage({ properties }) {
    const app = useApp();
    // note: useEffect runs in the browser but does not run during server-side rendering
    useEffect(() => {
        // If no logged in user, log in
        if (app && !app.currentUser) {
            const anonymousUser = Realm.Credentials.anonymous();
            app.logIn(anonymousUser);
        }
    }, [app, app?.currentUser]);

    return (
        <main className="bg-slate-100 flex justify-center">
            <div className="w-3/6">
                <div className="flex justify-center mt-10 mb-6">
                    <h2 className="inline-block text-center text-2xl sm:text-3xl font-extrabold text-slate-900 tracking-tight">
                        Server-Side Propertiers
                    </h2>
                </div>
                <div>
                    <ul>
                        {properties ? (
                            properties.map((property) => (
                                <li
                                    key="math.random()"
                                    className="bg-white rounded-md border border-slate-200/75 my-2 p-4 leading-normal"
                                >
                                    {property.title}
                                </li>
                            ))
                        ) : (
                            <p>None found</p>
                        )}
                    </ul>
                </div>
            </div>
        </main>
    );
}

The only thing I've added in addition to the guide is the useEffect to actually log the user in. It seemed to me this was missing but the result is the same either way, with or without the useEffect.

_app.js:

import '@/styles/globals.css';

import { useApp } from '@/hooks/useApp';
import { setCookie } from 'nookies';
import { useEffect } from 'react';

function MyApp({ Component, pageProps }) {
    const app = useApp();

    // Reset the user access token in cookies on a regular interval
    useEffect(() => {
        const user = app?.currentUser;
        if (user) {
            setCookie(null, 'accessToken', user.accessToken);
            // Refresh token before session expires
            const TWENTY_MIN_MS = 1200000;
            const resetAccessToken = setInterval(async () => {
                await app?.currentUser?.refreshCustomData();
                setCookie(null, 'accessToken', user.accessToken);
            }, TWENTY_MIN_MS);
            // Clear interval setting access token whenever component unmounts or
            // there's a change in user.
            return () => clearInterval(resetAccessToken);
        }
    }, [app, app?.currentUser]);

    return (
        <>
            <Component {...pageProps} app={app} />
        </>
    );
}

export default MyApp;

hooks/useApp.js simply retrieves the Atlas app ID:

import { useEffect, useState } from 'react';
import * as Realm from 'realm-web';
export function useApp() {
    const [app, setApp] = useState(null);
    // Run in useEffect so that App is not created in server-side environment
    useEffect(() => {
        setApp(Realm.getApp(process.env.NEXT_PUBLIC_APP_ID));
    }, []);
    return app;
}

I've also setup a StackBlitz here to show it in full (although .env variables would need to be replaced).

To clarify, I'm trying to authenticate with MongoDB Atlas App Services using anonymous authentication via the Realm Web SDK (which supports all Next.js rendering modes including CSR, SSR, and SSG). I'm using Apollo Client to make the GraphQL request and then I want to render the response server-side.

I can also confirm that I have enabled anonymous authentication on my API and I can successfully make the request using the same App ID and endpoint via Postman. The response comes back fine.

Is there anything I am missing?

0

There are 0 best solutions below