Getting started
Installation
Grafast is an alternative execution layer for GraphQL; we still need GraphQL.js
for building the schema, and parsing and validating requests. So the first thing
you need to do to get started is to install grafast and graphql:
- npm
- Yarn
- pnpm
- Bun
npm install --save grafast graphql
yarn add grafast graphql
pnpm add grafast graphql
bun add grafast graphql
We intend to write up a specification so that other languages may implement the Grafast execution strategy, but for now Grafast is JavaScript/TypeScript only.
If you have an existing GraphQL.js schema, you can run it through Grafast ─ see using with an existing schema.
TypeScript v5.0.0+ (optional)
We recommend that you use TypeScript for the best experience - auto-completion, inline documentation, etc.
You do not need to use TypeScript to use Grafast, but if you do then you
must use a version from TypeScript v5.0.0 upward and configure it to support the
exports property in package.json by setting moduleResolution to node16
or nodenext. If you already have a tsconfig.json, you can do so by adding
this:
{
"compilerOptions": {
+ "moduleResolution": "node16" // Or "nodenext"
"strict": true,
// ...
Otherwise, a minimal tsconfig that enables this could use the @tsconfig/node24 module:
{
"extends": "@tsconfig/node24/tsconfig.json"
}
Our adherence to semver does not cover types - we may make breaking changes to TypeScript types in patch-level updates. The reason for this is that TypeScript itself is ever-changing, and the libraries we depend on often make breaking type changes, forcing us to do so too. Further, improvements to types are generally a good thing for developer experience, even if it might mean you have to spend a couple minutes after updating to address any issues.
However, we try and keep the TypeScript types as stable as possible, only making breaking changes when their benefits outweigh the costs (as determined by our maintainer), and we do our best to detail in the release notes how to deal with these changes (if any action is necessary).
Not using TypeScript?
You do not need to use TypeScript to use PostGraphile, but without it you will
find editors such as VSCode will highlight your import paths with error
notifications. To stop this, you can add the following to jsconfig.json:
{
"compilerOptions": {
"moduleResolution": "node16"
}
}
My first plan
Let's build a simple GraphQL schema powered by Grafast plans and query it.
See https://github.com/grafast/getting-started for this project fully fleshed out.
For the following example we're going to use Node v24's built in type stripping support along with the matching tsconfig.json configuration:
{
"extends": [
"@tsconfig/node24/tsconfig.json",
"@tsconfig/node-ts/tsconfig.json"
]
}
And we're using ESM, so we add "type": "module" to our package.json file.
{
"type": "module",
"private": true,
"dependencies": {
"grafast": "^1.0.0-rc.1",
"graphql": "^16.12.0"
},
"devDependencies": {
"@tsconfig/node-ts": "^23.6.1",
"@tsconfig/node24": "^24.0.1",
}
}
Other setups may require helpers to run (for example ts-node, tsx, or
compilation through tsc or similar), and may require slight modification of
the source code (e.g. removal of file extensions).
We have a playground you can use for experimenting with Grafast without having to install any software.
Let's build an incredibly simple schema with a single field that adds together
its two arguments. To build the schema, we can use the makeGrafastSchema
helper which will stitch the typeDefs and the plans together.
There are many ways to build a GraphQL schema, we're going to use the "schema first" approach in this example, but there's no reason that a Grafast schema couldn't be produced "code first" or "database first" or any other approach.
Our schema is described via the GraphQL schema definition
language (SDL) in typeDefs, and the Query object's plan for the
addTwoNumbers field is provided alongside:
import { makeGrafastSchema, lambda } from "grafast";
export const schema = makeGrafastSchema({
typeDefs: /* GraphQL */ `
type Query {
addTwoNumbers(a: Int!, b: Int!): Int
}
`,
objects: {
Query: {
plans: {
addTwoNumbers(_, fieldArgs) {
const { $a, $b } = fieldArgs;
return lambda([$a, $b], ([a, b]) => a + b);
},
},
},
},
});
The plan for our Query.addTwoNumbers field is to
read the arguments, then use the lambda step to add them
together. The lambda step takes a list of other steps, and then determines the
result by calling the given callback for each set of resulting values.
lambda is a bit of an escape hatch ─ it enables one-by-one processing of
values rather than the batched processing that Grafast prefers for
efficiency. It can be handy as a utility function when batching would confer no
benefit, but in general you should pick a more suitable step;
for loading data from a remote source that would typically be loadOne or
loadMany.
Making the callback function to lambda a global (defined once) function would
enable Grafast to potentially detect multiple uses of it and deduplicate
them. This is important for performance if a similar callback is used in lots of
places in a query.
Finally, we can run our query:
import { grafast } from "grafast";
import { schema } from "./schema.ts";
const result = await grafast({
schema,
source: /* GraphQL */ `
{
addTwoNumbers(a: 40, b: 2)
}
`,
});
console.log(JSON.stringify(result, null, 2));
Calling node src/main.ts should produce:
{
"data": {
"addTwoNumbers": 42
}
}
We could then serve this schema over HTTP using a server such as grafserv or any envelop-capable server.
My first step class
The building blocks of an operation plan are "steps." Steps are instances of
"step classes," and Grafast makes available a modest range of standard steps
that you can use; but when these aren't enough you can write your own.
Step classes extend the Step class. The only required method to
define is execute, however most steps will also have a constructor in which
they accept their arguments (some of which may be dependencies) and may also
have the various lifecycle methods.
Full details for doing so can be found in Step classes, but let's build
ourselves a simple one now to replace the lambda usage above:
import { Step, type ExecutionDetails } from "grafast";
class AddStep extends Step<number> {
constructor($a: Step<number>, $b: Step<number>) {
super();
this.addDependency($a);
this.addDependency($b);
}
execute(details: ExecutionDetails<[number, number]>) {
const {
indexMap,
values: [aDep, bDep],
} = details;
return indexMap((i) => {
const a = aDep.at(i);
const b = bDep.at(i);
return a + b;
});
}
}
export function add($a: Step<number>, $b: Step<number>) {
return new AddStep($a, $b);
}
Note that by convention, we always define a function that constructs an instance
of our class - in this case it's the add() function.
There's multiple reasons that we encourage the use of these step functions rather than using the step classes directly.
A simple reason is to make the plan code easier to read: we won't see the new
calls in our plan resolver functions, nor the redundant Step wording,
resulting in a higher signal-to-noise ratio.
More importantly, though, is that the small layer of indirection allows us to do manipulations before deferring to the class constructor. This makes the APIs more future-proof since we can have the function perform different actions or return something different (even a different step class) in future without having to refactor our plans in the schema.
The additional cost of this function call is only incurred at plan-time, plans are cached and reused between requests, and each field in an operation is only planned once; so the overhead of an additional function call is negligible.
Now we can use this function to add our numbers, rather than the lambda plan:
@@ -1,4 +1,4 @@
-import { lambda } from "grafast";
+import { add } from "./steps/add.ts";
import { typedMakeGrafastSchema } from "./schema-generated.ts";
export const schema = typedMakeGrafastSchema({
@@ -12,7 +12,7 @@ export const schema = typedMakeGrafastSchema({
plans: {
addTwoNumbers(_, fieldArgs) {
const { $a, $b } = fieldArgs;
- return lambda([$a, $b], ([a, b]) => a + b);
+ return add($a, $b);
},
},
},
You may well be able to write an entire Grafast schema using off-the-shelf step classes, but it's worth being aware of how step classes work in case you want to push your optimizations further. Read more about step classes, or continue through the documentation.