Server-Side Rendering with GraphQL Yoga: SSR, SSG, ISR
Modern web applications demand fast initial page loads, SEO-friendly content, and dynamic data fetching. Combining GraphQL Yoga with rendering strategies like Server-Side Rendering (SSR), Static Site Generation (SSG), and Incremental Static Regeneration (ISR) gives developers a powerful toolkit for building performant applications. In this tutorial, we'll explore how to integrate GraphQL Yoga with each of these rendering approaches using Next.js as our framework of choice.
What Is GraphQL Yoga?
GraphQL Yoga is a fully-featured GraphQL server built on top of the Envelop and GraphQL.js ecosystem. It is framework-agnostic, supports file uploads, subscriptions, and works seamlessly in Node.js, Deno, Bun, and edge environments. Because Yoga can run as a lightweight HTTP handler, it pairs naturally with meta-frameworks like Next.js, Nuxt, and SvelteKit.
Why Rendering Strategy Matters
Choosing the right rendering strategy determines how and when your page HTML is produced. Each approach trades off between freshness, performance, and build time:
- SSR — HTML is generated on every request. Best for highly personalized or frequently changing data.
- SSG — HTML is generated once at build time. Best for content that rarely changes and needs maximum performance.
- ISR — HTML is generated at build time and revalidated in the background at a configurable interval. Best for content that updates periodically but doesn't need real-time freshness.
Setting Up GraphQL Yoga
Let's start by installing the necessary dependencies in a Next.js project.
npm install graphql graphql-yoga @graphql-yoga/render-graphiql
Next, create a GraphQL Yoga server instance that can be reused across API routes and server components. We'll define a simple schema with a list of articles.
// lib/yoga.ts
import { createYoga, createSchema } from 'graphql-yoga';
const typeDefs = /* GraphQL */ `
type Article {
id: ID!
title: String!
body: String!
updatedAt: String!
}
type Query {
articles: [Article!]!
article(id: ID!): Article
}
`;
const articles = [
{
id: '1',
title: 'Understanding SSR',
body: 'Server-side rendering generates HTML on each request.',
updatedAt: '2024-01-15T10:00:00Z',
},
{
id: '2',
title: 'Static Site Generation',
body: 'SSG pre-renders pages at build time for maximum speed.',
updatedAt: '2024-02-20T12:00:00Z',
},
];
const resolvers = {
Query: {
articles: () => articles,
article: (_: unknown, args: { id: string }) =>
articles.find((a) => a.id === args.id),
},
};
export const yoga = createYoga({
schema: createSchema({ typeDefs, resolvers }),
graphqlEndpoint: '/api/graphql',
fetchAPI: { Response },
});
Create the API route handler so Yoga can serve GraphQL requests:
// app/api/graphql/route.ts
import { yoga } from '@/lib/yoga';
export { yoga as GET, yoga as POST };
Server-Side Rendering (SSR) with GraphQL Yoga
SSR fetches data and renders HTML on every request. In Next.js App Router, server components are SSR by default. We can query our Yoga endpoint directly using a fetch call inside the component.
// app/articles/page.tsx
async function fetchArticles() {
const res = await fetch('http://localhost:3000/api/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: `
query Articles {
articles {
id
title
body
updatedAt
}
}
`,
}),
cache: 'no-store',
});
const { data } = await res.json();
return data.articles;
}
export default async function ArticlesPage() {
const articles = await fetchArticles();
return (
<main>
<h1>Articles (SSR)</h1>
<ul>
{articles.map((article) => (
<li key={article.id}>
<h2>{article.title}</h2>
<p>{article.body}</p>
<small>Updated: {article.updatedAt}</small>
</li>
))}
</ul>
</main>
);
}
The cache: 'no-store' option ensures the data is fetched fresh on every request, which is the defining characteristic of SSR. This approach is ideal for dashboards, user-specific content, or any page where data freshness is critical.
Using a Typed GraphQL Client
For larger applications, raw fetch calls become unwieldy. You can use graphql-request or generate typed clients with GraphQL Code Generator. Here's a lightweight wrapper:
// lib/client.ts
const endpoint = 'http://localhost:3000/api/graphql';
export async function gql<T = unknown>(
query: string,
variables?: Record<string, unknown>
): Promise<T> {
const res = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query, variables }),
});
const json = await res.json();
if (json.errors) {
throw new Error(json.errors.map((e: Error) => e.message).join(', '));
}
return json.data;
}
Static Site Generation (SSG) with GraphQL Yoga
SSG pre-renders pages at build time. In Next.js App Router, this is achieved by removing no-store and allowing fetch to cache the response. For dynamic routes, you export a generateStaticParams function.
// app/articles/[id]/page.tsx
import { notFound } from 'next/navigation';
async function fetchArticle(id: string) {
const res = await fetch('http://localhost:3000/api/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: `
query Article($id: ID!) {
article(id: $id) {
id
title
body
updatedAt
}
}
`,
variables: { id },
}),
});
const { data } = await res.json();
return data.article;
}
export async function generateStaticParams() {
const res = await fetch('http://localhost:3000/api/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: `query { articles { id } }`,
}),
});
const { data } = await res.json();
return data.articles.map((a: { id: string }) => ({ id: a.id }));
}
export default async function ArticlePage({
params,
}: {
params: { id: string };
}) {
const article = await fetchArticle(params.id);
if (!article) notFound();
return (
<main>
<h1>{article.title}</h1>
<p>{article.body}</p>
<small>Updated: {article.updatedAt}</small>
</main>
);
}
Because we did not pass cache: 'no-store', Next.js caches the fetch result at build time and generates static HTML. The page is then served as a static asset from a CDN, delivering near-instant load times.
Incremental Static Regeneration (ISR) with GraphQL Yoga
ISR combines the speed of SSG with the freshness of SSR. Pages are generated at build time and revalidated in the background at a specified interval. In the App Router, you control this with the revalidate option on fetch or by exporting a revalidate constant from the page.
// app/articles/isr/page.tsx
async function fetchArticlesISR() {
const res = await fetch('http://localhost:3000/api/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: `
query Articles {
articles {
id
title
body
updatedAt
}
}
`,
}),
next: { revalidate: 60 },
});
const { data } = await res.json();
return data.articles;
}
export const revalidate = 60;
export default async function ISRPage() {
const articles = await fetchArticlesISR();
return (
<main>
<h1>Articles (ISR — revalidates every 60s)</h1>
<ul>
{articles.map((article) => (
<li key={article.id}>
<h2>{article.title}</h2>
<p>{article.body}</p>
<small>Updated: {article.updatedAt}</small>
</li>
))}
</ul>
</main>
);
}
With next: { revalidate: 60 }, the first request after 60 seconds triggers a background regeneration. The user still receives the cached page immediately, and the updated version is served on subsequent requests. This is perfect for blogs, news sites, and product catalogs that update on a predictable cadence.
On-Demand Revalidation
For content that updates unpredictably, you can use on-demand revalidation via an API route. When a mutation occurs in your GraphQL server, trigger a revalidation webhook:
// app/api/revalidate/route.ts
import { revalidatePath } from 'next/cache';
import { NextRequest, NextResponse } from 'next/server';
export async function POST(req: NextRequest) {
const secret = req.headers.get('x-revalidate-secret');
if (secret !== process.env.REVALIDATE_SECRET) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const { path } = await req.json();
revalidatePath(path);
return NextResponse.json({ revalidated: true, path });
}
You can call this endpoint from a Yoga mutation resolver or an external CMS webhook to instantly refresh specific pages without waiting for the time-based interval.
Best Practices
- Colocate your Yoga server — Running Yoga in the same Next.js project reduces network latency and simplifies deployment. For production, consider a dedicated server if your schema is large.
- Use persistent queries — For SSR and ISR, sending full query strings on every request adds overhead. GraphQL Yoga supports persisted queries via
@graphql-yoga/plugin-persisted-operationsto reduce payload sizes. - Set appropriate revalidation windows — A 60-second window works for most content. For breaking news, use on-demand revalidation instead of shrinking the interval.
- Handle errors gracefully — Always check for
errorsin the GraphQL response. A failed fetch during SSG will fail the build, so consider fallback data or try/catch blocks. - Leverage Data Loaders — When SSR triggers multiple nested resolvers, use DataLoader to batch and cache database queries per request, preventing the N+1 problem.
- Secure your endpoints — Add authentication middleware to Yoga for SSR pages that require user context, and protect revalidation endpoints with secrets.
- Monitor cache hit rates — Use tools like Next.js's built-in telemetry or custom headers to track whether pages are served from cache or regenerated, helping you tune revalidation intervals.
Conclusion
GraphQL Yoga's lightweight, framework-agnostic design makes it an excellent companion for modern rendering strategies. By combining Yoga with SSR, you get fresh, personalized content on every request. With SSG, you achieve maximum performance for static content. And with ISR, you strike a balance between speed and freshness by regenerating pages in the background. The key is understanding your data's update frequency and choosing the right strategy for each route — or even mixing them within the same application. With the patterns and best practices covered in this tutorial, you're equipped to build fast, SEO-friendly, and data-driven applications that scale gracefully.