Skip to content

Server Side Rendering (SSR) ​

This guide covers how to implement Server Side Rendering with Apollo Runes in SvelteKit applications.

Overview ​

Apollo Runes provides seamless SSR support by allowing you to:

  • Create Apollo clients that work on both server and client
  • Pre-fetch data on the server for faster initial page loads
  • Hydrate client-side components with server-fetched data
  • Maintain consistent state between server and client

Setting up Apollo Client ​

1. Create the Apollo Client Factory ​

First, create a factory function to generate Apollo clients for both server and client environments:

ts
// lib/apollo.ts
import { browser } from "$app/environment";
import { ApolloClient, HttpLink, InMemoryCache } from "@apollo/client";

interface ApolloClientParams {
  token?: string;
}

export function createApolloClient({ token }: ApolloClientParams = {}) {
  return new ApolloClient({
    ssrMode: !browser,
    link: new HttpLink({
      uri: "http://localhost:3010",
      credentials: "same-origin",
      headers: {
        ...(token && { Authorization: `Bearer ${token}` }),
      },
    }),
    cache: new InMemoryCache(),
  });
}

Important

Apollo Client should create one client per request on the server to avoid data leakage between users.

2. Configure the Apollo Provider ​

Set up the Apollo provider in your root layout:

svelte
<!-- src/routes/+layout.svelte -->
<script lang="ts">
  import { browser } from "$app/environment";
  import { ApolloProvider } from 'apollo-runes';
  import { createApolloClient } from '$lib/apollo';

  let { children } = $props();

  // Only create client on browser for client-side operations
  const client = browser ? createApolloClient({
    token: document.cookie.match(/(^|; )token=([^;]*)/)?.[2]
  }) : null;
</script>

{#if client}
  <ApolloProvider {client}>
    {@render children()}
  </ApolloProvider>
{:else}
  {@render children()}
{/if}

SSR Implementation Strategies ​

Option 1: Per-Route Apollo Client ​

Create a new Apollo client for each server load function:

ts
// src/routes/posts/+page.server.ts
import type { PageServerLoad } from './$types';
import { createApolloClient } from '$lib/apollo';
import { GET_POSTS } from '$lib/queries';

export const load: PageServerLoad = async ({ cookies }) => {
  const client = createApolloClient({ 
    token: cookies.get('token') 
  });

  try {
    const result = await client.query({
      query: GET_POSTS,
      errorPolicy: 'all'
    });

    return {
      posts: result.data,
      // Optionally pass errors for client handling
      ...(result.error && { apolloError: result.error })
    };
  } catch (error) {
    console.error('Failed to load posts:', error);
    return {
      posts: null,
      error: 'Failed to load posts'
    };
  }
};

Then use the pre-fetched data in your component:

svelte
<!-- src/routes/posts/+page.svelte -->
<script lang="ts">
  import { Query } from 'apollo-runes';
  import { GET_POSTS } from '$lib/queries';

  let { data } = $props();

  // Use server data as initial data, enabling client-side updates
  const query = new Query(GET_POSTS, { 
    initialData: data.posts 
  });
</script>

{#if query.loading && !query.data}
  <div>Loading...</div>
{:else if query.error}
  <div>Error: {query.error.message}</div>
{:else if query.data}
  <div>
    {#each query.data.posts as post}
      <article>{post.title}</article>
    {/each}
  </div>
{/if}

For larger applications, use SvelteKit's locals to reduce boilerplate:

ts
// src/hooks.server.ts
import type { Handle } from '@sveltejs/kit';
import { createApolloClient } from '$lib/apollo';

export const handle: Handle = async ({ event, resolve }) => {
  // Create Apollo client once per request
  event.locals.apollo = createApolloClient({ 
    token: event.cookies.get('token') 
  });

  return resolve(event);
};

Update your TypeScript definitions:

ts
// src/app.d.ts
import type { ApolloClient, NormalizedCacheObject } from '@apollo/client';

declare global {
  namespace App {
    interface Locals {
      apollo: ApolloClient<NormalizedCacheObject>;
    }
  }
}

export {};

Now use the shared client in your load functions:

ts
// src/routes/posts/+page.server.ts
import type { PageServerLoad } from './$types';
import { GET_POSTS } from '$lib/queries';

export const load: PageServerLoad = async ({ locals }) => {
  const { apollo } = locals;
  const result = await apollo.query({
    query: GET_POSTS
  });

  return {
    posts: result.data
  };
};

FAQ ​

Why use `Query` when data is already available?

Using Apollo Runes, you can fetch data again, subscribe to updates, or load more items for pagination. The reason to use a query even when data exists is that it expands your options for interacting with that resource.