Skip to main content

Grafserv Introduction

Grafserv - an incredibly fast GraphQL server for Node.js powered by Grafast.

Usage

Grafserv supports many different servers, and because each server is different each has their own entrypoint, e.g. grafserv/node for the Node.js HTTP server or grafserv/express/v4 for Express v4. Generally you import the grafserv function from the relevant entrypoint for your server of choice and then create an instance:

const serv = grafserv({ schema, preset });

grafserv is passed the GraphQL schema to use (if it's available, otherwise passing either null or a promise is also acceptable) and a graphile-config preset - i.e. your configuration. The preset can be an empty object, but here's a bigger (but not exhaustive) example:

const preset = {
grafserv: {
port: 5678,
host: "0.0.0.0",
dangerouslyAllowAllCORSRequests: false,
graphqlPath: "/graphql",
eventStreamPath: "/graphql/stream",
graphqlOverGET: true,
graphiql: true,
graphiqlPath: "/",
websockets: true,
maxRequestLength: 100000,
},
};

Calling grafserv will return an instance; this instance will have a number of helpers on it, including helpers specific to integrating it with your framework of choice. For servers that operate on a middleware basis this is typically serv.addTo(app) (which allows registering multiple route handlers), though different servers may have different APIs, such as serv.createGraphQLHandler() for Lambda and Next.js.

Note: There is little value in Grafserv reimplementing every non-GraphQL concern your server may have, so instead it leans on the ecosystem of your chosen server to handle things like compression, rate limits, sessions, cookies, etc. For example, to compress your responses you'd need to use a module like compression for Express, koa-compress for Koa, or @fastify/compress for Fastify.

serv.release()

Releases any resources created by the instance; no further requests should be handled (though currently active requests will be allowed to complete).

// TODO: consider terminating subscriptions or other long-lived things.

serv.onRelease(cb)

Adds cb to the list of callbacks to be called when the server is released; useful for releasing resources you created only for the server. Callbacks will be called in reverse order that they were added.

serv.setSchema(newSchema)

Replaces the schema to use for future requests (currently active requests are unaffected) - this is primarily used for "watch" mode.

serv.setPreset(newPreset)

Replaces the config to use for future requests (currently active requests are unaffected) - this is primarily used for "watch" mode. Note that certain configuration changes might not be reflected by certain servers until a restart.

serv.getSchema()

Returns either the GraphQL schema that is currently in use, or a promise to the same.

serv.getPreset()

Returns the resolved graphile-config preset that is currently in use.

Masking errors

To protect your systems from a class of potential information disclosure bugs, by default Grafserv masks "unsafe" errors before they reach the client. "Safe" errors are errors that are constructed either via new SafeError(...), or new GraphQLError(...) without an originalError - all other errors are treated as "unsafe" and will be masked by default.

Supply preset.grafserv.maskError to take control of error masking; the function must return the GraphQLError instance that should be sent back to the caller. If you do not provide an override, Grafserv uses defaultMaskError, which passes safe errors through, but for unsafe errors it will:

  1. Generate an errorId for them (random string) and a hash of the message
  2. Log the error on the server side (Masked GraphQL error (hash: '...', id: '...')[...])
  3. Replace the client-facing message of the error with a reference to this (An error occurred (logged with hash: '...', id: '...'))
Only expose intentional data

Any data you add to the masked error will be returned to clients. Reuse the incoming error path and avoid leaking stack traces or secrets.

To log all errors on the server-side while retaining Grafserv's masking behaviour, you can wrap the default implementation:

graphile.config.mjs
import { defaultMaskError } from "grafserv";

export default {
grafserv: {
maskError(error) {
console.error("GraphQL execution error:", error);
return defaultMaskError(error);
},
},
};