Svelte, Sapper, GraphQL, and Subscriptions
I recently started a new hobby project, and so I decided to take a quick look at the technologies that were available.
Svelte 3.0 was recently released, and it looks pretty interesting. It has support for Server Side Rendering (for that SEO magic) via a package called Sapper.
I’ve been wanting to explore GraphQL, and this seemed like a good opportunity. Real time updates are important for this project, so I wanted to try GraphQL subscriptions.
Great, technologies decided. Lets just google for a tutorial, or sample app, and get started!
And I ran into my first roadblock. I found a tweet from Rich Harris, the mastermind behind Svelte and Sapper, mentioning express-graphql. I also found a blog post from Hasura mentioning svelte-apollo. But nothing doing the exact combo that I am looking for.
Ok, doing things the hard way. Lets get started. The final code will be available on github here.
First, I followed the instructions on the Sapper website to create the template. I choose Rollup for building and bundling, but you can also use Webpack.
npx degit "sveltejs/sapper-template#rollup" sapper-graphql-subscriptions
cd sapper-graphql-subscriptions
npm install
npm run dev
Next, I added the express-graphql
, graphql
, and
graphql-tools
modules as a dev dependencies, so they
will get bundled at build time.
npm i -D express-graphql graphql graphql-tools
Restarting the development server, I saw some build errors. I fixed these
by moving all three in to the dependencies
section of package.json
. For Sapper, this means that they will be
imported at run time, not build time. I am trying to find the documentation
that explains this, and will update with a link here when I find it.
Now, adding GraphQL support was as easy as adding a graphql.js
file
under routes/
.
import graphqlHTTP from 'express-graphql';
import { makeExecutableSchema } from 'graphql-tools';
const typeDefs = `
type Query {
random: Float
}
`;
const resolvers = {
Query: {
random: () => Math.random(),
}
}
const schema = makeExecutableSchema({typeDefs, resolvers});
export const post = graphqlHTTP({
schema,
pretty: true
});
You can test this using curl:
curl 'http://localhost:3000/graphql' \
-H 'content-type: application/json' \
--data '{"query":"{random}"}'
You should see a response like:
{
"data": {
"random": 0.482803512333974
}
}
Next step, consuming this data. I decided to work on the About
page.
First, I modified about.svelte
, adding this to the top:
<script>
let random = 0;
</script>
and this at the end:
<div>
Random: {random}
</div>
Next, I installed a GraphQL client, Apollo, via apollo-boost
and some utilites via graphql-tag
:
npm i -D apollo-boost graphql-tag
and imported the onMount
method from Svelte, the gpl
method from
graphql-tag
and ApolloClient
from apollo-boost
:
import ApolloClient from 'apollo-boost';
import gql from 'graphql-tag';
import { onMount } from 'svelte';
and added this:
onMount(() => {
const client = new ApolloClient();
client.query({
query: gql`{ random }`
}).then(result => {
random = result.data.random;
});
});
When you load About page, you should see something like this:
Almost there! All that was left was the subscriptions. For this, I needed to
use web sockets. Starting with the backend, I modified server.js
.
I needed to switch from polka
to express
, to have a little more control
of startup. I also needed to use SubscriptionServer
from
subscription-transport-ws
. First, I installed the dependencies:
npm i -D express subscriptions-transport-ws
Unfortunatley, this caused a build error, so I moved them both from
devDependencies
to dependencies
.
Then, I changed the startup to use express
and the http
package:
import express from 'express';
import http from 'http';
const app = express() // You can also use Express
.use(
compression({ threshold: 0 }),
sirv('static', { dev }),
sapper.middleware()
);
const server = http.createServer(app);
server.listen(PORT, err => {
if (err) console.log('error', err);
});
in the listen callback, I created the SubscriptionServer
:
import { execute, subscribe } from 'graphql';
import { SubscriptionServer } from 'subscriptions-transport-ws';
new SubscriptionServer({execute, subscribe, schema},
{
server: server,
path: '/subscriptions'
});
But, we are missing one thing, the schema
. I extracted it from
graphql.js
into _schema.js
:
import { makeExecutableSchema } from 'graphql-tools';
const typeDefs = `
type Query {
random: Float
}
`;
const resolvers = {
Query: {
random: () => Math.random(),
}
}
export const schema = makeExecutableSchema({typeDefs, resolvers});
and imported it into server.js
:
import { schema } from './_schema';
Finally, I defined a subscription in _schema.js
:
type Subscription {
randoms: Float
}
Subscription: {
randoms: {
subscribe: async function* asyncRandomNumbers() {
while (true) {
yield { randoms: Math.random() };
await sleep(1000);
}
}
}
}
Quick note. On the server side, a subscription is implemented as an AsyncIterator
.
I made a quick async generator function that sleeps for 1 second after
returning a random number.
Back to the About
page. Client initialization is a little more
complicated, so I moved it out to a helper file, after installing
more modules:
npm i -D apollo-client apollo-cache-inmemory apollo-link \
apollo-link-http apollo-link-ws apollo-utilities
_client.js
:
import { ApolloClient } from 'apollo-client';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { split } from 'apollo-link';
import { HttpLink } from 'apollo-link-http';
import { WebSocketLink } from 'apollo-link-ws';
import { getMainDefinition } from 'apollo-utilities';
export const createApolloClient = () => {
const httpLink = new HttpLink({
uri: 'http://localhost:3000/graphql'
});
const wsLink = new WebSocketLink({
uri: `ws://localhost:3000/subscriptions`,
options: {
reconnect: true
}
});
const link = split(
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
);
},
wsLink,
httpLink
);
return new ApolloClient({link, cache: new InMemoryCache()});
}
And, in About.svelte
:
let randoms = 0;
...
client.subscribe({
query: gql`subscription { randoms }`
}).subscribe(result => {
randoms = result.data.randoms;
});
...
Randoms: {randoms}
Load the About page, and you should see your results!