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. However, we can adapt it to any other libraries.
In this tutorial, we will use the SDL-first library graphql-tools for our schema. (We can also 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-votes
cd book-votes
npm init -y
npm i ws graphql @benzene/http @benzene/ws @graphql-tools/schema
Also, 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 the HTTP and WebSocket handlers by supplying 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. Check out Examples for more integrations.
Similarly, @benzene/ws
is called with any WebSocket-like instance, so we can use it
with libraries other than ws
.
The same Benzene class is exported from both @benzene/ws
and @benzene/http
Optional: Use a different runtime
Benzene supports different implementations of GraphQL. If you are looking for a more performant GraphQL server,
take a look at the runtime documentation.
Below is an example using graphql-jit
(from the @benzene/jit package) to achieve more than 2x performance gain.
import { makeCompileQuery } from "@benzene/jit";
const GQL = new Benzene({
compileQuery: makeCompileQuery(),
});
Also, check out the benchmarks section to learn more about the difference in performance.
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-votes
cd book-votes
npm i
npm run start
Trying 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.js
Go to the http://localhost:3000 and open the DevTools.
First, try to query all books:
await fetch("/graphql", {
method: "POST",
// application/json is also supported as a legacy content-type
headers: { "content-type": "application/graphql+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/graphql+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"
}