Apollo Client with HTTP, WS, and auth

Personally I perceive Apollo to be pretty horrible. The docs are not great1, the tool is too focused on React, and it’s not a holistic drop-in solution. Instead, using Apollo requires to wire up multiple packages which are partially not well-maintained. Lastly, Apollo bloats the client side bundle considerably.

But one can’t undo dependency choices of the past, and migrating to another library can be too costly today. That was the case for me when a Vue CLI v3’s codebase’s dependencies prevented an upgrade to the current Node.js LTS release. The unmaintained vue-cli-plugin-apollo depended on apollo-client v2, which depended on apollo-language-server, which reached end of live in April and only allowed up to Node.js v17. Additionally, the codebase was using the unmaintained subscriptions-transport-ws. There was close to a hundred optimistic Apollo cache updates spread throughout the application code, which would have made a departure from Apollo too costly. I updated the project to @apollo/client v3 with the maintained graphql-ws library for WebSocket connections. This transition thankfully removed 3500 lines of code from yarn.lock and shaved a cool 100mb from the dreaded node_modules.

I’m sure there’s complete guides out there showing how to set up @apollo/client v3 defaulting to HTTP traffic, using WebSockets for subscriptions, and authentication. Today’s Google just didn’t put them on my 1st search results page. So let me share what I came up with:

The following (simplified) code…

// vue-apollo.js
import Vue from 'vue'
import VueApollo from 'vue-apollo'
import {createApolloClient, restartWebsockets} from 'vue-cli-plugin-apollo/graphql-client'

Vue.use(VueApollo)

async function getAuth() {
	// get "isAuthenticated" and "token" from your auth provider code
 	if (isAuthenticated) return ''
 	return 'Bearer ' + token
}

const options = {
	httpEndpoint,
	wsEndpoint,
	websocketsOnly: false, // Only run subscriptions over WS
	getAuth, // Override the way the "Authorization" header is set
}

// Call this in the Vue app file
export function createProvider() {
	const {apolloClient, wsClient} = createApolloClient(options)
	
	// Set "Authorization" header for WS, see https://github.com/vuejs/vue-apollo/issues/520#issuecomment-491036830
	wsClient.connectionParams = async () => {
		const Authorization = await getAuth()
		if (!Authorization) return {}
		return {
			headers: {
				Authorization,
			},
		}
	}
	apolloClient.wsClient = wsClient

	return new VueApollo({defaultClient: apolloClient})
}


// Call this function when the user auth changes
export async function onUserAuthChange(apolloClient) {
	if (apolloClient.wsClient) restartWebsockets(apolloClient.wsClient)
	await apolloClient.resetStore()
}

…became:

import Vue from 'vue'
import VueApollo from 'vue-apollo'
import {ApolloClient, HttpLink, from, split, InMemoryCache} from '@apollo/client/core'
import {setContext} from '@apollo/client/link/context'
import {GraphQLWsLink} from '@apollo/client/link/subscriptions'
import {getMainDefinition} from '@apollo/client/utilities'
import {createClient} from 'graphql-ws'

Vue.use(VueApollo)

async function getAuth() {
	// get "isAuthenticated" and "token" from your auth provider code
	if (isAuthenticated) return ''
	return 'Bearer ' + token
}

const getHeaders = async () => {
	const Authorization = await getAuth()
	if (!Authorization) return {}
	return {
		headers: {
			Authorization,
		},
	}
}

const httpLink = new HttpLink({
	uri: httpEndpoint,
})

const wsClient = createClient({
	url: wsEndpoint,
	connectionParams: getHeaders,
})
const wsLink = new GraphQLWsLink(wsClient)

const authMiddleware = setContext(getHeaders)

const link = from([
	authMiddleware,
	split(
		// Only use WS for subscriptions
		({query}) => {
			const definition = getMainDefinition(query)
			return definition.kind === 'OperationDefinition' && definition.operation === 'subscription'
		},
		wsLink,
		httpLink
	),
])

export function createProvider() {
	const apolloClient = new ApolloClient({
		link,
		cache: new InMemoryCache(),
	})
	return new VueApollo({
		defaultClient: apolloClient,
	})
}

// Call this function when the user auth changes
export async function onUserAuthChange(apolloClient) {
	if (wsClient) await wsClient.dispose()
	await apolloClient.resetStore()
}

Footnotes

  1. The horrible hover super-nav of the docs alone cost me a few strains of hair because it was blocking content in annoying ways: apollo docs navigation