Skip Navigation

Scott Spence

Getting Started with KitQL and GraphCMS

16 min read
Hey! Thanks for stopping by! Just a word of warning, this post is about 2 years old, . If there's technical information in here it's more than likely out of date.

KitQL is a GraphQL client for Svelte. It is a set of tools to help you query GraphQL APIs. I’ve been using it in a couple of projects now and want to share what I have learned. What better way to do that than the (now) classic re-reaction of the GraphCMS blog template with SvelteKit.

First up I want to thank the creator of KitQL for his work. Jean-Yves Couët (or JYC) has done a great job with KitQL and the additional tooling he’s created for use in SvelteKit.

He’s also done a great introductory video over on YouTube with an explanation video for setting up KitQL. The API has changed a bit since the video so I’ll go over how to use that here.

What’s in the box?

KitQL uses the GraphQL Code Generator to generate typed queries, mutations and subscriptions. This means that you can use the built in VS Code intellisense to find fields in queries.

Let’s get set up!

If you haven’t got the GraphQL VS Code extension mentioned in JYC’s video installed go install that for the ability to run queries in VS Code.

I have a couple of other projects with this naming convention x-with-sveltekit-and-graphcms where x is the GraphQL client being used. So the projects are identical bar the client used and in this case with KitQL being a TypeScript tool I’ll be using TypeScript in place of JavaScript.

Create a new project

I’ll start by creating the project with the npm init command and pick the following options:

➜ npm init svelte@next kitql-with-sveltekit-and-graphcms

✔ Which Svelte app template? › Skeleton project
✔ Use TypeScript? … Yes
✔ Add ESLint for code linting? … Yes
✔ Add Prettier for code formatting? … Yes
✔ Add Playwright for browser testing? … Yes

I’ll follow the rest of the steps from the CLI output to get set up!

Next steps:
  1: cd kitql-with-sveltekit-and-graphcms
  2: npm install (or pnpm install, etc)
  3: git init && git add -A && git commit -m "Initial commit" (optional)
  4: npm run dev -- --open

I’m using pnpm in these examples.

Create a .env file for my GraphCMS content API endpoint.

touch .env

In the .env file I’ll create a VITE_ environment variable for the GraphCMS content API endpoint:

VITE_GRAPHQL_API=https://api.graphcms.com/v2/projectid/master

Install dependencies and configure

Then I’ll need to install KitQL and the JavaScript implementation of GraphQL.

pnpm i -D @kitql/all-in graphql

Create a .graphqlrc.yaml file:

touch .graphqlrc.yaml

I’ve taken the config for the file here from the KitQL all-in docs. To be able to do code gen on this there needs to be a schema for use in the .graphqlrc.yaml file. The schema can be either a local file or a remote URL, I’m pointing this to the public content API of the GraphCMS blog template.

projects:
  default:
    schema: https://api.graphcms.com/v2/projectid/master
    documents:
      - '**/*.gql'
    extensions:
      endpoints:
        default:
          url: https://api.graphcms.com/v2/projectid/master
      codegen:
        generates:
          ./src/lib/graphql/_kitql/graphqlTypes.ts:
            plugins:
              - typescript
              - typescript-operations
              - typed-document-node
              - typescript-document-nodes

          ./src/lib/graphql/_kitql/graphqlStores.ts:
            plugins:
              - '@kitql/graphql-codegen'
            config:
              importBaseTypesFrom: $lib/graphql/_kitql/graphqlTypes

        config:
          useTypeImports: true

You may be thinking, wait, why is the content API hardcoded in here? If you watched JYC’s video you would have seen him take the schema from the browser network tab and put in into a local schema.json file.

I won’t be committing the API endpoints to git as I don’t really want to be adding them to a public repo. Even though the is publicly accessible. I’ll replace the schema and url properties with ${VITE_GRAPHQL_API} before I commit the file to git.

There’s then the sveltekit.config.js file. In here I’ll need to add and configure the vite-plugin-watch-and-run, take note here if you’re not using pnpm you may want to change the run command.

This plugin is really handy, with this configuration it will watch for any changes to GraphQL files and then run the gen, package.json script.

import watchAndRun from '@kitql/vite-plugin-watch-and-run'
import adapter from '@sveltejs/adapter-auto'
import preprocess from 'svelte-preprocess'

/** @type {import('@sveltejs/kit').Config} */
const config = {
  // Consult https://github.com/sveltejs/svelte-preprocess
  // for more information about preprocessors
  preprocess: preprocess(),

  kit: {
    adapter: adapter(),
    vite: {
      plugins: [
        watchAndRun([
          {
            watch: '**/*.(gql|graphql)',
            run: 'pnpm run gen',
          },
        ]),
      ],
    },
  },
}

export default config

Now that pnpm run gen is added to vite-plugin-watch-and-run and being called each time there’s a change to any GraphQL files, I’ll need to add it to the scripts in the package.json file.

"scripts": {
  "prepare": "pnpm run gen",
  "dev": "svelte-kit dev --port 3777",
  "gen": "graphql-codegen --config ./.graphqlrc.yaml",
},

Because I’m using pnpm I’ll also need to amend the .npmrc file:

-engine-strict=true
+save-exact=true
+node-linker=hoisted

Alright! Nearly done!

If I run the dev script (pnpm run dev) command now (which will call the gen script with vite-plugin-watch-and-run) I’ll get some error output from graphql-codegen because I’ve not created any GraphQL queries!

I’ll create that folder structure now, note the -p flag will create parent folders as well. So this command creates the lib, graphql and _kitql folders.

mkdir src/lib/graphql/_kitql -p

In the src/lib/graphql folder I’ll create an all-posts.gql file:

touch src/lib/graphql/all-posts.gql

this is to list all the posts in the blog on the index page:

query AllPosts {
  posts {
    title
    slug
    date
    excerpt
    tags
    coverImage {
      url(
        transformation: {
          image: { resize: { fit: clip, height: 369, width: 656 } }
        }
      )
    }
  }
}

Now with the dev script running vite-plugin-watch-and-run will do it’s thing and run the gen script. This creates the graphqlStores.ts and graphqlTypes.ts files in the _kitql folder.

Taking a look at the graphqlStores.ts file now I can see that there is a function for KQL_AllPostsStore() which exports KQL_AllPosts.

The function has all the methods on it I’ll need for working with the data.

Additional config for Tailwind

The rest of the code examples will be using Tailwind. If you’re following along and you’re not into Tailwind you can skip this bit.

Don’t like Tailwind? That’s cool, you do you. 😊 It’s not relevant to this example, really. I do want this to be a guide you can follow to have the same result as what I have put on GitHub. If you only want to check out the code there’s a link at the end of the post. 👍

# install and config tailwind
npx svelte-add@latest tailwindcss
# add typography and daisyUI
pnpm i -D @tailwindcss/typography daisyui

The Typography and daisyUI plugins are added to the tailwind.config.cjs file:

const config = {
  content: ['./src/**/*.{html,js,svelte,ts}'],

  theme: {
    extend: {
      typography: {
        DEFAULT: {
          css: {
            maxWidth: null,
          },
        },
      },
    },
  },

  plugins: [require('@tailwindcss/typography'), require('daisyui')],
}

module.exports = config

Create KitQL client

Now I can create the KitQL client to start querying data. Create the .ts file for it first:

touch src/lib/graphql/kitQLClient.ts

Then I’ll add the following configuration to the kitQLClient.ts file:

import { KitQLClient } from '@kitql/client'
const GRAPHQL_ENDPOINT = import.meta.env.VITE_GRAPHQL_API

export const kitQLClient = new KitQLClient({
  url: GRAPHQL_ENDPOINT as string,
  credentials: 'omit',
  headersContentType: 'application/json',
  logType: ['client', 'server', 'operationAndvariables'],
})

Use queried data from graphqlStores

Now for the home page (src/routes/index.svelte) I can use the KQL_AllPosts store and use the queryLoad method.

You may notice there’s two types of query you can use on the store, there’s a query and a queryLoad method that are on the store.

queryLoad is intended for use in a SvelteKit load function whereas with the query is non blocking and can be used in components and pages.

In a SvelteKit load function I can await the query for AllPosts and return an empty object from the load function. It’s empty because once the data has been fetched before the page loads it’s added to a Svelte store for use in the page.

<script lang="ts" context="module">
  import { KQL_AllPosts } from '$lib/graphql/_kitql/graphqlStores'

  export const load = async ({ fetch }) => {
    await KQL_AllPosts.queryLoad({ fetch })
    return {}
  }
</script>

Once the page has loaded I can subscribe (referencing the store value with the $ ) to the KQL_AllPosts store to get the data.

<script lang="ts">
  let posts = $KQL_AllPosts.data?.posts
</script>

I can now use that in a Svelte each expression to render the posts. This is with Tailwind and daisyUI classes.

{#each posts as { title, slug, excerpt, coverImage, tags }}
  <div class="card text-center shadow-2xl mb-20">
    <figure class="">
      <img
        class=""
        src={coverImage.url}
        alt={`Cover image for ${title}`}
      />
    </figure>
    <div class="card-body prose">
      <h2 class="title">{title}</h2>
      <p>{excerpt}</p>
      <div class="flex justify-center mb-5 space-x-2">
        {#each tags as tag}
          <span class="badge badge-primary">{tag}</span>
        {/each}
      </div>
      <div class="justify-center card-actions">
        <a
          sveltekit:prefetch
          href={`/posts/${slug}`}
          class="btn btn-outline btn-primary">Read &rArr;</a
        >
      </div>
    </div>
  </div>
{/each}

Sweet! That’s the index page rendering out all the posts!

I’ll now concentrate on creating the src/routes/posts/[slug].svelte file so clicking the link on the index page will take you to that post.

Passing GraphQL variables

So, clicking a link on the index page will give a 404 because the route for it doesn’t exist yet. That page is going to need the specific data for that post. So I’m going to want to create a GraphQL query for that post passing in the ${slug} as a variable to query on.

I’ll make the query file first, via bash:

touch src/lib/graphql/get-post.gql

Then I’ll add the following to the get-post.gql file:

query GetPost($slug: String!) {
  post(where: { slug: $slug }) {
    title
    date
    tags
    author {
      name
      authorTitle: title
      picture {
        url(
          transformation: {
            image: { resize: { fit: clip, height: 50, width: 50 } }
          }
        )
      }
    }
    content {
      html
    }
    coverImage {
      url
    }
  }
}

Now I’ve created that query, vite-plugin-watch-and-run will do it’s thing (as I have the dev server running) and generate the store and types for that query.

This query is expecting the $slug variable, I’ll need to get that from the context in the load function of the [slug].svelte file. Which doesn’t exist yet.

Soooo, another bash command or two to create the folder and file for the posts route.

# make the posts folder
mkdir src/routes/posts
# create the /posts/[slug].svelte file
touch src/routes/posts/'[slug]'.svelte

I’ll break down the several parts of the posts/[slug].svelte file here, so first up I’ll need to get that slug variable from the context in the load function of the [slug].svelte file. I’ll just go straight in here and destructure the params object (which has the slug value) and also destructure fetch for KitQL to use:

<script lang="ts" context="module">
  import { KQL_GetPost } from '$lib/graphql/_kitql/graphqlStores'

  export const load = async ({ params, fetch }) => {
    const { slug } = params
    if (slug)
      await KQL_GetPost.queryLoad({ fetch, variables: { slug } })
    return {}
  }
</script>

I’m importing the KQL_GetPost store here, and then I’m calling the .queryLoad passing in the fetch and variables object.

Using context="module" means that code will run before the page loads.

When the page loads I will be able to subscribe to the KQL_GetPost store in some <script> tags and get the data I need out of the store to use in the page.

<script lang="ts">
  let post = $KQL_GetPost.data?.post
  const {
    title,
    date,
    tags,
    author: { name, authorTitle, picture },
    content: { html },
    coverImage,
  } = post
</script>

⚠️ code wall incoming! ⚠️

So now I have all I need to render out the post from the KQL_GetPost store.

Here’s what the rest of the posts/[slug].svelte file looks like:

Made it through all that? Good! Ok, so now I have a list of posts that I can click through to from the index page.

I’ll explore a bit now with KitQL and detail what I get out of the box with the configuration I currently have.

Caching in KitQL

Alright, I have something of a project now where I can click a link on the index page to go to the post page.

If I pop open the browser console, do a page reload and take a look at the output I get this:

[KitQL Client] From: SSR,     Operation: KQL_AllPages
[KitQL Client] From: SSR,     Operation: KQL_AllPosts

KitQL is getting the data Server Side Rendered (SSR). For me before the page loads.

Now if hover the mouse over one of the READ → links I get this:

[KitQL Client] From: NETWORK, Operation: KQL_GetPost, Variables: {"slug":"technical-seo-with-graphcms"}

KitQL has got the data from the NETWORK ready to go.

This is because of the sveltekit:prefetch in the a tag, this will go off and run the load function in the [slug].svelte file. So it’s loaded the data before I’ve clicked the link.

Clicking the link will take me to the page with the data already loaded. 🔥

This is a super snappy experience for the user.

Now, because I haven’t added a navbar yet (more on that soon), if I go to the browser ‘back’ button on the browser the console logs this out (on mouse hover):

[KitQL Client] From: CACHE,   Operation: KQL_AllPosts

KitQL is getting the data from the cache for the route. The index page loads up super fast!

There’s a widget!!!

One thing I found from a recent meetup talk from JYC is that there’s also a widget that is part of KitQL.

You can use it to display the store contents on the page with the KitQLInfo component that’s now part of the @kitql/all-in package.

So say I want to check out the contents of the KQL_AllPosts store, in the index page I’ll import the component:

<script lang="ts" context="module">
  import { KQL_AllPosts } from '$lib/graphql/_kitql/graphqlStores'
  import { KitQLInfo } from '@kitql/all-in'
  export const load = async ({ fetch }) => {
    await KQL_AllPosts.queryLoad({ fetch })
    return {}
  }
</script>

Then I can use the component passing in the KQL_AllPosts store:

<KitQLInfo store={KQL_AllPosts} />

Going over to the index page of the project now I can see the KitQLInfo component in it’s initial state:

kitql-kitqlinfo-component-initial

If I click on the store name in the component it expands out with the store data with options to reset the current store and options for where to query the data from:

kitql-kitqlinfo-component-expanded

Super neat!

Add in additional components

With the setup and configuration done now; along with querying data and passing variables to get specific data for a page route this post is effectively done!

The rest of this is just adding in the additional components I need to keep it in line with the rest of the examples for the differing clients.

So that’s a navbar, a footer and a theme switch.

There’s also an additional page src/routes/[slug].svelte file that I’ll use for generating pages from the GraphCMS schema. It has a Pages model that can be used to create additional pages. Currently there’s an about page but with the schema it allows content editors to create new pages via GraphCMS.

The theme switch will need an additional package in the way of theme-change, I’ll install that now:

pnpm i -D theme-change

Then I’ll create the files needed for the navbar, footer, theme select and the page route:

mkdir src/lib/components
touch src/lib/components/{navbar.svelte,footer.svelte,theme-select.svelte}
touch src/routes/'[slug]'.svelte
# create query files for the page route
touch src/lib/graphql/{all-pages.gql,get-page.gql}

I’ll make the AllPages and GetPage queries for the page route and add the code to the all-pages.gql and get-page.gql files.

All pages first:

query AllPages {
  pages {
    title
    slug
    content {
      html
    }
  }
}

Then the get page query:

query GetPage($slug: String!) {
  page(where: { slug: $slug }) {
    title
    content {
      html
    }
  }
}

Next up I’ll add the markup to the theme switch component, this is a list of all the available themes in daisyUI:

The footer, this is taken straight from the daisyUI examples nothing of note here, aesthetics only:

Before I tackle the navbar, I’ll need to implement the theme change feature in the __layout.svelte file. I’m also querying all pages so I can pass that to the navbar and it can generate links from the result of that query:

The navbar is expecting a pages prop, which is the result of the AllPages query from KitQL.

There’s a Svelte each loop in there to create the page name and link for anything being passed into the component.

Lastly I’ll add in the src/routes/[slug].svelte file markup!

That’s it, I have a sweet looking, basic project now!

Conclusion

KitQL is an awesome bit of, ahem, kit!

With all the GraphQL Code Generator plugins added this gives you code intellisense superpowers with VS Code in your SvelteKit project. All packaged up for you in one dependency in the @kitql/all-in package.

This makes working though GraphQL queries in your SvelteKit projects a joy!

I’ve already linked the resources but will put them here for ease of access:

You can check out the source code on GitHub for this post.

Thanks!

There's a reactions leaderboard you can check out too.

Copyright © 2017 - 2024 - All rights reserved Scott Spence