Next.js blurs the line between client and server: data fetching moves into Server Components, mutations can run in Server Actions, and the API itself can live in the same project. After this lesson you will be able to query a GraphQL API from Server Components with caching, host a GraphQL endpoint in a Route Handler, and use Apollo Client only where interactivity requires it.
Server Components run on the server and can await data directly. A plain fetch to the GraphQL endpoint is all that is needed, and Next.js caching options apply to it like any other request:
// lib/gql.ts
export async function gql<T>(query: string, variables?: Record<string, unknown>, revalidate = 60): Promise<T> {
const res = await fetch(process.env.GRAPHQL_URL!, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ query, variables }),
next: { revalidate },
});
const { data, errors } = await res.json();
if (errors?.length) throw new Error(errors[0].message);
return data as T;
}
// app/posts/page.tsx
export default async function PostsPage() {
const { posts } = await gql<{ posts: { id: string; title: string }[] }>(
`query Posts { posts(first: 20) { id title } }`
);
return <ul>{posts.map((p) => <li key={p.id}>{p.title}</li>)}</ul>;
}next: { revalidate: 60 } caches the response for a minute; cache: "no-store" fetches on every request for personalised data. Nothing GraphQL-specific reaches the browser.
If the frontend and API belong to the same team, the GraphQL server can be a Route Handler. GraphQL Yoga works with the Web Request/Response types that Route Handlers use:
// app/api/graphql/route.ts
import { createYoga, createSchema } from "graphql-yoga";
import { typeDefs, resolvers } from "@/graphql/schema";
const { handleRequest } = createYoga({
schema: createSchema({ typeDefs, resolvers }),
graphqlEndpoint: "/api/graphql",
fetchAPI: { Response },
});
export { handleRequest as GET, handleRequest as POST, handleRequest as OPTIONS };Open /api/graphql in the browser to get GraphiQL. Apollo Server can be used the same way through startServerAndCreateNextHandler from @as-integrations/next. Route Handlers are stateless, so build DataLoaders in the context function and avoid the in-memory PubSub; subscriptions need a long-lived process.
Interactive parts such as a live search box still benefit from a client cache. The @apollo/client-integration-nextjs package wires Apollo Client into the App Router, including streaming SSR:
// app/ApolloWrapper.tsx
"use client";
import { HttpLink } from "@apollo/client";
import { ApolloNextAppProvider, ApolloClient, InMemoryCache } from "@apollo/client-integration-nextjs";
function makeClient() {
return new ApolloClient({ cache: new InMemoryCache(), link: new HttpLink({ uri: "/api/graphql" }) });
}
export function ApolloWrapper({ children }: { children: React.ReactNode }) {
return <ApolloNextAppProvider makeClient={makeClient}>{children}</ApolloNextAppProvider>;
}Wrap the relevant layout with ApolloWrapper, then use useQuery and useMutation from @apollo/client/react in Client Components exactly as in a plain React app. The package also exports registerApolloClient for Server Components that must use Apollo.
A form submission does not need Apollo either. A Server Action can call the mutation with the same gql helper (using cache: "no-store"), then call revalidatePath("/posts") so the Server Component re-renders with fresh data. The browser never sees the token used to call the API.
| Need | Use |
|---|---|
| Render data on the server, SEO, cached pages | Server Component with fetch |
| Form submission, then refresh the page | Server Action calling the mutation |
| Live interactivity, optimistic UI, polling | Client Component with Apollo hooks |
How do you cache a GraphQL response fetched in a Server Component for 60 seconds?
fetch; next.revalidate and cache: "no-store" control caching.@apollo/client-integration-nextjs provides ApolloNextAppProvider for Client Components that need a cache.revalidatePath refreshes the page.Next lesson: Testing Resolvers and Operations — unit-test resolver functions, execute operations against a test server, and mock the schema.