Getting started
Install
yarn add graphql-ws
Create a GraphQL schema
import { GraphQLSchema, GraphQLObjectType, GraphQLString } from 'graphql';
export const schema = new GraphQLSchema({
query: new GraphQLObjectType({
name: 'Query',
fields: {
hello: {
type: GraphQLString,
resolve: () => 'world',
},
},
}),
subscription: new GraphQLObjectType({
name: 'Subscription',
fields: {
greetings: {
type: GraphQLString,
subscribe: async function* () {
for (const hi of ['Hi', 'Bonjour', 'Hola', 'Ciao', 'Zdravo']) {
yield { greetings: hi };
}
},
},
},
}),
});
Start the server
With ws
import { WebSocketServer } from 'ws';
import { useServer } from 'graphql-ws/lib/use/ws';
import { schema } from './previous-step';
const server = new WebSocketServer({
port: 4000,
path: '/graphql',
});
useServer({ schema }, server);
console.log('Listening to port 4000');
import uWS from 'uWebSockets.js';
import { makeBehavior } from 'graphql-ws/lib/use/uWebSockets';
import { schema } from './previous-step';
uWS
.App()
.ws('/graphql', makeBehavior({ schema }))
.listen(4000, (listenSocket) => {
if (listenSocket) {
console.log('Listening to port 4000');
}
});
import Fastify from 'fastify';
import fastifyWebsocket from '@fastify/websocket';
import { makeHandler } from 'graphql-ws/lib/use/@fastify/websocket';
import { schema } from './previous-step';
const fastify = Fastify();
fastify.register(fastifyWebsocket);
fastify.register(async (fastify) => {
fastify.get('/graphql', { websocket: true }, makeHandler({ schema }));
});
fastify.listen(4000, (err) => {
if (err) {
fastify.log.error(err);
return process.exit(1);
}
console.log('Listening to port 4000');
});
Use the client
import { createClient } from 'graphql-ws';
const client = createClient({
url: 'ws://localhost:4000/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: 'ws://hey.there:4000/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: 'ws://iterators.ftw:4000/graphql',
});
function subscribe<T>(payload: SubscribePayload): AsyncGenerator<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 throw(err) {
throw err;
},
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: 'ws://graphql.loves:4000/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 {
Network,
Observable,
RequestParameters,
Variables,
} from 'relay-runtime';
import { createClient } from 'graphql-ws';
const subscriptionsClient = createClient({
url: 'ws://i.love:4000/graphql',
connectionParams: () => {
const session = getSession();
if (!session) {
return {};
}
return {
Authorization: `Bearer ${session.token}`,
};
},
});
function fetchOrSubscribe(
operation: RequestParameters,
variables: Variables,
): Observable<any> {
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,
);
});
}
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: 'ws://its.urql:4000/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 Client Web
import { createClient } from 'graphql-ws';
import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
const link = new GraphQLWsLink(
createClient({
url: 'ws://where.is:4000/graphql',
connectionParams: () => {
const session = getSession();
if (!session) {
return {};
}
return {
Authorization: `Bearer ${session.token}`,
};
},
}),
);
import {
ApolloLink,
Operation,
FetchResult,
Observable,
} from '@apollo/client/core';
import { print } from 'graphql';
import { createClient, Client } from 'graphql-ws';
class GraphQLWsLink extends ApolloLink {
constructor(private client: Client) {
super();
}
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: sink.error.bind(sink),
},
);
});
}
}
🔗 Client usage with Apollo Kotlin
Connect to graphql-transport-ws
compatible server in Kotlin using Apollo Kotlin
val apolloClient = ApolloClient.Builder()
.networkTransport(
WebSocketNetworkTransport.Builder()
.serverUrl(
serverUrl = "http://localhost:9090/graphql",
).protocol(
protocolFactory = GraphQLWsProtocol.Factory()
).build()
)
.build()
🔗 Client usage with Apollo iOS
Connect to graphql-transport-ws
compatible server in Swift using Apollo iOS
import Foundation
import Apollo
import ApolloWebSocket
let store = ApolloStore()
let normalTransport = RequestChainNetworkTransport(
interceptorProvider: DefaultInterceptorProvider(store: store),
endpointURL: URL(string: "http://localhost:8080/graphql")!
)
let webSocketClient = WebSocket(
request: URLRequest(url: URL(string: "ws://localhost:8080/websocket")!),
protocol: .graphql_transport_ws
)
let webSocketTransport = WebSocketTransport(
websocket: webSocketClient,
store: store
)
let splitTransport = SplitNetworkTransport(
uploadingNetworkTransport: normalTransport,
webSocketNetworkTransport: webSocketTransport
)
let client = ApolloClient(
networkTransport: splitTransport,
store: store
)
🔗 Client usage with Apollo Studio Explorer
In Explorer Settings, click "Edit" for "Connection Settings" and select graphql-ws
under "Implementation".
🔗 Client usage with GraphiQL
import React from 'react';
import ReactDOM from 'react-dom';
import { GraphiQL } from 'graphiql';
import { createGraphiQLFetcher } from '@graphiql/toolkit';
import { createClient } from 'graphql-ws';
const fetcher = createGraphiQLFetcher({
url: 'https://myschema.com/graphql',
wsClient: createClient({
url: 'wss://myschema.com/graphql',
}),
});
export const App = () => <GraphiQL fetcher={fetcher} />;
ReactDOM.render(document.getElementByID('graphiql'), <App />);
🔗 Client usage with retry on any connection problem
import { createClient } from 'graphql-ws';
import { waitForHealthy } from './my-servers';
const client = createClient({
url: 'ws://any.retry:4000/graphql',
shouldRetry: () => true,
});
🔗 Client usage with custom retry timeout strategy
import { createClient } from 'graphql-ws';
import { waitForHealthy } from './my-servers';
const client = createClient({
url: 'ws://i.want.retry:4000/control/graphql',
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,
opened: (socket) => {
options.on?.opened?.(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: 'ws://graceful.restart:4000/is/a/non-fatal/close-code',
connectionParams: async () => {
const token = await giveMeAFreshToken();
return { token };
},
});
🔗 Client usage with ping/pong timeout and latency metrics
import { createClient } from 'graphql-ws';
let activeSocket,
timedOut,
pingSentAt = 0,
latency = 0;
createClient({
url: 'ws://i.time.out:4000/and-measure/latency',
keepAlive: 10_000,
on: {
opened: (socket) => (activeSocket = socket),
ping: (received) => {
if (!received ) {
pingSentAt = Date.now();
timedOut = setTimeout(() => {
if (activeSocket.readyState === WebSocket.OPEN)
activeSocket.close(4408, 'Request Timeout');
}, 5_000);
}
},
pong: (received) => {
if (received) {
latency = Date.now() - pingSentAt;
clearTimeout(timedOut);
}
},
},
});
🔗 Client usage with abrupt termination on pong timeout
import { createClient } from 'graphql-ws';
let timedOut;
const client = createClient({
url: 'ws://terminate.me:4000/on-pong-timeout',
keepAlive: 10_000,
on: {
ping: (received) => {
if (!received ) {
timedOut = setTimeout(() => {
client.terminate();
}, 5_000);
}
},
pong: (received) => {
if (received) {
clearTimeout(timedOut);
}
},
},
});
🔗 Client usage with manual pings and pongs
import {
createClient,
Client,
ClientOptions,
stringifyMessage,
PingMessage,
PongMessage,
MessageType,
} from 'graphql-ws';
interface PingerClient extends Client {
ping(payload?: PingMessage['payload']): void;
pong(payload?: PongMessage['payload']): void;
}
function createPingerClient(options: ClientOptions): PingerClient {
let activeSocket: WebSocket;
const client = createClient({
disablePong: true,
...options,
on: {
opened: (socket) => {
options.on?.opened?.(socket);
activeSocket = socket;
},
},
});
return {
...client,
ping: (payload) => {
if (activeSocket.readyState === WebSocket.OPEN)
activeSocket.send(
stringifyMessage({
type: MessageType.Ping,
payload,
}),
);
},
pong: (payload) => {
if (activeSocket.readyState === WebSocket.OPEN)
activeSocket.send(
stringifyMessage({
type: MessageType.Pong,
payload,
}),
);
},
};
}
🔗 Client usage supported check
import { createClient } from 'graphql-ws';
function supportsGraphQLTransportWS(url: string): Promise<boolean> {
return new Promise((resolve) => {
const client = createClient({
url,
retryAttempts: 0,
lazy: false,
on: {
closed: () => resolve(false),
connected: () => {
resolve(true);
client.dispose();
},
},
});
});
}
const supported = await supportsGraphQLTransportWS(
'ws://some.unknown:4000/enpoint',
);
if (supported) {
} else {
}
🔗 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: 'ws://umdfor.the:4000/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: 'ws://no.browser:4000/graphql',
webSocketImpl: ws,
generateID: () =>
([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, (c) =>
(c ^ (Crypto.randomBytes(1)[0] & (15 >> (c / 4)))).toString(16),
),
});
🔗 Client usage with reconnect listener
import { createClient, Client, ClientOptions } from 'graphql-ws';
import { refetchSomeQueries } from './on-reconnected';
interface ClientWithOnReconnected extends Client {
onReconnected(cb: () => void): () => void;
}
function createClientWithOnReconnected(
options: ClientOptions,
): ClientWithOnReconnected {
let abruptlyClosed = false;
const reconnectedCbs: (() => void)[] = [];
const client = createClient({
...options,
on: {
...options.on,
closed: (event) => {
options.on?.closed?.(event);
if ((event as CloseEvent).code !== 1000) {
abruptlyClosed = true;
}
},
connected: (...args) => {
options.on?.connected?.(...args);
if (abruptlyClosed) {
abruptlyClosed = false;
reconnectedCbs.forEach((cb) => cb());
}
},
},
});
return {
...client,
onReconnected: (cb) => {
reconnectedCbs.push(cb);
return () => {
reconnectedCbs.splice(reconnectedCbs.indexOf(cb), 1);
};
},
};
}
const client = createClientWithOnReconnected({
url: 'ws://ireconnect:4000/and/notify',
});
const unlisten = client.onReconnected(() => {
refetchSomeQueries();
});
🔗 Server usage with ws
import { WebSocketServer } from 'ws';
import { makeServer, CloseCode } from 'graphql-ws';
import { schema } from './my-graphql-schema';
const server = makeServer({ schema });
const wsServer = new WebSocketServer({
port: 4000,
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(CloseCode.InternalServerError, 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 { WebSocketServer } from 'ws';
import { makeServer, CloseCode } 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);
},
});
const wsServer = new WebSocketServer({
port: 4000,
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(CloseCode.InternalServerError, err.message);
}
}
});
},
},
{ request },
);
socket.once('close', (code, reason) => closed(code, reason));
});
🔗 Server usage with ws and subprotocol pings and pongs
import { WebSocketServer } from 'ws';
import {
makeServer,
CloseCode,
stringifyMessage,
MessageType,
} from 'graphql-ws';
import { schema } from './my-graphql-schema';
const server = makeServer({ schema });
const wsServer = new WebSocketServer({
port: 4000,
path: '/graphql',
});
wsServer.on('connection', (socket, request) => {
let pinger, pongWait;
function ping() {
if (socket.readyState === socket.OPEN) {
socket.send(stringifyMessage({ type: MessageType.Ping }));
pongWait = setTimeout(() => {
clearInterval(pinger);
socket.close();
}, 6_000);
}
}
pinger = setInterval(() => ping(), 12_000);
const closed = server.opened(
{
protocol: socket.protocol,
send: (data) => socket.send(data),
close: (code, reason) => socket.close(code, reason),
onMessage: (cb) =>
socket.on('message', async (event) => {
try {
await cb(event.toString());
} catch (err) {
socket.close(CloseCode.InternalServerError, err.message);
}
}),
onPong: () => clearTimeout(pongWait),
},
{ socket, request },
);
socket.once('close', (code, reason) => {
clearTimeout(pongWait);
clearInterval(pinger);
closed(code, reason);
});
});
🔗 Server usage with Cloudflare Workers
Please check the worker-graphql-ws-template
repo out.
🔗 ws server usage with GraphQL Yoga
import { ExecutionArgs, execute, subscribe } from 'graphql';
import { createServer } from '@graphql-yoga/node';
import { WebSocketServer } from 'ws';
import { useServer } from 'graphql-ws/lib/use/ws';
import { schema } from './my-graphql-schema';
async function main() {
const yogaApp = createServer({
schema,
graphiql: {
subscriptionsProtocol: 'WS',
},
});
const httpServer = await yogaApp.start();
const wsServer = new WebSocketServer({
server: httpServer,
path: yogaApp.getAddressInfo().endpoint,
});
type EnvelopedExecutionArgs = ExecutionArgs & {
rootValue: {
execute: typeof execute;
subscribe: typeof subscribe;
};
};
useServer(
{
execute: (args) =>
(args as EnvelopedExecutionArgs).rootValue.execute(args),
subscribe: (args) =>
(args as EnvelopedExecutionArgs).rootValue.subscribe(args),
onSubscribe: async (ctx, msg) => {
const { schema, execute, subscribe, contextFactory, parse, validate } =
yogaApp.getEnveloped(ctx);
const args: EnvelopedExecutionArgs = {
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,
);
}
main().catch((e) => {
console.error(e);
process.exit(1);
});
🔗 ws server usage with Express GraphQL
import { WebSocketServer } 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(4000, () => {
const wsServer = new WebSocketServer({
server,
path: '/graphql',
});
useServer({ schema }, wsServer);
});
🔗 ws server usage with Apollo Server Express
import { ApolloServer } from 'apollo-server-express';
import { createServer } from 'http';
import express from 'express';
import { ApolloServerPluginDrainHttpServer } from 'apollo-server-core';
import { WebSocketServer } from 'ws';
import { useServer } from 'graphql-ws/lib/use/ws';
import { schema } from './my-graphql-schema';
const app = express();
const httpServer = createServer(app);
const wsServer = new WebSocketServer({
server: httpServer,
path: '/graphql',
});
const serverCleanup = useServer({ schema }, wsServer);
const apolloServer = new ApolloServer({
schema,
plugins: [
ApolloServerPluginDrainHttpServer({ httpServer }),
{
async serverWillStart() {
return {
async drainServer() {
await serverCleanup.dispose();
},
};
},
},
],
});
await apolloServer.start();
apolloServer.applyMiddleware({ app });
httpServer.listen(4000);
🔗 ws server usage with Apollo Server Hapi.js
import {
ApolloServer,
ApolloServerPluginStopHapiServer,
} from 'apollo-server-hapi';
import { ApolloServerPluginDrainHttpServer } from 'apollo-server-core';
import Hapi from '@hapi/hapi';
import { WebSocketServer } from 'ws';
import { useServer } from 'graphql-ws/lib/use/ws';
import { createServer } from 'http';
import { schema } from './my-graphql-schema';
const httpServer = createServer();
const hapiServer = Hapi.server({
port: 4001,
host: 'localhost',
listener: httpServer,
routes: { security: true },
});
const wsServer = new WebSocketServer({
server: httpServer,
path: '/graphql',
});
const serverCleanup = useServer({ schema }, wsServer);
const apolloServer = new ApolloServer({
schema,
plugins: [
ApolloServerPluginDrainHttpServer({ httpServer }),
ApolloServerPluginStopHapiServer({ hapiServer }),
{
async serverWillStart() {
return {
async drainServer() {
await serverCleanup.dispose();
},
};
},
},
],
});
await apolloServer.start();
await apolloServer.applyMiddleware({ app: hapiServer });
await hapiServer.start();
console.log('Open GraphQL editor on: %s/graphql', hapiServer.info.uri);
🔗 ws server usage with deprecated fastify-websocket
import Fastify from 'fastify';
import fastifyWebsocket from 'fastify-websocket';
import { makeHandler } from 'graphql-ws/lib/use/fastify-websocket';
import { schema } from './previous-step';
const fastify = Fastify();
fastify.register(fastifyWebsocket);
fastify.get('/graphql', { websocket: true }, makeHandler({ schema }));
fastify.listen(4000, (err) => {
if (err) {
fastify.log.error(err);
return process.exit(1);
}
console.log('Listening to port 4000');
});
🔗 ws server usage with subscriptions-transport-ws backwards compatibility
import http from 'http';
import { WebSocketServer } 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 WebSocketServer({ noServer: true });
useServer({ schema }, graphqlWs);
const subTransWs = new WebSocketServer({ noServer: true });
SubscriptionServer.create(
{
schema,
execute,
subscribe,
},
subTransWs,
);
const server = http.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);
});
});
server.listen(4000);
🔗 ws server usage with console logging
import { WebSocketServer } from 'ws';
import { useServer } from 'graphql-ws/lib/use/ws';
import { schema } from './my-graphql-schema';
const wsServer = new WebSocketServer({
port: 4000,
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 http from 'http';
import { WebSocketServer } from 'ws';
import url from 'url';
import { useServer } from 'graphql-ws/lib/use/ws';
import { schema } from './my-graphql-schema';
const server = http.createServer(function weServeSocketsOnly(_, res) {
res.writeHead(404);
res.end();
});
const waveWS = new WebSocketServer({ noServer: true });
const graphqlWS = new WebSocketServer({ 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(4000);
🔗 ws server usage with custom context value
import { WebSocketServer } from 'ws';
import { useServer } from 'graphql-ws/lib/use/ws';
import { schema, getDynamicContext } from './my-graphql';
const wsServer = new WebSocketServer({
port: 4000,
path: '/graphql',
});
useServer(
{
context: (ctx, msg, args) => {
return getDynamicContext(ctx, msg, args);
},
schema,
},
wsServer,
);
🔗 ws server usage with dynamic schema
import { WebSocketServer } from 'ws';
import { useServer } from 'graphql-ws/lib/use/ws';
import { schema, checkIsAdmin, getDebugSchema } from './my-graphql';
const wsServer = new WebSocketServer({
port: 4000,
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 { WebSocketServer } from 'ws';
import { useServer } from 'graphql-ws/lib/use/ws';
import { validate } from 'graphql';
import { schema, myValidationRules } from './my-graphql';
const wsServer = new WebSocketServer({
port: 4000,
path: '/graphql',
});
useServer(
{
validate: (schema, document) =>
validate(schema, document, myValidationRules),
},
wsServer,
);
🔗 ws server usage with custom execution arguments
import { parse, validate } from 'graphql';
import { WebSocketServer } from 'ws';
import { useServer } from 'graphql-ws/lib/use/ws';
import { schema, myValidationRules } from './my-graphql';
const wsServer = new WebSocketServer({
port: 4000,
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 { WebSocketServer } from 'ws';
import { useServer } from 'graphql-ws/lib/use/ws';
import { schema } from './my-graphql';
const wsServer = new WebSocketServer({
port: 4000,
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, ExecutionArgs } from 'graphql';
import { WebSocketServer } 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 WebSocketServer({
port: 4000,
path: '/graphql',
});
useServer(
{
onSubscribe: (_ctx, msg) => {
const persistedQuery =
queriesStore[msg.payload.extensions?.persistedQuery];
if (persistedQuery) {
return {
...persistedQuery,
variableValues: msg.payload.variables,
};
}
throw new Error('404: Query Not Found');
},
},
wsServer,
);
import { createClient } from 'graphql-ws';
const client = createClient({
url: 'ws://persisted.graphql:4000/queries',
});
(async () => {
const onNext = () => {
};
await new Promise((resolve, reject) => {
client.subscribe(
{
query: '',
extensions: {
persistedQuery: 'iWantTheGreetings',
},
},
{
next: onNext,
error: reject,
complete: resolve,
},
);
});
expect(onNext).toBeCalledTimes(5);
})();
🔗 ws server and client auth usage with token expiration, validation and refresh
import { WebSocketServer } from 'ws';
import { useServer } from 'graphql-ws/lib/use/ws';
import { CloseCode } from 'graphql-ws';
import { schema } from './my-graphql-schema';
import { isTokenValid } from './my-auth';
const wsServer = new WebSocket.Server({
port: 4000,
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(CloseCode.Forbidden, 'Forbidden');
},
},
wsServer,
);
import { createClient, CloseCode } from 'graphql-ws';
import {
getCurrentToken,
getCurrentTokenExpiresIn,
refreshCurrentToken,
} from './my-auth';
let shouldRefreshToken = false,
tokenExpiryTimeout = null;
const client = createClient({
url: 'ws://server-validates.auth:4000/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(CloseCode.Forbidden, 'Forbidden');
}, getCurrentTokenExpiresIn());
},
closed: (event) => {
if (event.code === CloseCode.Forbidden) shouldRefreshToken = true;
},
},
});
🔗 ws server and client usage with subscription acknowledgment
import { WebSocketServer } from 'ws';
import { MessageType, stringifyMessage } from 'graphql-ws';
import { useServer } from 'graphql-ws/lib/use/ws';
import { schema } from './my-graphql-schema';
const wsServer = new WebSocketServer({
port: 4000,
path: '/graphql',
});
useServer<undefined, { ackWaiters: Record<string, () => void> }>(
{
schema,
onConnect: (ctx) => {
ctx.extra.ackWaiters = {};
},
onSubscribe: (ctx, msg) => {
const ackId = msg.payload.extensions?.ackId;
if (typeof ackId === 'string') {
ctx.extra.ackWaiters![msg.id] = () => {
ctx.extra.socket.send(
stringifyMessage({
type: MessageType.Ping,
payload: {
ackId,
},
}),
);
};
}
},
onOperation: (ctx, msg) => {
ctx.extra.ackWaiters![msg.id]?.();
delete ctx.extra.ackWaiters![msg.id];
},
},
wsServer,
);
console.log('Listening to port 4000');
import {
Client,
ClientOptions,
createClient,
ExecutionResult,
Sink,
SubscribePayload,
} from 'graphql-ws';
type ClientWithSubscribeAck = Omit<Client, 'subscribe'> & {
subscribe<Data = Record<string, unknown>, Extensions = unknown>(
payload: SubscribePayload,
sink: Sink<ExecutionResult<Data, Extensions>>,
onAck: () => void,
): () => void;
};
function createClientWithSubscribeAck(
options: ClientOptions,
): ClientWithSubscribeAck {
const client = createClient(options);
const ackListeners: Record<string, () => void> = {};
client.on('ping', (_received, payload) => {
const ackId = payload?.ackId;
if (typeof ackId === 'string') {
ackListeners[ackId]?.();
delete ackListeners[ackId];
}
});
return {
...client,
subscribe: (payload, sink, onAck) => {
const ackId = Math.random().toString();
ackListeners[ackId] = onAck;
return client.subscribe(
{
...payload,
extensions: {
...payload.extensions,
ackId,
},
},
sink,
);
},
};
}
Using the augmented client would be as simple as:
const client = createClientWithSubscribeAck({
url: 'ws://i.want.ack:4000/graphql',
});
(async () => {
const onNext = () => {
};
let unsubscribe = () => {
};
let subscriptionAcknowledged = () => {
};
await new Promise((resolve, reject) => {
unsubscribe = client.subscribe(
{
query: 'subscription { greetings }',
},
{
next: onNext,
error: reject,
complete: resolve,
},
subscriptionAcknowledged,
);
});
expect(subscriptionAcknowledged).toBeCalledFirst();
expect(onNext).then.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!
Disclaimer
This library and the GraphQL over WebSocket Protocol are not cross-compatible with the deprecated 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.