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:

src/components/UserList.svelte
<script lang="ts">
    import { graphql } from '$houdini'

    const UserList = graphql(`
        query UserList @load {
            users {
                name
            }
        }
    `)
</script>

{$UserList.data?.users?.map(user => user.name).join(', ')}
src/components/UserList.svelte
<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:

src/components/UserList.svelte
<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(', ')}
src/components/UserList.svelte
<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 of UserListStore
  • 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.