I am working with a NextJS project that needs to access a graphql API. I am using the nextjs app router and am trying to follow the best practices outlined here apollo-client-integrations/packages/nextjs/README.md at 1affb2b75c55ae2eeec4abf3833162a7d49a636c · apollographql/apollo-client-integrations · GitHub . However, I am running into some issues when it comes to authenticating with cookies.
A cookie is set when the user logs in, and the token from the cookie is inserted into the header of the graphql request before it is sent.
This is pretty simple in the RSC context:
// ApolloClient.ts
import { ApolloLink, from, HttpLink } from "@apollo/client";
import {
ApolloClient,
InMemoryCache,
registerApolloClient,
} from "@apollo/client-integration-nextjs";
import { cookies } from "next/headers";
const authLink = new ApolloLink((operation, forward) => {
operation.setContext(
async ({ headers }: { headers: { [key: string]: string } }) => {
const cookieStore = await cookies();
const token = cookieStore.get("token")?.value;
return {
headers: {
...headers,
authorization: token ? `Bearer ${token}` : "",
},
};
},
);
return forward(operation);
});
const httpLink = new HttpLink({
// this needs to be an absolute url, as relative urls cannot be used in SSR
uri: "http://localhost:4000/gql",
fetchOptions: {
// you can pass additional options that should be passed to `fetch` here,
// e.g. Next.js-related `fetch` options regarding caching and revalidation
// see https://nextjs.org/docs/app/api-reference/functions/fetch#fetchurl-options
},
});
export const link = from([authLink, httpLink]);
export const { getClient, query, PreloadQuery } = registerApolloClient(() => {
return new ApolloClient({
cache: new InMemoryCache(),
link: link,
});
});
However, things are not so simple when it comes to the Apollo client wrapper. In the NextJS middleware, all requests going to /graphql
are proxied and forwarded to the external graphql server so we can get the token from the request’s cookies.
Here is the middleware:
// middleware.ts
import { type NextRequest, NextResponse } from "next/server";
export async function middleware(request: NextRequest) {
const response = NextResponse.next({
request,
});
const token = request.cookies.get("token")?.value;
if (request.nextUrl.pathname === "/graphql" && request.method === "POST") {
// Clone request and add Authorization header
const modifiedRequest = token
? new Request(request, {
headers: {
...Object.fromEntries(request.headers),
Authorization: `Bearer ${token}`,
},
})
: request;
// Forward to External GraphQL server
return NextResponse.rewrite(
new URL("http://localhost:4000/gql"),
{
request: modifiedRequest,
},
);
}
return response;
}
export const config = {
matcher: [
/*
* Match all request paths except:
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
* - images - .svg, .png, .jpg, .jpeg, .gif, .webp
* Feel free to modify this pattern to include more paths.
*/
"/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)",
],
};
Now here is where the problems start. In the ApolloWrapper, the URI needs to be an absolute URL because it runs in an SSR pass before being run in the browser. This creates a challenge because cookies are only included when the URL is a relative URL and it is run in the browser.
Here is the wrapper:
// ApolloWrapper.ts
"use client";
import { HttpLink } from "@apollo/client";
import {
ApolloClient,
ApolloNextAppProvider,
InMemoryCache,
} from "@apollo/client-integration-nextjs";
let link = new HttpLink({
uri: "http://localhost:3000/graphql",
credentials: "include",
});
function makeClient() {
return new ApolloClient({
cache: new InMemoryCache(),
link,
});
}
export function ApolloWrapper({ children }: React.PropsWithChildren) {
return (
<ApolloNextAppProvider makeClient={makeClient}>
{children}
</ApolloNextAppProvider>
);
}
I tried formatting the link like the following:
const isServer = typeof window === "undefined";
let link = new HttpLink({
uri: isServer ? "http://localhost:3000/graphql" : "/graphql",
credentials: "include",
});
However, when running in the SSR pass, there are no cookies set because it is running on the server.
So I tried:
const isServer = typeof window === "undefined";
let link = new HttpLink({
uri: "/graphql",
credentials: "include",
});
if (isServer) {
link = require("ApolloClient").link; // from first code snippet
}
This, of course, throws an error because the link from ApolloClient.ts that runs on the server references next/headers
and ApolloWrapper
is marked with “use client”
.
Is there a better way to do this? Am I missing something here?