GraphQL over HTTP
GraphQL over HTTP is supported by the @benzene/http package.
It implements the server according to the official GraphQL over HTTP specification.
Create handler function
Use makeHandler
from @benzene/http
to create a handler from a Benzene instance.
import { Benzene, makeHandler } from "@benzene/http";
import schema from "./schema";
const GQL = new Benzene({ schema });
const graphqlHTTP = makeHandler(GQL, options);
Configuration
makeHandler
allows a custom options
param, containing the following field:
onParams
: A function that is called whenGraphQLParams
is resolved from request query and body. Can be used to override theGraphQLParams
or returns an early response. See Override GraphQLParams.
Request handling
Being framework-agnostic, @benzene/http
does not automatically handle the request and response for us. However, it is easy to do so using your framework-specific APIs.
Body parsing
The graphql-over-http spec allows different incoming Content-Type, each must be parsed differently.
The most popular content types in real-world GraphQL application are application/graphql+json
and the legacy application/json
.
While application/json
is parsed by framework with built-in body parser like express, other content types may not be supported/be parsed incorrectly.
In those cases, we must first parse it into appropriate representation using the parseGraphQLBody
export.
import { parseGraphQLBody } from "@benzene/http";
async function onRequest(req) {
const rawBody = await getRawBody(req);
const body = parseGraphQLBody(rawBody, req.headers["content-type"]);
}
Behind the scene, parseGraphQLBody
will call JSON.parse or apply special transformations based on the received content type according to the graphql-over-http spec.
Handle incoming HTTP request
graphqlHTTP
is a framework-agnostic function that accepts a generic request object with the following shape:
interface HTTPRequest {
method: string;
query?: Record<string, string | string[]>;
body?: Record<string, any>;
headers: Headers;
}
type Headers = Record<string, string | string[] | undefined>;
Regardless of frameworks and runtimes, we simply need to call it using an object with:
method
(The HTTP method),query
(an object with query parameters - optional)body
(an object of the request body - can be retrieved usingparseGraphQLBody
- optional)headers
(an object of all headers, but an object containing onlycontent-type
should be sufficient)
async function onRequest(req, res) {
const rawBody = await getRawBody(req);
const result = await graphqlHTTP({
method: req.method,
body: parseGraphQLBody(rawBody, req.headers["content-type"]),
headers: req.headers,
query: parseQueryString(req.url),
});
}
A GraphQLParams
will constructed using values from body
and query
.
Respond with GraphQL result
graphqlHTTP
will resolve into a generic response object:
interface HTTPResponse {
status: number;
headers: Headers;
payload: FormattedExecutionResult;
}
We will use values in this object the send back the HTTP response:
// Node.js
res
.writeHead(result.status, result.headers)
.end(JSON.stringify(result.payload));
// Deno
req.respond({
body: JSON.stringify(result.payload),
status: result.status,
headers: new Headers(result.headers), // headers in deno must be a Header instance
});
From the example above, we can see that Benzene gives us full control over how we send the HTTP response after the GraphQL execution. It also allows us to plug the library into any HTTP libraries without needing a binding package.
Passing extra
It is possible to pass extra
as the second argument to graphqlHTTP
. See The extra argument.
Override GraphQLParams
It is possible to override the GraphQLParams by setting a function to options.onParams
.
The function will receive the params resolved from the request query and body, which you can override by returning a new one. If a value is not returned from onParams
, the original params
is used.
makeHandler(GQL, {
onParams(params) {
return {
query: "query Foo { myGraphQLQuery }",
variables: {
someVariable: "foo",
},
operationName: "Foo",
extensions: {},
};
},
});
This is useful if we need to implement things like persisted queries:
makeHandler(GQL, {
onParams(params) {
const query = findQueryFromHash(params.extensions.queryHash);
return {
...params,
query,
};
},
});
Optionally, you can also return an ExecutionResult early.
makeHandler(GQL, {
onParams(params) {
if (params.query.includes("evilGraphQLQuery")) {
return {
errors: [new GraphQLError("Query is forbidden")],
};
}
// returning nothing to use the original params
},
});