Architecture
Houdini is a highly customizable framework for building GraphQL applications. It does come with a basic set of assumptions that we believe are important, however it relies on plugins to make this core integrate as closely as possible with your application.
This document is meant to illustrate the high level organization of Houdini and is meant for people who are looking to extend the framework or contribute to the project. Once you are some-what comfortable with this document, head over to the Codegen Plugins reference for a deeper dive in how to build your own extensions. As always, if you have any questions, please join us on discord and ask.
Overview
Our solution is broken up into 3 parts:
- HoudiniClient: The heart of your application. It’s built out of a composable structure that defines your documents’ behavior (query the API, cache the result, mutations, etc). Out of the box, it contains a normalized cache, garbage collector, optimistic updates, pagination, and much more.
- Codegen: A code generation tool that’s responsible for taking your GraphQL documents and generating all of the necessary files that the runtime needs to perform its job. Plugins can also hook into this pipeline and generate their own documents or runtime utilities. This is incredibly helpful when combined with custom file transformations.
- Source Transformations: Plugins can define transformations that change your project’s files from an ergonomic API to whatever it takes to support the desired behavior.
As a guiding principal, Houdini tries to shift as much processing and logic into a build/compile step. This significantly trims down your application’s bundle size since we can avoid including things like your schema or extraneous logic to parse your queries. As a loose rule, the more we can do at compile time, the less our browsers are responsible for.
For more info on HoudiniClient
, check out the Client Plugin docs. For the code generation pipeline and source transformations,
check out the Codegen Plugin docs.
An Example
Let’s see this in practice with a concrete example: a Svelte component. Don’t worry if you don’t know Svelte - the actual contents and syntax should translate to any framework. But, just in case it’s not clear, this component fires a query and renders the name of every user separated by a comma:
<script lang="ts">
import { graphql } from '$houdini'
const UserList = graphql(`
query UserList @load {
users {
name
}
}
`)
</script>
{$UserList.data?.users?.map(user => user.name).join(', ')}
<script>
import { graphql } from '$houdini'
const UserList = graphql(`
query UserList @load {
users {
name
}
}
`)
</script>
{$UserList.data?.users?.map(user => user.name).join(', ')}
This small example contains a lot of things:
- The user defines a query by passing a string to the
graphql
function. This function has an overloaded definition that returns a specific store for the given string. - The code generator parses, validates and creates an internal representation of the document that the cache needs to process the result. The svelte plugin generates the store in the previous step from this representation.
- The component content is transformed to automatically load the query when the component mounts.
It might help to see the transformed version. This does contain a little more Svelte
but hopefully its clear that the function passed to onMount
happens when the component
mounts:
<script lang="ts">
import { graphql } from '$houdini'
import { UserListStore } from '$houdini'
const UserList = new UserListStore()
onMount(() => {
UserList.fetch()
})
</script>
{$UserList.data?.users?.map(user => user.name).join(', ')}
<script>
import { graphql } from '$houdini'
import { UserListStore } from '$houdini'
const UserList = new UserListStore()
onMount(() => {
UserList.fetch()
})
</script>
{$UserList.data?.users?.map(user => user.name).join(', ')}
With this in mind, we can now be a little more explicit about what we have to do:
- Generate an overloaded definition for
graphql
for typescript that points to a Svelte Store - Generate a
UserListStore
class that will perform the actual loading. Internally, it holds a reference to the query’s internal representation and creates a Document Store which handles the caching and network interactions. - Replace the
graphql
function with an instantiation ofUserListStore
- Add the
onMount
block to load the store
While this example is slightly complex, it does illustrates how Houdini leverages a combination of code generation and file transformations to deliver its core user experience. It’s also a good overview of the kinds of things a plugin might be concerned about.
If you’re interested in building your own plugins, check out the Codegen Plugins reference.