Getting Started
This tutorial will take us through the process of building a GraphQL server with @benzene/http and @benzene/ws. We will build a simple book voting app that can:
- Returns a list of books and authors
- Upvote a book
- Synchronize votes with real-time changes
Everything will be built with the bare http module and the ws library. We can adapt it to any other libraries.
In this tutorial, we will use the SDL-first library graphql-tools for our schema. (However, we can choose code-first libraries likes type-graphql and @nexus/schema)
Check out the source code or follow along.
Setup Project
Create a project and install the necessary dependencies.
mkdir book-votescd book-votesnpm init -ynpm i ws graphql @benzene/http @benzene/ws @graphql-tools/schemaAlso, make sure to set type to "module" in package.json since we are working with ESModule.
(Otherwise, we can write the upcoming code in CommonJS)
Define our schema
Create a file called schema.js and add the following:
import { makeExecutableSchema } from "@graphql-tools/schema";import { on, EventEmitter } from "events";
const authors = [ { id: 1, name: "Tom Coleman" }, { id: 2, name: "Sashko Stubailo" }, { id: 3, name: "Mikhail Novikov" },];
const books = [ { id: 1, authorId: 1, title: "Introduction to GraphQL", votes: 2 }, { id: 2, authorId: 2, title: "Welcome to Meteor", votes: 3 }, { id: 3, authorId: 2, title: "Advanced GraphQL", votes: 1 }, { id: 4, authorId: 3, title: "Launchpad is Cool", votes: 7 },];
const typeDefs = ` type Author { id: Int! name: String books: [Book] }
type Book { id: Int! title: String author: Author votes: Int }
type Query { books: [Book] }
type Mutation { bookUpvote ( bookId: Int! ): Book }
type Subscription { bookSubscribe: Book }`;
const ee = new EventEmitter();
const resolvers = { Query: { books: () => books, },
Mutation: { bookUpvote: (_, { bookId }) => { const book = books.find((book) => book.id === bookId); if (!book) { throw new Error(`Couldn't find book with id ${bookId}`); } book.votes += 1; ee.emit("BOOK_SUBSCRIBE", { bookSubscribe: book }); return book; }, },
Subscription: { bookSubscribe: { subscribe: async function* bookSubscribe() { for await (const event of on(ee, "BOOK_SUBSCRIBE")) { yield event[0]; } }, }, },
Author: { books: (author) => books.filter((book) => book.authorId === author.id), },
Book: { author: (book) => authors.find((author) => author.id === book.authorId), },};
const schema = makeExecutableSchema({ typeDefs, resolvers,});
export default schema;We created a GraphQL schema with three functionalities:
- A query to retrieve all books, as well resolvers for Book and Author to resolve nested fields.
- A mutation to upvote a book, which also announces the change to the
"BOOK_SUBSCRIBE"event. - A subscription to book updates that listens to the
"BOOK_SUBSCRIBE"event.
We use events.on to create the async iterator, but you may be more familiar with graphql-subscriptions)
Create the server
Create a file called server.js and add the following:
import { createServer } from "http";import WebSocket from "ws";import { Benzene, parseGraphQLBody, makeHandler } from "@benzene/http";import { makeHandler as makeHandlerWs } from "@benzene/ws";import schema from "./schema.js";
function readBody(request) { return new Promise((resolve) => { let body = ""; request.on("data", (chunk) => (body += chunk)); request.on("end", () => resolve(body)); });}
const GQL = new Benzene({ schema });
const graphqlHTTP = makeHandler(GQL);const graphqlWS = makeHandlerWs(GQL);
const server = createServer(async (req, res) => { const rawBody = await readBody(req); const result = await graphqlHTTP({ method: req.method, headers: req.headers, body: parseGraphQLBody(rawBody, req.headers["content-type"]), }); res.writeHead(result.status, result.headers); res.end(JSON.stringify(result.payload));});
const wss = new WebSocket.Server({ server });
wss.on("connection", (ws) => { graphqlWS(ws);});
server.listen(3000, () => { console.log(`🚀 Server ready at http://localhost:3000`);});Parse incoming request
Our defined readBody function read the data from the incoming request and output it as a string.
Meanwhile, the graphql-over-http spec allows different incoming Content-Type,
each must be parsed differently. We provide parseGraphQLBody function, which accepts the body string and the content type, to do just that.
We do not have to do this if we use a framework or library with body parsing, like express
Create a Benzene instance and transport handlers
After creating a Benzene instance, we create HTTP and WebSocket handler by supplying the it to the makeHandler functions from
@benzene/http and @benzene/ws.
@benzene/http is then called with a generic request object and returns a generic response object with status, headers, and payload so we can respond
as we wish. This allows us to work with any frameworks or runtimes.
Similarly, @benzene/ws is called with any WebSocket-like instance, so we can use it
with libraries other than ws.
Benzene class is exported from both @benzene/ws and @benzene/http
Start the application
Using the Svelte + urql app
Although setting up the client is not in the scope of this tutorial, examples/book-votes features one built with svelte and urql.

curl https://codeload.github.com/hoangvvo/benzene/tar.gz/main | tar -xz --strip=2 benzene-main/examples/book-votescd book-votesnpm inpm run startTrying out with DevTools
Without building a front-end, we can still test out the Getting Started example using DevTools.
Start the Node application using
node ./server.jsGo to the http://localhost:3000 and open the DevTools.
First, try to query all books:
await fetch("/graphql", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ query: "query { books { id, title, author { name }, votes } }", }),}).then((res) => res.json());Before we make a mutation, let's try to subscribe to our books' changes using GraphQL Subscription over WebSockets.
const websocket = new WebSocket("ws://localhost:3000/graphql", "graphql-transport-ws");// Wait a bit for WebSocket to connect and then run:websocket.send(JSON.stringify({ type: "connection_init" }));Take a look at the WS tab in the Network panel. We can see an opening WebSocket communicating with our GraphQL server.
Let's try to subscribe to bookSubscribe.
websocket.send( JSON.stringify({ type: "subscribe", id: "1", payload: { query: "subscription { bookSubscribe { title, votes } }" }, }));Now make a request to increase the vote:
await fetch("/graphql", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ query: "mutation { bookUpvote(bookId: 1) { id } }" }),}).then((res) => res.json());We will notice a message coming from the WebSocket channel:
{ "id": "1", "payload": { "data": { "bookSubscribe": { "title": "Introduction to GraphQL", "votes": 5 } } }, "type": "next"}