Getting started
Install
$ yarn add graphql-ws
Create a GraphQL schema
import { buildSchema } from 'graphql';
const schema = buildSchema(`
type Query {
hello: String
}
type Subscription {
greetings: String
}
`);
const roots = {
query: {
hello: () => 'Hello World!',
},
subscription: {
greetings: async function* sayHiIn5Languages() {
for (const hi of ['Hi', 'Bonjour', 'Hola', 'Ciao', 'Zdravo']) {
yield { greetings: hi };
}
},
},
};
Start the server
import https from 'https';
import { execute, subscribe } from 'graphql';
import { createServer } from 'graphql-ws';
const server = https.createServer(function weServeSocketsOnly(_, res) {
res.writeHead(404);
res.end();
});
createServer(
{
schema,
roots,
execute,
subscribe,
},
{
server,
path: '/graphql',
},
);
server.listen(443);
Use the client
import { createClient } from 'graphql-ws';
const client = createClient({
url: 'wss://welcomer.com/graphql',
});
(async () => {
const result = await new Promise((resolve, reject) => {
let result;
client.subscribe(
{
query: '{ hello }',
},
{
next: (data) => (result = data),
error: reject,
complete: () => resolve(result),
},
);
});
expect(result).toEqual({ hello: 'Hello World!' });
})();
(async () => {
const onNext = () => {
};
await new Promise((resolve, reject) => {
client.subscribe(
{
query: 'subscription { greetings }',
},
{
next: onNext,
error: reject,
complete: resolve,
},
);
});
expect(onNext).toBeCalledTimes(5);
})();
Recipes
Client usage with Promise
import { createClient, SubscribePayload } from 'graphql-ws';
const client = createClient({
url: 'wss://hey.there/graphql',
});
async function execute<T>(payload: SubscribePayload) {
return new Promise<T>((resolve, reject) => {
let result: T;
client.subscribe<T>(payload, {
next: (data) => (result = data),
error: reject,
complete: () => resolve(result),
});
});
}
(async () => {
try {
const result = await execute({
query: '{ hello }',
});
} catch (err) {
}
})();
Client usage with AsyncIterator
import { createClient, SubscribePayload } from 'graphql-ws';
const client = createClient({
url: 'wss://iterators.ftw/graphql',
});
function subscribe<T>(payload: SubscribePayload): AsyncIterableIterator<T> {
let deferred: {
resolve: (done: boolean) => void;
reject: (err: unknown) => void;
} | null = null;
const pending: T[] = [];
let throwMe: unknown = null,
done = false;
const dispose = client.subscribe<T>(payload, {
next: (data) => {
pending.push(data);
deferred?.resolve(false);
},
error: (err) => {
throwMe = err;
deferred?.reject(throwMe);
},
complete: () => {
done = true;
deferred?.resolve(true);
},
});
return {
[Symbol.asyncIterator]() {
return this;
},
async next() {
if (done) return { done: true, value: undefined };
if (throwMe) throw throwMe;
if (pending.length) return { value: pending.shift()! };
return (await new Promise<boolean>(
(resolve, reject) => (deferred = { resolve, reject }),
))
? { done: true, value: undefined }
: { value: pending.shift()! };
},
async return() {
dispose();
return { done: true, value: undefined };
},
};
}
(async () => {
const subscription = subscribe({
query: 'subscription { greetings }',
});
for await (const result of subscription) {
}
})();
Client usage with Observable
import { Observable } from 'relay-runtime';
import { Observable } from '@apollo/client';
import { Observable } from 'rxjs';
import Observable from 'zen-observable';
const client = createClient({
url: 'wss://graphql.loves/observables',
});
function toObservable(operation) {
return new Observable((observer) => client.subscribe(operation, observer));
}
const observable = toObservable({ query: `subscription { ping }` });
const subscription = observable.subscribe({
next: (data) => {
expect(data).toBe({ data: { ping: 'pong' } });
},
});
subscription.unsubscribe();
Client usage with Relay
import { GraphQLError } from 'graphql';
import {
Network,
Observable,
RequestParameters,
Variables,
} from 'relay-runtime';
import { createClient } from 'graphql-ws';
const subscriptionsClient = createClient({
url: 'wss://i.love/graphql',
connectionParams: () => {
const session = getSession();
if (!session) {
return {};
}
return {
Authorization: `Bearer ${session.token}`,
};
},
});
function fetchOrSubscribe(operation: RequestParameters, variables: Variables) {
return Observable.create((sink) => {
if (!operation.text) {
return sink.error(new Error('Operation text cannot be empty'));
}
return subscriptionsClient.subscribe(
{
operationName: operation.name,
query: operation.text,
variables,
},
{
...sink,
error: (err) => {
if (err instanceof Error) {
sink.error(err);
} else if (err instanceof CloseEvent) {
sink.error(
new Error(
`Socket closed with event ${err.code}` + err.reason
? `: ${err.reason}`
: '',
),
);
} else {
sink.error(
new Error(
(err as GraphQLError[])
.map(({ message }) => message)
.join(', '),
),
);
}
},
},
);
});
}
export const network = Network.create(fetchOrSubscribe, fetchOrSubscribe);
Client usage with urql
import { createClient, defaultExchanges, subscriptionExchange } from 'urql';
import { createClient as createWSClient } from 'graphql-ws';
const wsClient = createWSClient({
url: 'wss://its.urql/graphql',
});
const client = createClient({
url: '/graphql',
exchanges: [
...defaultExchanges,
subscriptionExchange({
forwardSubscription(operation) {
return {
subscribe: (sink) => {
const dispose = wsClient.subscribe(operation, sink);
return {
unsubscribe: dispose,
};
},
};
},
}),
],
});
Client usage with Apollo
import { ApolloLink, Operation, FetchResult, Observable } from '@apollo/client';
import { print, GraphQLError } from 'graphql';
import { createClient, ClientOptions, Client } from 'graphql-ws';
class WebSocketLink extends ApolloLink {
private client: Client;
constructor(options: ClientOptions) {
super();
this.client = createClient(options);
}
public request(operation: Operation): Observable<FetchResult> {
return new Observable((sink) => {
return this.client.subscribe<FetchResult>(
{ ...operation, query: print(operation.query) },
{
next: sink.next,
complete: sink.complete,
error: (err) => {
if (err instanceof Error) {
sink.error(err);
} else if (err instanceof CloseEvent) {
sink.error(
new Error(
`Socket closed with event ${err.code}` + err.reason
? `: ${err.reason}`
: '',
),
);
} else {
sink.error(
new Error(
(err as GraphQLError[])
.map(({ message }) => message)
.join(', '),
),
);
}
},
},
);
});
}
}
const link = new WebSocketLink({
url: 'wss://where.is/graphql',
connectionParams: () => {
const session = getSession();
if (!session) {
return {};
}
return {
Authorization: `Bearer ${session.token}`,
};
},
});
Client usage in browser
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>GraphQL over WebSocket</title>
<script
type="text/javascript"
src="https://unpkg.com/graphql-ws/umd/graphql-ws.min.js"
></script>
</head>
<body>
<script type="text/javascript">
const client = graphqlWs.createClient({
url: 'wss://umdfor.the/win/graphql',
});
</script>
</body>
</html>
Client usage in Node
const WebSocket = require('ws');
const Crypto = require('crypto');
const { createClient } = require('graphql-ws');
const client = createClient({
url: 'wss://no.browser/graphql',
webSocketImpl: WebSocket,
generateID: () =>
([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, (c) =>
(c ^ (Crypto.randomBytes(1)[0] & (15 >> (c / 4)))).toString(16),
),
});
Server usage with Express GraphQL
import https from 'https';
import express from 'express';
import { graphqlHTTP } from 'express-graphql';
import { createServer } from 'graphql-ws';
import { execute, subscribe } from 'graphql';
import { schema } from 'my-graphql-schema';
const app = express();
app.use('/graphql', graphqlHTTP({ schema }));
const server = https.createServer(app);
server.listen(443, () => {
createServer(
{
schema,
execute,
subscribe,
},
{
server,
path: '/graphql',
},
);
});
Server usage with console logging
import https from 'https';
import { execute, subscribe } from 'graphql';
import { createServer } from 'graphql-ws';
import { schema } from 'my-graphql-schema';
const server = https.createServer(function weServeSocketsOnly(_, res) {
res.writeHead(404);
res.end();
});
createServer(
{
schema,
onConnect: (ctx) => {
console.log('Connect', ctx);
},
onSubscribe: (ctx, msg) => {
console.log('Subscribe', { ctx, msg });
},
onNext: (ctx, msg, args, result) => {
console.debug('Next', { ctx, msg, args, result });
},
onError: (ctx, msg, errors) => {
console.error('Error', { ctx, msg, errors });
},
onComplete: (ctx, msg) => {
console.log('Complete', { ctx, msg });
},
},
{
server,
path: '/graphql',
},
);
server.listen(443);
Server usage on a multi WebSocket server
import https from 'https';
import WebSocket from 'ws';
import url from 'url';
import { execute, subscribe } from 'graphql';
import { createServer, createClient } from 'graphql-ws';
import { schema } from 'my-graphql-schema';
const server = https.createServer(function weServeSocketsOnly(_, res) {
res.writeHead(404);
res.end();
});
const waveWS = new WebSocket.Server({ noServer: true });
const graphqlWS = new WebSocket.Server({ noServer: true });
server.on('upgrade', (request, socket, head) => {
const pathname = url.parse(request.url).pathname;
if (pathname === '/wave') {
waveWS.handleUpgrade(request, socket, head, (client) => {
waveWS.emit('connection', client, request);
});
} else if (pathname === '/graphql') {
graphqlWS.handleUpgrade(request, socket, head, (client) => {
graphqlWS.emit('connection', client, request);
});
} else {
socket.destroy();
}
});
waveWS.on('connection', (socket) => {
socket.send('🌊');
});
createServer(
{
schema,
execute,
subscribe,
},
graphqlWS,
);
server.listen(443);
Server usage with custom context value
import { validate, execute, subscribe } from 'graphql';
import { createServer } from 'graphql-ws';
import { schema, roots, getDynamicContext } from 'my-graphql';
createServer(
{
context: (ctx, msg, args) => {
return getDynamicContext(ctx, msg, args);
},
schema,
roots,
execute,
subscribe,
},
{
server,
path: '/graphql',
},
);
Server usage with custom execution arguments and validation
import { parse, validate, execute, subscribe } from 'graphql';
import { createServer } from 'graphql-ws';
import { schema, myValidationRules } from 'my-graphql';
createServer(
{
execute,
subscribe,
onSubscribe: (ctx, msg) => {
const args = {
schema,
operationName: msg.payload.operationName,
document: parse(msg.payload.query),
variableValues: msg.payload.variables,
};
const errors = validate(args.schema, args.document, myValidationRules);
if (errors.length > 0) {
return errors;
}
return args;
},
},
{
server,
path: '/graphql',
},
);
Server and client usage with persisted queries
import { parse, execute, subscribe } from 'graphql';
import { createServer } from 'graphql-ws';
import { schema } from 'my-graphql-schema';
type QueryID = string;
const queriesStore: Record<QueryID, ExecutionArgs> = {
iWantTheGreetings: {
schema,
document: parse('subscription Greetings { greetings }'),
},
};
createServer(
{
execute,
subscribe,
onSubscribe: (_ctx, msg) => {
const query = queriesStore[msg.payload.query];
if (!query) {
throw new Error('404: Query Not Found');
}
return {
...query,
variableValues: msg.payload.variables,
};
},
},
{
server,
path: '/graphql',
},
);
import { createClient } from 'graphql-ws';
const client = createClient({
url: 'wss://persisted.graphql/queries',
});
(async () => {
const onNext = () => {
};
await new Promise((resolve, reject) => {
client.subscribe(
{
query: 'iWantTheGreetings',
},
{
next: onNext,
error: reject,
complete: resolve,
},
);
});
expect(onNext).toBeCalledTimes(5);
})();
Check the docs folder out for TypeDoc generated documentation.
Read about the exact transport intricacies used by the library in the GraphQL over WebSocket Protocol document.
File a bug, contribute with code, or improve documentation? Read up on our guidelines for contributing and drive development with yarn test --watch
away!