← Back to DevBytes

Server-Side Rendering with GraphQL Yoga: SSR, SSG, ISR

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:

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

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.

— Ad —

Google AdSense will appear here after approval

← Back to all articles