GraphQL over WebSocket Protocol Client & Server
graphql-ws is a JavaScript/TypeScript library that provides a coherent, zero-dependency, and lazy implementation of the GraphQL over WebSocket Protocol for both server and client applications. The current stable version is 6.0.8, with a development cadence that includes frequent patch releases for bug fixes and minor enhancements. Major versions, like v6, typically introduce targeted breaking changes related to API adjustments or adapter integrations. A crucial differentiator is its strict adherence to the modern GraphQL over WebSocket Protocol, which makes it explicitly incompatible with the older, deprecated `subscriptions-transport-ws` library and its distinct protocol. The library offers flexible integration with various Node.js WebSocket server implementations such as `ws`, Fastify's `@fastify/websocket`, and `crossws`, catering to diverse server environments.
Common errors
-
WebSocket connection to 'ws://localhost:4000/graphql' failed: WebSocket opening handshake timed out
cause The WebSocket server is not running, not listening on the specified port/path, or a firewall is blocking the connection.fixEnsure your GraphQL WebSocket server is actively running and accessible at `ws://localhost:4000/graphql`. Check server logs for errors and firewall configurations. -
WebSocket connection to 'ws://localhost:4000/graphql' failed: Error during WebSocket handshake: Unexpected response code: 400
cause The server received a WebSocket connection request but rejected it, often due to an incorrect subprotocol or an invalid connection initialization payload from the client. This can also occur if the HTTP server is serving the same path as the WebSocket server without proper upgrade handling.fixVerify that your client is using the correct subprotocol (`graphql-ws`) and sending a valid `ConnectionInit` message. On the server, ensure proper WebSocket upgrade logic is in place for the specified path, and check `onConnect` hooks for rejection conditions. -
Cannot find module 'graphql-ws/lib/use/ws' or its corresponding type declarations.
cause This error typically occurs in v6+ when an old import path for adapters is used. The `/lib` segment was removed from the import paths.fixUpdate the import path to `import { useServer } from 'graphql-ws/use/ws';` (remove `/lib`). -
TypeError: (0 , graphql_ws__WEBPACK_IMPORTED_MODULE_0__.createClient) is not a function
cause This usually indicates a CommonJS `require()` style import is being used in an ESM context, or vice-versa, leading to incorrect module resolution, especially with `graphql-ws`'s dual package setup.fixEnsure you are using `import { createClient } from 'graphql-ws';` in ESM contexts. If strictly in CommonJS (though less common for client), use dynamic import or check your bundler/TypeScript configuration for module interop issues.
Warnings
- breaking Starting from v6, for the `@fastify/websocket` adapter, the `connection` property in `ctx.extra` has been renamed to `socket`. This change requires updating any server-side code that accesses the raw WebSocket instance via the context.
- breaking The import paths for adapters like `useServer` (for `ws`) and `makeHandler` (for `@fastify/websocket`) changed from `graphql-ws/lib/use/<adapter>` to `graphql-ws/use/<adapter>` in v6.
- breaking graphql-ws is explicitly not compatible with the older, deprecated `subscriptions-transport-ws` library due to different WebSocket subprotocols. Mixing them will lead to connection failures.
- breaking Minimum Node.js version supported is 20. Attempts to run on older Node.js versions will result in runtime errors.
- gotcha The `uWebSockets.js` library was removed from `graphql-ws`'s peer dependencies in v6.0.7 because it's no longer on NPM. While it can still be used if installed manually, `npm` might report warnings or issues if not handled carefully.
- gotcha The `onSubscribe`, `onOperation`, `onError`, `onNext`, and `onComplete` hooks in `useServer` no longer receive the full message object, only the ID and the relevant payload part. This simplifies the API and avoids redundant serialization.
Install
-
npm install graphql-ws -
yarn add graphql-ws -
pnpm add graphql-ws
Imports
- createClient
const createClient = require('graphql-ws').createClient;import { createClient } from 'graphql-ws'; - useServer
import { useServer } from 'graphql-ws/use/ws'; // Incorrect path pre-v6import { useServer } from 'graphql-ws/lib/use/ws'; - makeHandler
import { makeHandler } from 'graphql-ws/use/@fastify/websocket'; // Incorrect path pre-v6import { makeHandler } from 'graphql-ws/lib/use/@fastify/websocket';
Quickstart
import { createServer } from 'http';
import { WebSocketServer } from 'ws';
import { useServer } from 'graphql-ws/lib/use/ws';
import { makeExecutableSchema } from '@graphql-tools/schema';
import { PubSub } from 'graphql-subscriptions';
import { createClient } from 'graphql-ws';
// --- Server Setup ---
const pubsub = new PubSub();
const HELLO_EVENT = 'hello_event';
const typeDefs = `
type Query {
hello: String
}
type Subscription {
greetings: String
}
`;
const resolvers = {
Query: {
hello: () => 'world',
},
Subscription: {
greetings: {
subscribe: () => pubsub.asyncIterator(HELLO_EVENT),
resolve: (payload) => payload.greetings,
},
},
};
const schema = makeExecutableSchema({ typeDefs, resolvers });
const server = createServer((req, res) => {
res.writeHead(404);
res.end();
});
const wsServer = new WebSocketServer({
server,
path: '/graphql',
});
useServer(
{
schema,
context: async (ctx) => {
// Example: access the websocket instance through ctx.extra.socket in v6+
// const socket = ctx.extra.socket;
return { currentUser: 'someUser' };
}
},
wsServer
);
server.listen(4000, () => {
console.log('GraphQL server running on http://localhost:4000/graphql');
console.log('WebSocket server running on ws://localhost:4000/graphql');
let count = 0;
setInterval(() => {
pubsub.publish(HELLO_EVENT, { greetings: `Hello from server! (${count++})` });
}, 2000);
});
// --- Client Setup ---
const client = createClient({
url: 'ws://localhost:4000/graphql',
webSocketImpl: WebSocket, // Required for Node.js environments; in browser, it's global
on: {
connected: () => console.log('Client connected to server.'),
closed: (event) => console.log(`Client disconnected: ${event.code} - ${event.reason}`),
error: (err) => console.error('Client error:', err),
},
});
async function subscribeToGreetings() {
const onNext = ({ data }) => {
console.log('Received greeting:', data.greetings);
};
const onError = (err) => {
console.error('Subscription error:', err);
};
const onComplete = () => {
console.log('Subscription complete.');
};
const unsubscribe = client.subscribe(
{
query: `subscription { greetings }`,
},
{
next: onNext,
error: onError,
complete: onComplete,
}
);
setTimeout(() => {
unsubscribe();
console.log('Unsubscribed from greetings.');
client.dispose(); // Close the WebSocket connection
server.close(); // Close the HTTP/WebSocket server
}, 10000);
}
// Run 'npm install ws graphql-subscriptions @graphql-tools/schema graphql' first
subscribeToGreetings().catch(console.error);