GraphQL Yoga v3 documentation (superseded by v5): A GraphQL subscription initiates an event stream, where each event in the stream is pushed from the server to the client that issued the subscription.
A GraphQL subscription initiates an event stream, where each event in the stream is pushed from the
server to the client that issued the subscription.
Example use cases for subscriptions are applications that require real-time data such as chat
applications or stock trading platforms.
GraphQL Yoga uses
server-sent-events
for the subscription protocol. You don’t need any extra packages to use subscriptions.
Quick Start with simple Server-Sent Events (SSE)
Subscriptions can be added by extending your GraphQL schema with a Subscription type.
Subscription countdown implementation
import { createServer } from 'node:http'import { createSchema, createYoga } from 'graphql-yoga'// Provide your schemaconst yoga = createYoga({ schema: createSchema({ typeDefs: /* GraphQL */ ` type Query { hello: String } type Subscription { countdown(from: Int!): Int! } `, resolvers: { Query: { hello: () => 'world' }, Subscription: { countdown: { // This will return the value on every 1 sec until it reaches 0 subscribe: async function* (_, { from }) { for (let i = from; i >= 0; i--) { await new Promise(resolve => setTimeout(resolve, 1000)) yield { countdown: i } } } } } } })})const server = createServer(yoga)server.listen(4000, () => { console.info('Server is running on http://localhost:4000/graphql')})
WHATWG standard EventSource can be
used without any extra packages to handle SSE.
eventsource.ts
const url = new URL('http://localhost:4000/graphql')url.searchParams.append( 'query', /* GraphQL */ ` subscription Countdown($from: Int!) { countdown(from: $from) } `)url.searchParams.append('variables', JSON.stringify({ from: 10 }))const eventsource = new EventSource(url.toString(), { withCredentials: true // This is required for cookies})eventsource.onmessage = function (event) { const data = JSON.parse(event.data) console.log(data) // This will result something like `{ "data": { "countdown": 0 } }`}eventsource.addEventListener('complete', () => { eventsource.close() // If operation ends, close the connection and prevent the client from reconnecting})
Client Usage with Apollo
You can either decide to use the pre-built Yoga Apollo Link, or build your own from scratch. The
Yoga Apollo Link requires you to install an additional package, while the custom link requires you
to write more code. However, by writing the link yourself you get full control over the link and can
customize it to your needs.
Install the @graphql-yoga/apollo-link package.
npm i @graphql-yoga/apollo-link
yarn add @graphql-yoga/apollo-link
pnpm add @graphql-yoga/apollo-link
bun add @graphql-yoga/apollo-link
You can use YogaLink from @graphql-yoga/apollo-link package to use subscriptions together with
file uploads and defer/stream in Apollo Client.
YogaLink apollo client usage
import { ApolloClient, InMemoryCache } from '@apollo/client/core'import { YogaLink } from '@graphql-yoga/apollo-link'const client = new ApolloClient({ link: new YogaLink({ endpoint: 'http://localhost:4000/graphql' }), cache: new InMemoryCache()})
import { getOperationAST, print } from 'graphql'import { ApolloClient, ApolloLink, FetchResult, InMemoryCache, Observable, Operation} from '@apollo/client/core'import { split } from '@apollo/client/link/core'import { HttpLink } from '@apollo/client/link/http'type SSELinkOptions = EventSourceInit & { uri: string }class SSELink extends ApolloLink { constructor(private options: SSELinkOptions) { super() } request(operation: Operation): Observable<FetchResult> { const url = new URL(this.options.uri) url.searchParams.append('query', print(operation.query)) if (operation.operationName) { url.searchParams.append('operationName', operation.operationName) } if (operation.variables) { url.searchParams.append('variables', JSON.stringify(operation.variables)) } if (operation.extensions) { url.searchParams.append('extensions', JSON.stringify(operation.extensions)) } return new Observable(sink => { const eventsource = new EventSource(url.toString(), this.options) eventsource.onmessage = function (event) { const data = JSON.parse(event.data) sink.next(data) if (eventsource.readyState === 2) { sink.complete() } } eventsource.onerror = function (error) { sink.error(error) } eventsource.addEventListener('complete', () => { eventsource.close() // If operation ends, close the connection and prevent the client from reconnecting }) return () => eventsource.close() }) }}const uri = 'http://localhost:4000/graphql'const sseLink = new SSELink({ uri })const httpLink = new HttpLink({ uri })const link = split( ({ query, operationName }) => { const definition = getOperationAST(query, operationName) return definition?.kind === 'OperationDefinition' && definition.operation === 'subscription' }, sseLink, httpLink)const client = new ApolloClient({ link, cache: new InMemoryCache()})
Client Usage with urql
You can either decide to use the pre-built Yoga Urql Exchange, or build your own from scratch. The
Yoga Urql Exchange requires you to install an additional package, while the custom link requires you
to write more code. However, by writing the link yourself you get full control over the link and can
customize it to your needs.
You need to install @urql/exchange-graphql for GraphQL Yoga, and use yogaExchange from that
package.
npm i @graphql-yoga/urql-exchange
yarn add @graphql-yoga/urql-exchange
pnpm add @graphql-yoga/urql-exchange
bun add @graphql-yoga/urql-exchange
import { createClient, defaultExchanges } from 'urql'import { yogaExchange } from '@graphql-yoga/urql-exchange'const client = createClient({ url: 'http://localhost:4000/graphql', exchanges: [...defaultExchanges, yogaExchange()]})
import { createClient, defaultExchanges, subscriptionExchange } from 'urql'const client = createClient({ url: 'http://localhost:4000/graphql', exchanges: [ ...defaultExchanges, subscriptionExchange({ forwardSubscription(operation) { const url = new URL('http://localhost:4000/graphql') url.searchParams.append('query', operation.query) if (operation.variables) { url.searchParams.append('variables', JSON.stringify(operation.variables)) } return { subscribe(sink) { const eventsource = new EventSource(url.toString(), { withCredentials: true // This is required for cookies }) eventsource.onmessage = event => { const data = JSON.parse(event.data) sink.next(data) if (eventsource.readyState === 2) { sink.complete() } } eventsource.onerror = error => { sink.error(error) } eventsource.addEventListener('complete', () => { eventsource.close() // If operation ends, close the connection and prevent the client from reconnecting }) return { unsubscribe: () => eventsource.close() } } } } }) ]})
Client usage with Relay
Relay Subscription client.
import type { Variables } from 'react-relay'import { Network, Observable, type GraphQLResponse, type RequestParameters } from 'relay-runtime'import { executeQueryOrMutation } from './execute-query-or-mutation.js'const executeSubscription = ( request: RequestParameters, variables: Variables): Observable<GraphQLResponse> => { if (!request.text) throw new Error('Missing document.') const { text: operation, name } = request const url = new URL('http://localhost:4000/graphql') url.searchParams.append('query', operation) if (name) { url.searchParams.append('operationName', name) } if (variables) { url.searchParams.append('variables', JSON.stringify(variables)) } return Observable.create<GraphQLResponse>(sink => { const source = new EventSource(url.toString()) source.onmessage = function (event) { const data = JSON.parse(event.data) sink.next(data) if (source.readyState === 2) { sink.complete() } } source.onerror = function (ev) { console.error(ev) sink.error(new Error('Unexpected error.')) } eventsource.addEventListener('complete', () => { eventsource.close() // If operation ends, close the connection and prevent the client from reconnecting }) return () => source.close() })}const network = Network.create(executeQueryOrMutation, executeSubscription)
GraphQL over Server-Sent Events Protocol (via graphql-sse)
import { createServer } from 'node:http'import { createYoga } from 'graphql-yoga'const yogaApp = createYoga({ // ... legacySse: false})
Single Connection Mode
In case you want the subscriptions to be transported following the
GraphQL over Server-Sent Events Protocol
also in the single connection mode, you simply use the @graphql-yoga/plugin-graphql-sse plugin for
GraphQL Yoga that exposes an additional endpoint (defaulting to /graphql/stream) used for
graphql-sse clients.
The plugin will hijack the request from the onRequest hook and will use all envelop plugins
provided.
Also, consider setting the subscriptionsProtocol in GraphiQL options to GRAPHQL_SSE in order to
have it use graphql-sse.
yoga-with-graphql-sse.ts
import { createServer } from 'node:http'import { createYoga } from 'graphql-yoga'import { useGraphQLSSE } from '@graphql-yoga/plugin-graphql-sse'const yogaApp = createYoga({ graphiql: { // Use graphql-sse in GraphiQL. subscriptionsProtocol: 'GRAPHQL_SSE' }, plugins: [ // Simply install the graphql-sse plugin and you're off! useGraphQLSSE() ]})// Get NodeJS Server from Yogaconst httpServer = createServer(yogaApp)httpServer.listen(4000, () => { console.log('Server is running on port 4000')})
Suppose you want to use the
graphql-transport-ws protocol
with GraphQL Yoga, you can use the graphql-ws library.
To have the same execution pipeline in graphql-ws, we can use the Envelop instance from
GraphQL Yoga like below in Node.JS. Also, you can set subscriptionsProtocol in GraphiQL options to
use WebSockets instead of Server-Sent Events within GraphiQL.
yoga-with-ws.ts
import { createServer } from 'node:http'import { useServer } from 'graphql-ws/lib/use/ws'import { createYoga } from 'graphql-yoga'import { WebSocketServer } from 'ws'const yogaApp = createYoga({ graphiql: { // Use WebSockets in GraphiQL subscriptionsProtocol: 'WS' }})// Get NodeJS Server from Yogaconst httpServer = createServer(yogaApp)// Create WebSocket server instance from our Node serverconst wsServer = new WebSocketServer({ server: httpServer, path: yogaApp.graphqlEndpoint})// Integrate Yoga's Envelop instance and NodeJS server with graphql-wsuseServer( { execute: (args: any) => args.rootValue.execute(args), subscribe: (args: any) => args.rootValue.subscribe(args), onSubscribe: async (ctx, msg) => { const { schema, execute, subscribe, contextFactory, parse, validate } = yogaApp.getEnveloped({ ...ctx, req: ctx.extra.request, socket: ctx.extra.socket, params: msg.payload }) const args = { schema, operationName: msg.payload.operationName, document: parse(msg.payload.query), variableValues: msg.payload.variables, contextValue: await contextFactory(), rootValue: { execute, subscribe } } const errors = validate(args.schema, args.document) if (errors.length) return errors return args } }, wsServer)httpServer.listen(4000, () => { console.log('Server is running on port 4000')})
GraphQL Yoga comes with a built-in PubSub (publish/subscribe) bus. This makes it easy to send new
events to the client from within your mutation resolvers.
server.ts
import { createServer } from 'node:http'import { createPubSub, createSchema, createYoga } from 'graphql-yoga'const pubSub = createPubSub()// Provide your schemaconst yoga = createYoga({ schema: createSchema({ typeDefs: /* GraphQL */ ` type Query { hello: String } type Subscription { randomNumber: Float! } type Mutation { broadcastRandomNumber: Boolean } `, resolvers: { Query: { hello: () => 'world' }, Subscription: { randomNumber: { // subscribe to the randomNumber event subscribe: () => pubSub.subscribe('randomNumber'), resolve: payload => payload } }, Mutation: { broadcastRandomNumber: (_, args) => { // publish a random number pubSub.publish('randomNumber', Math.random()) } } } })})const server = createServer(yoga)server.listen(4000, () => { console.info('Server is running on http://localhost:4000/graphql')})
Topics
When using TypeScript it is possible to make the event emitter type-safe by providing a channel
configuration via a generic.
const pubSub = createPubSub<{ randomNumber: [randomNumber: number]}>()pubsub.subscribe('randomNumber')// This is now type-safe.pubSub.publish('randomNumber', 1)// This causes a TypeScript error.pubSub.publish('randomNumber')// This causes a TypeScript error.pubSub.publish('event does not exist')
You can subscribe to a specific topic using pubSub.subscribe.
const pubSub = createPubSub<{ randomNumber: [randomNumber: number]}>()// Usage outside a GraphQL subscribe functionasync function subscribe() { const eventSource = pubSub.subscribe('randomNumber') for await (const value of eventSource) { console.log(value) // dispose subscription after the first event has been published. eventSource.return() }}subscribe()pubSub.publish('randomNumber', 3)
You can declare events with and without a payload.
const pubSub = createPubSub<{ // event has no payload 'event:without:payload': [] // event has payload of type number 'event:payload:number': [payload: number] // event has payload of type { foo: number } 'event:payload:obj': [payload: { foo: number }]}>()pubSub.publish('event:without:payload')pubSub.publish('event:payload:number', 12)pubSub.publish('event:payload:obj', { foo: 1 })
Topic with Dynamic ID
Sometimes you only want to emit and listen for events for a specific entity (e.g. user or product).
You can declare topics scoped to a special identifier.
const pubSub = createPubSub<{ 'user:followerCount': [userId: string, payload: { followerCount: number }]}>()const userId1 = '420'const userId2 = '69'// the userId argument is enforced by the TypeScript compiler.pubSub.subscribe('user:followerCount', userId1)pubSub.subscribe('user:followerCount', userId2)pubSub.publish('user:followerCount', userId1, { followerCount: 30 })pubSub.publish('user:followerCount', userId2, { followerCount: 12 })
Distributed Pub/Sub for Production
If you spin up multiple instances of your GraphQL server each server instance will have their own
in-memory pub/sub instance. An event triggered on the one server instance will not be distributed to
the other server instances, resulting in subscribers on the other server not receiving any updates.
The createPubSub function allows you to specify a custom
EventTarget implementation, which
can use an external datastore for distributing the events across all server replicas such as
Redis Pub/Sub or Kafka.
Yoga comes with an EventTarget implementation for
Redis Pub/Sub.
npm i @graphql-yoga/redis-event-target ioredis
yarn add @graphql-yoga/redis-event-target ioredis
pnpm add @graphql-yoga/redis-event-target ioredis
bun add @graphql-yoga/redis-event-target ioredis
import { createPubSub } from 'graphql-yoga'import { Redis } from 'ioredis'import { createRedisEventTarget } from '@graphql-yoga/redis-event-target'const publishClient = new Redis()const subscribeClient = new Redis()const eventTarget = createRedisEventTarget({ publishClient, subscribeClient})const pubSub = createPubSub({ eventTarget })
Please note that Redis Pub/Sub requires a stable long-running connection and thus is not a
suitable solution for serverless or edge function environments.
Please also note that the event payloads must be JSON serializable. If you want to send complex
data structures over the wire you can use tools such as
superjson.
Advanced
Filter and Map Values
Sometimes it is useful to filter or map events for an individual subscription set up by a client
based on subscription field arguments. Yoga has a few utility functions that make this as simple as
possible.
import { createServer } from 'node:http'import { createPubSub, createSchema, createYoga } from 'graphql-yoga'const pubSub = createPubSub()const yoga = createYoga({ schema: createSchema({ typeDefs: /* GraphQL */ ` type Query { hello: String } type Subscription { randomNumber(multiplyBy: Int!, lessThan: Float!): Float! } type Mutation { broadcastRandomNumber: Boolean } `, resolvers: { Query: { hello: () => 'world' }, Subscription: { randomNumber: { // subscribe to the randomNumber event subscribe: (_, args) => pipe( pubSub.subscribe('randomNumber'), map(publishedNumber => publishedNumber * args.multiplyBy), filter(multipliedNumber => multipliedNumber < args.lessThan) ), resolve: payload => payload } }, Mutation: { broadcastRandomNumber: (_, args) => { // publish a random number pubSub.publish('randomNumber', Math.random()) } } } })})const server = createServer(yoga)server.listen(4000, () => { console.info('Server is running on http://localhost:4000/graphql')})
Subscriptions with Initial Value
GraphQL subscriptions are primarily designed to send a stream of events to the client. Sometimes it
is useful to send an initial value to a client as soon as the GraphQL subscription is set up.
An example for this would be a Subscription.globalCounter field that syncs a counter with all
clients by streaming the initial counter value to a client that sets up the subscription and then,
furthermore, streams the updated counter value to the clients every time it changes.
GraphQL subscriptions are implemented using
Async Iteration,
which in itself is a very complex topic with a lot of pitfalls that can cause memory leaks if not
treated with caution.
Yoga uses and re-exports Repeater.js (“The missing constructor for
creating safe async iterators”) for providing a friendly developer experience.
import { createServer } from 'node:http'import { createPubSub, createSchema, createYoga, map, pipe, Repeater } from 'graphql-yoga'let globalCounter = 0const pubSub = createPubSub()const yoga = createYoga({ schema: createSchema({ typeDefs: /* GraphQL */ ` type Query { hello: String } type Subscription { globalCounter: Int! } type Mutation { incrementGlobalCounter: Int! } `, resolvers: { Query: { hello: () => 'world' }, Subscription: { globalCounter: { // Merge initial value with source stream of new values subscribe: () => pipe( Repeater.merge([ // cause an initial event so the // globalCounter is streamed to the client // upon initiating the subscription undefined, // event stream for future updates pubSub.subscribe('globalCounter:change') ]), // map all stream values to the latest globalCounter map(() => globalCounter) ), resolve: payload => payload } }, Mutation: { incrementGlobalCounter: () => { globalCounter = globalCounter + 1 // publish a global counter increment event pubSub.publish('globalCounter:change') return globalCounter } } } })})const server = createServer(yoga)server.listen(4000, () => { console.info('Server is running on http://localhost:4000/graphql')})
Listen to Multiple Pub/Sub Topics
Sometimes it is handy to subscribe to multiple PubSub topics instead of a single one.
import { createServer } from 'node:http'import { createPubSub, createSchema, createYoga, map, pipe, Repeater } from 'graphql-yoga'type User = { id: string login: string}let user: User | null = { id: '1', login: 'Laurin'}const pubSub = createPubSub<{ userLoginChanged: [] userDeleted: []}>()const yoga = createYoga({ schema: createSchema({ typeDefs: /* GraphQL */ ` type Query { hello: String } type User { id: ID! login: String } type Subscription { user: User } type Mutation { deleteUser: Boolean updateUserLogin(newLogin: String!): Boolean } `, resolvers: { Query: { hello: () => 'world' }, Subscription: { user: { // Merge initial value with source streams of new values subscribe: () => pipe( Repeater.merge([ undefined, pubSub.subscribe('userLoginChanged'), pubSub.subscribe('userDeleted') ]), // map all stream values to the latest user map(() => user) ), resolve: payload => payload } }, Mutation: { deleteUser() { user = null pubSub.publish('userDeleted') return true }, updateUserLogin(_, args) { if (!user) { return false } user.login = args.newLogin pubSub.publish('userLoginChanged') return true } } } })})const server = createServer(yoga)server.listen(4000, () => { console.info('Server is running on http://localhost:4000/graphql')})
This site uses cookies for analytics and improving your experience.