NOTE: Houdini is in the early phases of development. Please create an issue or start a discussion if you run into problems. For more information on what's coming for this project, you can visit the roadmap.
If you are interested in helping out, please reach out to @AlecAivazis on the Svelte discord. There's lots to do regardless of how deep you want to dive 🙂
- Composable and colocated data requirements for your components
- Normalized cache with declarative updates
- Generated types
- Subscriptions
- Support for Sapper and SvelteKit's public beta
At its core, houdini seeks to enable a high quality developer experience without compromising bundle size. Like Svelte, houdini shifts what is traditionally handled by a bloated runtime into a compile step that allows for the generation of an incredibly lean GraphQL abstraction for your application.
- Example
- Installation
- Configuring Your Application
- Running the Compiler
- Fetching Data
- Fragments
- Mutations
- Subscriptions
- Authentication
- Notes, Constraints, and Conventions
A demo can be found in the example directory.
Please note that the examples in that directory and this readme showcase the typescript definitions generated by the compiler. While it is highly recommended, Typescript is NOT required in order to use houdini.
houdini is available on npm.
yarn add -D houdini houdini-preprocess
# or
npm install --save-dev houdini houdini-preprocess
Adding houdini to an existing project can easily be done with the provided command-line tool. If you don't already have an existing app, visit this link for help setting one up. Once you have a project and want to add houdini, execute the following command:
npx houdini init
This will create a few necessary files, as well as pull down a json representation of your API's schema. Next, add the preprocessor to your sapper setup. Don't forget to add it to both the client and the server configurations if you're using sapper.
import houdini from 'houdini-preprocess'
// somewhere in your config file
{
plugins: [
svelte({
preprocess: [houdini()],
}),
]
}
With that in place, the only thing left to configure your Sapper application is to connect your client and server to the generate network layer:
// in both src/client.js and src/server.js
import { setEnvironment } from '$houdini'
import env from './environment'
setEnvironment(env)
We need to define an alias so that your codebase can import the generated runtime. Add the following
values to svelte.config.js
:
{
kit: {
vite: {
resolve: {
alias: {
$houdini: path.resolve('.', '$houdini')
}
}
}
}
}
And finally, we need to configure our application to use the generated network layer. To do
this, add the following block of code to src/routes/__layout.svelte
:
<script context="module">
import env from '../environment';
import { setEnvironment } from '$houdini';
setEnvironment(env);
</script>
You might need to generate your runtime in order to fix typescript errors.
The compiler is responsible for a number of things, ranging from generating the actual runtime
to creating types for your documents. Running the compiler can be done with npx or via a script
in package.json
and needs to be run every time a GraphQL document in your source code changes:
npx houdini generate
The generated runtime can be accessed by importing $houdini
anywhere in your application.
If you have updated your schema on the server, you can pull down the most recent schema before generating your runtime by using --pull-schema
or -p
:
npx houdini generate --pull-schema
If you are working on an application that isn't using SvelteKit or Sapper, you have to configure the
compiler and preprocessor to generate the correct logic by setting the framework
field in your
config file to "svelte"
. You should also use this setting if you are building a SvelteKit application
in SPA mode.
Grabbing data from your API is done with the query
function:
<script lang="ts">
import { query, graphql, AllItems } from '$houdini'
// load the items
const { data } = query<AllItems>(graphql`
query AllItems {
items {
id
text
}
}
`)
</script>
{#each $data.items as item}
<div>{item.text}</div>
{/each}
At the moment, query variables are declared as a function in the module context of your component.
This function must be named after your query and in a sapper application, it takes the same arguments
that are passed to the preload
function described in the Sapper
documentation. In a SvelteKit project, this function takes the same arguments passed to the load
function
described in the SvelteKit docs. Regardless of the framework, you can return
the value from this.error
and this.redirect
in order to change the behavior of the response. Here is a
modified example from the demo:
// src/routes/[filter].svelte
<script lang="ts">
import { query, graphql, AllItems } from '$houdini'
// load the items
const { data } = query<AllItems>(graphql`
query AllItems($completed: Boolean) {
items(completed: $completed) {
id
text
}
}
`)
</script>
<script context="module" lang="ts">
// This is the function for the AllItems query.
// Query variable functions must be named <QueryName>Variables.
export function AllItemsVariables(page): AllItems$input {
// make sure we recognize the value
if (!['active', 'completed'].includes(page.params.filter)) {
return this.error(400, "filter must be one of 'active' or 'completed'")
}
return {
completed: page.params.filter === 'completed',
}
}
</script>
{#each $data.items as item}
<div>{item.text}</div>
{/each}
The methods used for tracking the loading state of your queries changes depending
on the context of your component. For queries that live in routes (ie, in
/src/routes/...
), the actual query happens in a load
function as described
in What about load?. Because of this, the best way to track
if your query is loading is to use the
navigating store exported from $app/stores
:
// src/routes/index.svelte
<script>
import { query } from '$houdini'
import { navigating } from '$app/stores'
const { data } = query(...)
</script>
{#if $navigating}
loading...
{:else}
data is loaded!
{/if}
However, since queries inside of non-route components (ie, ones that are not defined in /src/routes/...
)
do not get hoisted to a load
function, the recommended practice to is use the store returned from
the result of query:
// src/components/MyComponent.svelte
<script>
import { query } from '$houdini'
const { data, loading } = query(...)
</script>
{#if $loading}
loading...
{:else}
data is loaded!
{/if}
Refetching data is done with the refetch
function provided from the result of a query:
<script lang="ts">
import { query, graphql, AllItems } from '$houdini'
// load the items
const { refetch } = query<AllItems>(graphql`
query AllItems($completed: Boolean) {
items(completed: $completed) {
id
text
}
}
`)
let completed = true
$: refetch({ completed })
</script>
<input type=checkbox bind:checked={completed}>
Don't worry - that's where the preprocessor comes in. One of its responsibilities is moving the actual
fetch into a load
. You can think of the block at the top of this section as equivalent to:
<script context="module">
export async function load() {
return {
_data: await this.fetch({
text: `
query AllItems {
items {
id
text
}
}
`
}),
}
}
</script>
<script>
export let _data
const data = readable(_data, ...)
</script>
{#each $data.items as item}
<div>{item.text}</div>
{/each}
Your components will want to make assumptions about which attributes are
available in your queries. To support this, Houdini uses GraphQL fragments embedded
within your component. Take, for example, a UserAvatar
component that requires
the profilePicture
field of a User
:
// components/UserAvatar.svelte
<script lang="ts">
import { fragment, graphql, UserAvatar } from '$houdini'
// the reference will get passed as a prop
export let user: UserAvatar
const data = fragment(graphql`
fragment UserAvatar on User {
profilePicture
}
`, user)
</script>
<img src={$data.profilePicture} />
This component can be rendered anywhere we want to query for a user, with a guarantee that all necessary data has been asked for:
// src/routes/users.svelte
<script>
import { query, graphql, AllUsers } from '$houdini'
import { UserAvatar } from 'components'
const { data } = query<AllUsers>(graphql`
query AllUsers {
users {
id
...UserAvatar
}
}
`)
</script>
{#each $data.users as user}
<UserAvatar user={user} />
{/each}
It's worth mentioning explicitly that a component can rely on multiple fragments at the same time so long as the fragment names are unique and prop names are different.
Mutations are defined in your component like the rest of the documents but instead of triggering a network request when called, you get a function which can be invoked to execute the mutation. Here's another modified example from the demo:
<script lang="ts">
import { mutation, graphql, UncheckItem } from '$houdini'
let itemID: string
const uncheckItem = mutation<UncheckItem>(graphql`
mutation UncheckItem($id: ID!) {
uncheckItem(item: $id) {
item {
id
completed
}
}
}
`)
</script>
<button on:click={() => uncheckItem({ id: itemID })}>
Uncheck Item
</button>
Note: mutations usually do best when combined with at least one fragment grabbing the information needed for the mutation (for an example of this pattern, see below.)
When a mutation is responsible for updating fields of entities, houdini
should take care of the details for you as long as you request the updated data alongside the
record's id. Take for example, an TodoItemRow
component:
<script lang="ts">
import { fragment, mutation, graphql, TodoItemRow } from '$houdini'
export let item: TodoItemRow
// the resulting store will stay up to date whenever `checkItem`
// is triggered
const data = fragment(
graphql`
fragment TodoItemRow on TodoItem {
id
text
completed
}
`,
item
)
const checkItem = mutation<CompleteItem>(graphql`
mutation CompleteItem($id: ID!) {
checkItem(item: $id) {
item {
id
completed
}
}
}
`)
</script>
<li class:completed={$data.completed}>
<input
name={$data.text}
class="toggle"
type="checkbox"
checked={$data.completed}
on:click={handleClick}
/>
<label for={$data.text}>{$data.text}</label>
<button class="destroy" on:click={() => deleteItem({ id: $data.id })} />
</li>
Adding and removing records from a list is done by mixing together a few different generated fragments
and directives. In order to tell the compiler which lists are targets for these operations, you have to
mark them with the @connection
directive and provide a unique name:
query AllItems {
items @connection(name: "All_Items") {
id
}
}
It's recommended to name these connections with a different casing convention than the rest of your application to distinguish the generated fragments from those in your codebase.
With this field tagged, any mutation that returns an Item
can be used to insert items in this list:
mutation NewItem($input: AddItemInput!) {
addItem(input: $input) {
...All_Items_insert
}
}
Any mutation that returns an Item
can also be used to remove an item from the connection:
mutation RemoveItem($input: RemoveItemInput!) {
removeItem(input: $input) {
...All_Items_remove
}
}
Sometimes it can be tedious to remove a record from every single connection that mentions it. For these situations, Houdini provides a directive that can be used to mark a field in the mutation response holding the ID of a record to delete from all connections.
mutation DeleteItem($id: ID!) {
deleteItem(id: $id) {
itemID @Item_delete
}
}
Sometimes you only want to add or remove a record from a connection when an argument has a particular value.
For example, in a todo list you might only want to add the result to the list if there is no filter being
applied. To support this, houdini provides the @when
and @when_not
directives:
mutation NewItem($input: AddItemInput!) {
addItem(input: $input) {
...All_Items_insert @when_not(argument: "completed", value: "true")
}
}
Subscriptions in houdini are handled with the subscription
function exported by your runtime. This function
takes a tagged document, and returns a store with the most recent value returned by the server. Keep in mind
that houdini will keep the cache (and any subscribing components) up to date as new data is encountered.
It's worth mentioning that you can use the same fragments described in the mutation section in order to update houdini's cache with the response from a subscription.
Here is an example of a simple subscription from the example application included in this repo:
<script lang="ts">
import {
fragment,
mutation,
graphql,
subscription,
ItemEntry_item,
} from '$houdini'
// the reference we're passed from our parents
export let item: ItemEntry_item
// get the information we need about the item
const data = fragment(/* ... */)
// since we're just using subscriptions to stay up to date, we don't care about the return value
subscription(
graphql`
subscription ItemUpdate($id: ID!) {
itemUpdate(id: $id) {
item {
id
completed
text
}
}
}
`,
{
id: $data.id,
}
)
</script>
<li class:completed={$data.completed}>
<div class="view">
<input
name={$data.text}
class="toggle"
type="checkbox"
checked={$data.completed}
on:click={handleClick}
/>
<label for={$data.text}>{$data.text}</label>
<button class="destroy" on:click={() => deleteItem({ id: $data.id })} />
</div>
</li>
Houdini can work with any websocket client as long as you can provide an object that satisfies
the SubscriptionHandler
interface as the second argument to the Environment's constructor. Keep in mind
that WebSocket connections only exist between the browser and your API, therefor you must remember to
pass null
when configuring your environment on the rendering server.
If your API supports the graphql-ws
protocol, you can create a
client and pass it directly:
// environment.ts
import { createClient } from 'graphql-ws'
import { browser } from '$app/env'
// in sapper, this would be something like `(process as any).browser`
let socketClient = browser
? new createClient({
url: 'ws://api.url',
})
: null
export default new Environment(fetchQuery, socketClient)
If you are using the deprecated subscriptions-transport-ws
library and associated protocol,
you will have to slightly modify the above block:
// environment.ts
import { SubscriptionClient } from 'subscriptions-transport-ws'
import { browser } from '$app/env'
let socketClient: SubscriptionHandler | null = null
if (browser) {
// instantiate the transport client
const client = new SubscriptionClient('ws://api.url', {
reconnect: true,
})
// wrap the client in something houdini can use
socketClient = {
subscribe(payload, handlers) {
// send the request
const { unsubscribe } = client.request(payload).subscribe(handlers)
// return the function to unsubscribe
return unsubscribe
},
}
}
export default new Environment(fetchQuery, socketClient)
houdini defers to Sapper's sessions for authentication. Assuming that the session has been populated somehow, you can access it through the second argument in the environment definition:
//src/environment.ts
import { Environment } from '$houdini'
// this function can take a second argument that will contain the session
// data during a request or mutation
export default new Environment(async function ({ text, variables = {} }, session) {
const result = await this.fetch('http://localhost:4000', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': session.token ? `Bearer ${session.token}` : null,
},
body: JSON.stringify({
query: text,
variables,
}),
})
// parse the result as json
return await result.json()
})
- The compiler must be run every time the contents of a
graphql
tagged string changes - Every GraphQL Document must have a name that is unique
- Variable functions must be named after their query
- Documents with a query must have only one operation in them
- Documents without an operation must have only one fragment in them