Houdini: the disappearing GraphQL client!
In this post, we'll be taking a look at Houdini and how you could use it in your GraphCMS projects.
Houdini was created to reduce the amount of boilerplate code needed to make GraphQL queries to an API. This reduces the overhead needed for building a SvelteKit project that uses GraphQL. We'll go into detail on this shortly, but first, let's take a look at some of the features you get with Houdini.
- Composable and co-located data requirements for your components
- Normalized cache with declarative updates
- Generated types
- Subscriptions
- Support for SvelteKit and Sapper
- Pagination (cursors and offsets)
(Feature list taken from the Houdini documentation)
Getting set up!Anchor
We'll be using the GraphCMS starter blog template to get started here. You can quickly spin up this template from your GraphCMS dashboard.
In these examples, I'll be using pnpm
you can use npm
or yarn
if you prefer.
# create new svelte project named graphcms-with-houdinipnpm init svelte@next graphcms-with-houdini# change directory into the newly created projectcd graphcms-with-houdini# install dependenciespnpm install# optional init git repogit init && git add -A && git commit -m "Initial commit" (optional)
From the SvelteKit CLI options I'll be choosing:
Which Svelte app template? › Skeleton projectUse TypeScript? › NoAdd ESLint for code linting? › NoAdd Prettier for code formatting? › Yes
With the project initialized, I can add in the dependencies for Houdini as dev dependencies -D
.
pnpm i -D houdini houdini-preprocess
Now that's installed, I can bootstrap the project with the Houdini init
command.
npx houdini init
Here's where I'll be prompted for the project API, you can check out the video here for how to get that:
I'll paste in the content API URL and hit enter, I'll choose SvelteKit from the CLI prompt and I'll accept the default (./schema.graphql
) for where the schema should be written to.
Svelte ConfigurationAnchor
There's a couple of options I'll need to add into the svelte.config.js
file now so Vite (the tool used to build Svelte) can use Houdini in the project.
Here's what my svelte.config.js
file looks like:
import adapter from '@sveltejs/adapter-auto'import houdini from 'houdini-preprocess'import path from 'path'/** @type {import('@sveltejs/kit').Config} */const config = {preprocess: [houdini()],kit: {adapter: adapter(),// hydrate the <div id="svelte"> element in src/app.htmltarget: '#svelte',vite: {resolve: {alias: {$houdini: path.resolve('.', '$houdini'),},},server: {fs: {// Allow serving files from one level up to the project root// https://vitejs.dev/config/#server-fs-allowallow: ['..'],},},},},}export default config
Add Houdini ClientAnchor
Now to have the Houdini client available to the project, I'll need to add this to somewhere where it will be available to the whole project. The SvelteKit __layout
file is a good place to add this.
I'll need to create the layout file, I'll do that with this bash one-liner:
touch src/routes/__layout.svelte
In the newly created __layout.svelte
file, I'll add the following:
<script context="module">import { setEnvironment } from '$houdini'import environment from '../environment'setEnvironment(environment)</script><slot />
Optional stylingAnchor
I will just focus on how to use Houdini here, so I'll skip the styling. If you want to use Tailwind classes in the project, you can use the following to initialise the project to use Tailwind:
npx svelte-add@latest tailwindcss
Fetch the GraphCMS API DataAnchor
I'm going to define the first GraphQL query for use in the project. This is the standard information you'd want to display on a blog landing page.
I'll pop in a <pre>
tag with the posts
object returned from the Houdini query
so I can see the data on the page as a quick way to validate it's working.
<script>import { graphql, query } from '$houdini'const { data } = query(graphql`query AllPosts {posts {titleslugdateexcerpttagscoverImage {url(transformation: { image: { resize: { width: 400, height: 400, fit: clip } } })}}}`)const { posts } = $data</script><pre>{JSON.stringify(posts, null, 2)}</pre>
You may have noticed the $
on the data
object, this is a Svelte store and the $
is how I can subscribe to it.
Now for Houdini to know about that query, I'll need to run the Houdini generate
command:
npx houdini generate
This will throw an error, because Houdini needs to know how to handle the Date
type:
npx houdini generateAllPostsError: Could not convert scalar type: Date
There's a couple of issues on the Houdini GitHub repo detailing how to create a custom scalar type.
Here's what my houdini.config.js
looks like now with the custom scalar added:
/** @type {import('houdini').ConfigFile} */const config = {schemaPath: './schema.graphql',sourceGlob: 'src/**/*.svelte',module: 'esm',framework: 'kit',scalars: {// the name of the scalar we are configuringDate: {// the corresponding typescript type (what the typedef generator leaves behind in the response and operation inputs)type: 'Date',// turn the api's response into that typeunmarshal(val) {const date = new Date(val).toISOString()return date},// turn the value into something the API can usemarshal(date) {return date.getTime()},},},}export default config
If I try the Houdini generate
command again, I'll get the following output:
npx houdini generateAllPosts
Looks like there no issues there now!
As a side note here, I'll be adding in some more queries while building out this example, so what I'll do is add the npx houdini generate
command to the project scripts.
This is so that I don't have to stop the dev server each time I add a new query to have Houdini generate the files needed.
Here's what the scripts look like in my package.json
file now:
"scripts": {"dev": "npx houdini generate && svelte-kit dev","build": "npx houdini generate && svelte-kit build","package": "svelte-kit package","preview": "npx houdini generate && svelte-kit preview","lint": "prettier --ignore-path .gitignore --check --plugin-search-dir=. .","format": "prettier --ignore-path .gitignore --write --plugin-search-dir=. ."},
Ok, I'll run the dev
script again now and check localhost:3000
in my browser to see the data I've added.
On the browser page, I get the following JSON data back from the Houdini query:
[{"title": "Technical SEO with GraphCMS","slug": "technical-seo-with-graphcms","date": "2020-05-05T00:00:00.000Z","excerpt": "Get started with your SEO implementation when using a Headless CMS","tags": ["SEO"],"coverImage": {"url": "https://media.graphcms.com/resize=fit:clip,height:400,width:400/Ey8F3QcRzKVWqn9W7Pl7","id": "ckhz8xs9k1sv60952ql0sflru"},"id": "ckadrcx4g00pw01525c5d2e56"},{"title": "Union Types and Sortable Relations with GraphCMS","slug": "union-types-and-sortable-relations","date": "2020-05-01T00:00:00.000Z","excerpt": "Learn more about Polymorphic Relations and Sortable Relations with GraphCMS","tags": ["Union Types"],"coverImage": {"url": "https://media.graphcms.com/resize=fit:clip,height:400,width:400/OUT7id5vT2XOaLEMAspU","id": "ckhz8z76w1rpy0a53x96s7wkd"},"id": "ckadrfuu000pe0148kels2b5e"}]
Now I have everything I need to build out the landing page.
Add Page MarkupAnchor
After the <script>
tags, I'll use the Svelte Head API to give the page a title.
Some introductory text to explain what the page is about.
Then I'll use the Svelte {#each}
directive to loop through the posts
and display them on the page.
Here's what the file looks like now:
<script>import { graphql, query } from '$houdini'const { data } = query(graphql`query AllPosts {posts {titleslugdateexcerpttagscoverImage {url(transformation: {image: {resize: { width: 400, height: 400, fit: clip }}})}}}`)const { posts } = $data</script><svelte:head><title>Houdini with GraphCMS | Welcome</title></svelte:head><h1>Houdini with GraphCMS</h1><p>An example project using the GraphCMS blog template and Houdini forthe GraphQL client</p>{#each posts as { title, slug, excerpt, coverImage, tags }}<div><figure><img src={coverImage.url} alt={`Cover image for ${title}`} /></figure><div><h2>{title}</h2><p>{excerpt}</p><div>{#each tags as tag}<span>{tag}</span>{/each}</div><div><a sveltekit:prefetch href={`/posts/${slug}`}>Read ⇒</a></div></div></div>{/each}
Now that the landing page information is on there, I have a list of clickable links to take me to the post for more detail.
That route doesn't exist yet so I'll create that now, again with a terminal command:
# make the posts directorymkdir src/routes/posts# make the [slug].svelte filetouch src/routes/posts/'[slug]'.svelte
Now I'll need a way to get the slug from the URL into the PostQuery
in the src/routes/posts/[slug].svelte
file.
Using Query VariablesAnchor
So the query for the post page will need to take a query parameter (slug
) and use that to get the post.
The query will look something like this, I've taken out the author and cover image fields for brevity:
query PostQuery($slug: String!) {post(where: { slug: $slug }) {titledatetagscontent {html}}}
I can use that query much the same way I did it on the index page, following the same pattern.
<script>import { graphql, query } from '$houdini'const { data } = query(graphql`query PostQuery($slug: String!) {post(where: { slug: $slug }) {titledatetags# authorcontent {html}# coverImage}}`)const { post } = $data</script><pre>{JSON.stringify(post, null, 2)}</pre>
Here's where things get a bit specific to Houdini, as Houdini replaces the load
function in SvelteKit with a Variables
function. It takes the same arguments as the load
function, so this is where I can destructure out the params
object to get the slug that can be used in the query.
I'll add this to the top of the file:
<script context="module">export const PostQueryVariables = ({ params }) => {const { slug } = paramsreturn {slug,}}</script>
One thing to note is that the Variables
function will take on the name of the query defined, so on this file the query is PostQuery
so the load function for the page needs to be PostQueryVariables
.
Add [slug] Page MarkupAnchor
Now, I can add in the markup needed to display the post.
Here's what my src/routes/posts/[slug].svelte
file looks like now:
<script context="module">export const PostQueryVariables = ({ params }) => {const { slug } = paramsreturn {slug,}}</script><script>import { graphql, query } from '$houdini'const { data } = query(graphql`query PostQuery($slug: String!) {post(where: { slug: $slug }) {titledatetagsauthor {nameauthorTitle: titlepicture {url(transformation: { image: { resize: { fit: clip, height: 50, width: 50 } } })}}content {html}coverImage {url}}}`)const { post } = $dataconst {title,coverImage,author: { picture, name, authorTitle },date,tags,content: { html },} = post</script><svelte:head><title>Houdini with GraphCMS | {title}</title></svelte:head><img src="{coverImage.url}" alt="{`Cover" image for ${title}`} /><h1>{title}</h1><a href="/" class="inline-flex items-center mb-3"><img src="{picture.url}" alt="{name}" /><span>{name}</span><span>{authorTitle}</span></a><p>{new Date(date).toDateString()}</p>{#if tags} {#each tags as tag}<span>{tag}</span>{/each} {/if}<article>{@html html}</article>
FinAnchor
That's it! I've created a simple blog template using GraphCMS and Houdini.
I hope you found it useful.