Skip to content

Instantly share code, notes, and snippets.

@MartijnHols
Last active November 12, 2021 10:28
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save MartijnHols/60e1a1db4d82f685c6ac9300ecc9e0de to your computer and use it in GitHub Desktop.
Save MartijnHols/60e1a1db4d82f685c6ac9300ecc9e0de to your computer and use it in GitHub Desktop.
GraphQL queries over a websocket in Node.js (with TypeScript)

GraphQL over websockets doesn't seem to be widely used yet. There are several libraries, articles and docs available but half of them don't work, require additional new technologies or provide non-standard solutions. I tried to find a simple, reliable and scalable solution that didn't need much more knowledge outside of Apollo, GraphQL and WebSockets. I also don't want it to rely on unrelated libraries like Express so I don't lock myself in. It will probably also benefit the server structure to keep Apollo separate from other modules (sort of like a microservice).

Using apollo-server (while not very performant) seems to be one of the few proven technologies for having a GraphQL backend in Node.js. Using that on the server is probably going to provide the quickest road to production, as it's widely used it will probably have decent documentation and issues can probably be solved by a few Google queries. It seems that its subscription system has solutions built-in for future scaling through withFilter, i.e.. load balancing multiple Node.js servers handling socket connections with a shared message queue behind them. It seems a bit cumbersome to write subscriptions, but the scalability is probably going to be worth it. It does mean having to introduce a message queue to my stack, but hopefully that complexity can be delayed until after I get 1000+ users.

Unfortunately subscriptions-transport-ws seems to have a bunch of problems. First of all it has a big "Work in progress!" warning in the readme, suggesting it's not ready for production yet. It also hasn't had any real commits since March 2019 and there are questions about the package's maintenance status. Basically it seems to be discontinued before it ever was finished. Due to these reasons I would recommend avoiding this library if possible.

The Apollo Server documentation comes to our rescue with a section on subscriptions. The setup actually works for queries/mutations over WebSockets too. It provides a regular HTTP endpoint as well. This makes introspection possible, and it allows us to consider running login over HTTP instead of websockets. This may make it easier to recognize users and hopefully make the solution less viable to DOS attacks. This is the setup implemented below.

For the client-side apollo-client is proven technology I know will work well, so is used as a base. To make it work over websockets we need apollo-link-ws which unfortunately uses subscriptions-transport-ws under the hood. Sadly I can't find a better alternative at this time. I decided to stick with it anyway since it is used regularly enough and if it works, I guess it works.

The WebSocket when open will send messages as shown in the image below. Note that these are internals of apollo-link-ws and apollo-server, so technically you don't need to know about this.

image

The code below shows a proof of concept of this combination. I am guessing it will support up to 5000 simultaneous connections out of the box, but I'll be running tests to verify this long before reaching a 1000.

Relevant urls:

import { InMemoryCache } from 'apollo-cache-inmemory' // 1.6.5
import ApolloClient from 'apollo-client' // 2.6.8
import { WebSocketLink } from 'apollo-link-ws' // 1.0.19 requires subscriptions-transport-ws 0.9.16
import gql from 'graphql-tag' // 2.10.1
const GRAPHQL_ENDPOINT = 'ws://localhost:4000/graphql'
const wsLink = new WebSocketLink({
uri: GRAPHQL_ENDPOINT,
options: {
reconnect: true,
},
})
const apolloClient = new ApolloClient({
cache: new InMemoryCache(),
link: wsLink,
})
// Test query
apolloClient
.query({
query: gql`
query Books {
books {
title
author
}
}
`,
variables: {},
})
.then(console.log)
// Test subscription
apolloClient
.subscribe({
query: gql`
subscription onNewItem {
somethingChanged {
id
}
}
`,
variables: {},
})
.subscribe({
next(data) {
console.log(data)
},
})
import { ApolloServer, gql } from 'apollo-server' // 2.9.15
import { PubSub } from 'graphql-subscriptions' // 1.1.0
export const pubsub = new PubSub()
// A schema is a collection of type definitions (hence "typeDefs")
// that together define the "shape" of queries that are executed against
// your data.
export const schema = gql`
# Comments in GraphQL strings (such as this one) start with the hash (#) symbol.
# This "Book" type defines the queryable fields for every book in our data source.
type Book {
title: String
author: String
}
# The "Query" type is special: it lists all of the available queries that
# clients can execute, along with the return type for each. In this
# case, the "books" query returns an array of zero or more Books (defined above).
type Query {
books: [Book]
}
type Subscription {
somethingChanged: Result
}
type Result {
id: String
}
schema {
query: Query
subscription: Subscription
}
`
export const books = [
{
title: 'Harry Potter and the Chamber of Secrets',
author: 'J.K. Rowling',
},
{
title: 'Jurassic Park',
author: 'Michael Crichton',
},
]
export const SOMETHING_CHANGED_TOPIC = 'something_changed'
export const resolvers = {
Query: {
books: () => books,
},
Subscription: {
somethingChanged: {
subscribe: () => pubsub.asyncIterator(SOMETHING_CHANGED_TOPIC),
},
},
}
const server = new ApolloServer({
typeDefs: schema,
resolvers,
subscriptions: {
onConnect: (connectionParams, webSocket) => {
console.log(connectionParams, webSocket)
},
},
})
server.listen().then(({ url, subscriptionsUrl }) => {
console.log(`🚀 Server ready at ${url}`)
console.log(`🚀 Subscriptions ready at ${subscriptionsUrl}`)
setInterval(() => {
pubsub.publish(SOMETHING_CHANGED_TOPIC, { somethingChanged: { id: '123' } })
}, 1000)
})
@MartijnHols
Copy link
Author

https://github.com/enisdenjo/graphql-ws/ is the go-to solution nowadays

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment