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
With ws
import ws from 'ws';
import { useServer } from 'graphql-ws/lib/use/ws';
const server = new ws.Server({
port: 80,
path: '/graphql',
});
useServer(
{ schema, roots },
server,
);
console.log('Listening to port 80');
import uWS from 'uWebSockets.js';
import { makeBehavior } from 'graphql-ws/lib/use/uWebSockets';
uWS
.App()
.ws(
'/graphql',
makeBehavior(
{ schema, roots },
),
)
.listen(80, (listenSocket) => {
if (listenSocket) {
console.log('Listening to port 80');
}
});
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 = () => {
};
let unsubscribe = () => {
};
await new Promise((resolve, reject) => {
unsubscribe = 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/core';
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, {
next: (data) => observer.next(data),
error: (err) => observer.error(err),
complete: () => observer.complete(),
}),
);
}
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) {
return sink.error(err);
}
if (err instanceof CloseEvent) {
return sink.error(
new Error(
`Socket closed with event ${err.code} ${err.reason || ''}`,
),
);
}
return 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/core';
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.bind(sink),
complete: sink.complete.bind(sink),
error: (err) => {
if (err instanceof Error) {
return sink.error(err);
}
if (err instanceof CloseEvent) {
return sink.error(
new Error(
`Socket closed with event ${err.code} ${err.reason || ''}`,
),
);
}
return 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 with custom retry timeout strategy
import { createClient } from 'graphql-ws';
import { waitForHealthy } from './my-servers';
const url = 'wss://i.want.retry/control/graphql';
const client = createClient({
url,
retryWait: async function waitForServerHealthyBeforeRetry() {
await waitForHealthy(url);
await new Promise((resolve) =>
setTimeout(resolve, 1000 + Math.random() * 3000),
);
},
});
🔗 Client usage with graceful restart
import { createClient, Client, ClientOptions } from 'graphql-ws';
import { giveMeAFreshToken } from './token-giver';
interface RestartableClient extends Client {
restart(): void;
}
function createRestartableClient(options: ClientOptions): RestartableClient {
let restartRequested = false;
let restart = () => {
restartRequested = true;
};
const client = createClient({
...options,
on: {
...options.on,
connected: (socket) => {
options.on?.connected?.(socket);
restart = () => {
if (socket.readyState === WebSocket.OPEN) {
socket.close(4205, 'Client Restart');
} else {
restartRequested = true;
}
};
if (restartRequested) {
restartRequested = false;
restart();
}
},
},
});
return {
...client,
restart: () => restart(),
};
}
const client = createRestartableClient({
url: 'wss://graceful.restart/is/a/non-fatal/close-code',
connectionParams: async () => {
const token = await giveMeAFreshToken();
return { 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 ws = require('ws');
const Crypto = require('crypto');
const { createClient } = require('graphql-ws');
const client = createClient({
url: 'wss://no.browser/graphql',
webSocketImpl: ws,
generateID: () =>
([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, (c) =>
(c ^ (Crypto.randomBytes(1)[0] & (15 >> (c / 4)))).toString(16),
),
});
🔗 Server usage with ws
import ws from 'ws';
import { makeServer } from 'graphql-ws';
import { schema } from './my-graphql-schema';
const server = makeServer({ schema });
const wsServer = new ws.Server({
port: 443,
path: '/graphql',
});
wsServer.on('connection', (socket, request) => {
const closed = server.opened(
{
protocol: socket.protocol,
send: (data) =>
new Promise((resolve, reject) => {
socket.send(data, (err) => (err ? reject(err) : resolve()));
}),
close: (code, reason) => socket.close(code, reason),
onMessage: (cb) =>
socket.on('message', async (event) => {
try {
await cb(event.toString());
} catch (err) {
socket.close(1011, err.message);
}
}),
},
{ socket, request },
);
socket.once('close', (code, reason) => closed(code, reason));
});
🔗 Server usage with ws and custom auth handling
import http from 'http';
import ws from 'ws';
import { makeServer } from 'graphql-ws';
import { schema } from './my-graphql-schema';
import { validate } from './my-auth';
interface Extra {
readonly request: http.IncomingMessage;
}
class Forbidden extends Error {}
function handleAuth(request: http.IncomingMessage) {
const good = validate(request.headers['authorization']);
if (!good) {
throw new Forbidden(':(');
}
}
const gqlServer = makeServer<Extra>({
schema,
onConnect: async (ctx) => {
await handleAuth(ctx.extra.request);
},
onSubscribe: async (ctx) => {
await handleAuth(ctx.extra.request);
},
onNext: async (ctx) => {
await handleAuth(ctx.extra.request);
},
});
const wsServer = new ws.Server({
port: 443,
path: '/graphql',
});
wsServer.on('connection', (socket, request) => {
const closed = gqlServer.opened(
{
protocol: socket.protocol,
send: (data) =>
new Promise((resolve, reject) => {
socket.send(data, (err) => (err ? reject(err) : resolve()));
}),
close: (code, reason) => socket.close(code, reason),
onMessage: (cb) => {
socket.on('message', async (event) => {
try {
await cb(event.toString());
} catch (err) {
if (err instanceof Forbidden) {
} else {
socket.close(1011, err.message);
}
}
});
},
},
{ request },
);
socket.once('close', (code, reason) => closed(code, reason));
});
🔗 ws server usage with Express GraphQL
import ws from 'ws';
import express from 'express';
import { graphqlHTTP } from 'express-graphql';
import { useServer } from 'graphql-ws/lib/use/ws';
import { schema } from './my-graphql-schema';
const app = express();
app.use('/graphql', graphqlHTTP({ schema }));
const server = app.listen(443, () => {
const wsServer = new ws.Server({
server,
path: '/graphql',
});
useServer({ schema }, wsServer);
});
🔗 ws server usage with Apollo Server Express
import express from 'express';
import { ApolloServer } from 'apollo-server-express';
import ws from 'ws';
import { useServer } from 'graphql-ws/lib/use/ws';
import { schema } from './my-graphql-schema';
const app = express();
const apolloServer = new ApolloServer({ schema });
apolloServer.applyMiddleware({ app });
const server = app.listen(443, () => {
const wsServer = new ws.Server({
server,
path: '/graphql',
});
useServer({ schema }, wsServer);
});
🔗 ws server usage with subscriptions-transport-ws backwards compatibility
import https from 'https';
import ws from 'ws';
import { execute, subscribe } from 'graphql';
import { GRAPHQL_TRANSPORT_WS_PROTOCOL } from 'graphql-ws';
import { useServer } from 'graphql-ws/lib/use/ws';
import { SubscriptionServer, GRAPHQL_WS } from 'subscriptions-transport-ws';
import { schema } from './my-graphql-schema';
const graphqlWs = new ws.Server({ noServer: true });
useServer({ schema }, graphqlWs);
const subTransWs = new ws.Server({ noServer: true });
SubscriptionServer.create(
{
schema,
execute,
subscribe,
},
subTransWs,
);
const server = https.createServer(function weServeSocketsOnly(_, res) {
res.writeHead(404);
res.end();
});
server.on('upgrade', (req, socket, head) => {
const protocol = req.headers['sec-websocket-protocol'];
const protocols = Array.isArray(protocol)
? protocol
: protocol?.split(',').map((p) => p.trim());
const wss =
protocols?.includes(GRAPHQL_WS) &&
!protocols.includes(GRAPHQL_TRANSPORT_WS_PROTOCOL)
? subTransWs
:
graphqlWs;
wss.handleUpgrade(req, socket, head, (ws) => {
wss.emit('connection', ws, req);
});
});
🔗 ws server usage with console logging
import ws from 'ws';
import { useServer } from 'graphql-ws/lib/use/ws';
import { schema } from './my-graphql-schema';
const wsServer = new ws.Server({
port: 443,
path: '/graphql',
});
useServer(
{
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 });
},
},
wsServer,
);
🔗 ws server usage on a multi WebSocket server
import https from 'https';
import ws from 'ws';
import url from 'url';
import { createClient } from 'graphql-ws';
import { useServer } from 'graphql-ws/lib/use/ws';
import { schema } from './my-graphql-schema';
const server = https.createServer(function weServeSocketsOnly(_, res) {
res.writeHead(404);
res.end();
});
const waveWS = new ws.Server({ noServer: true });
const graphqlWS = new ws.Server({ noServer: true });
server.on('upgrade', (request, socket, head) => {
const pathname = url.parse(request.url).pathname;
if (pathname === '/wave') {
return waveWS.handleUpgrade(request, socket, head, (client) => {
waveWS.emit('connection', client, request);
});
}
if (pathname === '/graphql') {
return graphqlWS.handleUpgrade(request, socket, head, (client) => {
graphqlWS.emit('connection', client, request);
});
}
return socket.destroy();
});
waveWS.on('connection', (socket) => {
socket.send('🌊');
});
useServer({ schema }, graphqlWS);
server.listen(443);
🔗 ws server usage with custom context value
import ws from 'ws';
import { useServer } from 'graphql-ws/lib/use/ws';
import { schema, roots, getDynamicContext } from './my-graphql';
const wsServer = new ws.Server({
port: 443,
path: '/graphql',
});
useServer(
{
context: (ctx, msg, args) => {
return getDynamicContext(ctx, msg, args);
},
schema,
roots,
},
wsServer,
);
🔗 ws server usage with dynamic schema
import ws from 'ws';
import { useServer } from 'graphql-ws/lib/use/ws';
import { schema, checkIsAdmin, getDebugSchema } from './my-graphql';
const wsServer = new ws.Server({
port: 443,
path: '/graphql',
});
useServer(
{
schema: async (ctx, msg, executionArgsWithoutSchema) => {
const isAdmin = await checkIsAdmin(ctx.request);
if (isAdmin) return getDebugSchema(ctx, msg, executionArgsWithoutSchema);
return schema;
},
},
wsServer,
);
🔗 ws server usage with custom validation
import ws from 'ws';
import { useServer } from 'graphql-ws/lib/use/ws';
import { validate } from 'graphql';
import { schema, myValidationRules } from './my-graphql';
const wsServer = new ws.Server({
port: 443,
path: '/graphql',
});
useServer(
{
validate: (schema, document) =>
validate(schema, document, myValidationRules),
},
wsServer,
);
🔗 ws server usage with custom execution arguments
import { parse, validate } from 'graphql';
import ws from 'ws';
import { useServer } from 'graphql-ws/lib/use/ws';
import { schema, myValidationRules } from './my-graphql';
const wsServer = new ws.Server({
port: 443,
path: '/graphql',
});
useServer(
{
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;
},
},
wsServer,
);
🔗 ws server usage accepting only subscription operations
import { parse, validate, getOperationAST, GraphQLError } from 'graphql';
import ws from 'ws';
import { useServer } from 'graphql-ws/lib/use/ws';
import { schema } from './my-graphql';
const wsServer = new ws.Server({
port: 443,
path: '/graphql',
});
useServer(
{
onSubscribe: (_ctx, msg) => {
const args = {
schema,
operationName: msg.payload.operationName,
document: parse(msg.payload.query),
variableValues: msg.payload.variables,
};
const operationAST = getOperationAST(args.document, args.operationName);
if (!operationAST) {
return [new GraphQLError('Unable to identify operation')];
}
if (operationAST.operation !== 'subscription') {
return [new GraphQLError('Only subscription operations are supported')];
throw new Error('Only subscription operations are supported');
}
const errors = validate(args.schema, args.document);
if (errors.length > 0) {
return errors;
}
return args;
},
},
wsServer,
);
🔗 ws server and client usage with persisted queries
import { parse } from 'graphql';
import ws from 'ws';
import { useServer } from 'graphql-ws/lib/use/ws';
import { schema } from './my-graphql-schema';
type QueryID = string;
const queriesStore: Record<QueryID, ExecutionArgs> = {
iWantTheGreetings: {
schema,
document: parse('subscription Greetings { greetings }'),
},
};
const wsServer = new ws.Server({
port: 443,
path: '/graphql',
});
useServer(
{
onSubscribe: (_ctx, msg) => {
const query = queriesStore[msg.payload.query];
if (!query) {
throw new Error('404: Query Not Found');
}
return {
...query,
variableValues: msg.payload.variables,
};
},
},
wsServer,
);
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);
})();
🔗 ws server and client with client to server pings and latency
import {
GraphQLSchema,
GraphQLObjectType,
GraphQLNonNull,
GraphQLString,
} from 'graphql';
import ws from 'ws';
import { useServer } from 'graphql-ws/lib/use/ws';
import { schema } from './my-graphql-schema';
const pinger = new GraphQLSchema({
query: new GraphQLObjectType({
name: 'Query',
fields: {
ping: {
type: new GraphQLNonNull(GraphQLString),
resolve: () => 'pong',
},
},
}),
});
const wsServer = new WebSocket.Server({
port: 443,
path: '/graphql',
});
useServer(
{
schema: (_ctx, msg) => {
if (msg.payload.query === '{ ping }') return pinger;
return schema;
},
},
wsServer,
);
import { createClient } from 'graphql-ws';
let connection: WebSocket | undefined;
const client = createClient({
url: 'wss://client.can/send-pings/too',
on: {
connected: (socket) => (connection = socket as WebSocket),
closed: () => (connection = undefined),
},
});
async function ping() {
const pinged = Date.now();
const pongTimeout = setTimeout(
() => connection?.close(4408, 'Pong Timeout'),
2000,
);
await new Promise<void>((resolve, reject) => {
client.subscribe<{ data: { ping: string } }>(
{ query: '{ ping }' },
{
next: () => {
},
error: reject,
complete: resolve,
},
);
}).finally(() => clearTimeout(pongTimeout));
const ponged = Date.now();
return ponged - pinged;
}
(async () => {
for (;;) {
const latency = await ping();
console.info('GraphQL WebSocket connection latency', latency);
await new Promise((resolve) => setTimeout(resolve, 3000));
}
})();
🔗 ws server and client auth usage with token expiration, validation and refresh
import ws from 'ws';
import { useServer } from 'graphql-ws/lib/use/ws';
import { schema } from './my-graphql-schema';
import { isTokenValid } from './my-auth';
const wsServer = new WebSocket.Server({
port: 443,
path: '/graphql',
});
useServer(
{
schema,
onConnect: async (ctx) => {
if (!(await isTokenValid(ctx.connectionParams?.token)))
return false;
},
onSubscribe: async (ctx) => {
if (!(await isTokenValid(ctx.connectionParams?.token)))
return ctx.extra.socket.close(4403, 'Forbidden');
},
onNext: async (ctx) => {
if (!(await isTokenValid(ctx.connectionParams?.token)))
return ctx.extra.socket.close(4403, 'Forbidden');
},
},
wsServer,
);
import { createClient } from 'graphql-ws';
import {
getCurrentToken,
getCurrentTokenExpiresIn,
refreshCurrentToken,
} from './my-auth';
let shouldRefreshToken = false,
tokenExpiryTimeout = null;
const client = createClient({
url: 'wss://server-validates.auth/graphql',
connectionParams: async () => {
if (shouldRefreshToken) {
await refreshCurrentToken();
shouldRefreshToken = false;
}
return { token: getCurrentToken() };
},
on: {
connected: (socket) => {
clearTimeout(tokenExpiryTimeout);
tokenExpiryTimeout = setTimeout(() => {
if (socket.readyState === WebSocket.OPEN)
socket.close(4403, 'Unauthorized');
}, getCurrentTokenExpiresIn());
},
closed: (event) => {
if (event.code === 4403) shouldRefreshToken = true;
},
},
});
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!
Disclaimer
This library and the GraphQL over WebSocket Protocol are not cross-compatible with the legacy subscriptions-transport-ws
and its accompanying Protocol.
You must use graphql-ws
coherently and implement the GraphQL over WebSocket Protocol on both sides, server and the client.