GraphQL as a typesafe contract for your domain!
Tags: graphql, typescript, node, nexus
Published: 2020-11-27 09:44:15

# Building type-safe GraphQL schemas with *@nexus/schema* and TypeScript
In this post, I will discuss how *@nexus/schema* can be used to construct a typesafe GraphQL schema. I will assume that the reader has a passing familiarity with GraphQL. If interested in learning the basics of GraphQL look up GraphQL's homepage on [https://graphql.org/learn/](https://graphql.org/learn/).

## Motivation
When writing type-safe GraphQL implementations based on *graphql-js* in the past, the developer always had to take great care to supply the correct types to all resolvers themselves. Providing the correct types was often error prone; you either had to reference a common type export from the consuming and producing resolver, or take great care to track down all consuming and producing resolvers related to a type if it ever changed. *@nexus/schema* seeks to make this largely a thing of the past; by combining new type constructors wrapping vanilla *graphql-js* type constructors and allowing the developer to supply a few type imports, this allows *@nexus/schema* to generate typescript artifacts when you run your schema. This makes it possible to see most type errors you've made in relation to your schema before even having to test it.

# makeSchema
The *makeSchema* function is what ties everything together and makes the magic of *@nexus/schema* happen. In order to generate the appropriate type artifacts this function needs to be given the correct configuration, this includes references to all types used by your schema.

```typescript
export const schema = makeSchema({
  types: [...schemaTypes, ...scalars],
  outputs: {
    schema: path.resolve('./src/generated/schema.graphql'),
    // where to save the schema declaration artifact
    typegen: path.resolve('./src/generated/typings.ts'),
    // where to save the typescript schema definitions artifact
  },
});
```
Here we provide the config with all types used in the schema and instruct *@nexus/schema* in where to output the SDL schema artifacts and the typescript typing artifacts, these generated files should ideally not be included in your git repository and should instead be generated ad-hoc in your CI pipeline.

In the schema configuration you should also provide some configuration for where the Context type can be found for the schema.
```typescript
export const schema = makeSchema({
  ...,
  typegenAutoConfig: {
    sources: [
      {
        source: path.resolve(__dirname, './context.ts'),
        // this points to where the RequestContext type can be imported from
        alias: 'ctx',
        // the alias the module containing the RequestContext type is given in the schema typings artifact
      },
    ],
    contextType: 'ctx.RequestContext',
    // the path to the RequestContext in the typings artifact
  },
});
```
This instructs *@nexus/schema* to import the file './context.ts' which is collocated to the file performing the `makeSchema` function call and using the type exported from that module called `RequestContext` as the type of the Context parameter for all resolvers in your schema.

# Creating schema types
Schema types for a *@nexus/schema* schema are constructed through the use of a set of constructor methods, each of which takes a config object; containing among other things the name of the type, and an optional description of it.
* *objectType*:
  This function is used for constructing the underlying *GraphQLObjectType*. The config must be provided with a *definition* function which takes an *ObjectDefinitionBlock* parameter. This parameter is what is used to add fields to the type by calling methods named after the type the field should return or by calling *field* and providing it with the correct return type of the field. Each of these functions must be provided with the name of the field that they are adding and a config for the field containing a *resolve* function, this function becomes typesafe after the type artifacts have been generated. The *ObjectDefinitionBlock* is also used to instructing GraphQL that the object type should implement an interface through the use of the *implements* method.
* *interfaceType*:
  The *interfaceType* function works much the same as the *objectType* function, it is used to construct the underlying GraphQLInterfaceType.
* *unionType*:
  This function is used to constructing the underlying *GraphQLUnionType*. The config for this type must be provided with a *definition* function which takes a *UnionDefinitionBlock*. This is used to add members to the type through the *members* method, and instructing *graphql-js* in how to determine which member type a given object, returned to a field that should resolve to the union type, should resolve to.
* *extendType*:
  This function is used to append an existing object type. It should be given a config containing the type that is being extended and a *definition* function like *objectType* which adds any new fields.
* *queryField*:
  This is a decorated version of the *extendType* function which only acts on the *Query* type and thus is only given the *definition* function. It should be used to declare any queries possible in the schema.
* *mutationField*:
  This is a decorated version of the *extendType* function which only acts on the *Mutation* type and thus is only given the *definition* function. It should be used to declare any mutations possible in the schema.
* *enumType*:
  The *enumType* function is used to construct the GraphQLEnumType. This function must be given the set of members of the enum through the *members* property.
* *scalarType*:
  The *scalarType* function is used to construct scalar types. These types have special handling, if *asNexusMethod* is set to true in their config they will become available on the *ObjectDefinitionBlock* type. The config should also specify 3 functions:
  * *parseLiteral*:
    This function is used to parse the value of the field if written in the SDL.
  * *parseValue*:
    This function is used to parse the value of the field if given as a parameter.
  * *serialize*:
    This function is used to transform the value given to the field to a scalar value to be transferred to the client.

## rootTyping
You should only specify the rootTyping property when declaring an object, interface, or scalar type, when specifying other types *@nexus/schema* is smart enough to infer the correct type *graphql-js* will be expecting. Specifying another type for these cases is more likely to trip you up than to give you any benefit.

When specifying the rootTyping I always use *__filename* for the path property. This variable contains the absolute path to the current module. This means that if I ever move the file, I don't have to worry about changing the file import paths manually - I simply have to generate new artifacts. If the declaration of a schema type is not collocated with its root type; I suggest placing the *RootTypingImport* with the type declaration and importing that constant to the schema type declaration in order to maintain this behavior.

# Runtime
Setting up a runtime configuration for running a *@nexus/schema* server is made a lot easier by using *ts-node*, it removes the need for adding *.js* and *.js.map* files to your *.gitignore* and having to filter them out in your editor of choice; or outputting your typescript compilation to a separate *dist* folder, and thus doesn't change the value of the *__filename* variables in the runtime.

# Generating artifacts and making changes
When working on your schema you will from time to time need to verify that the changes you've made to the schema are typed correctly before finalizing all the schema changes you're making to the server. To do this, you need to generate new artifacts for the schema. This can be simplified by adding a check to the `makeSchema` constructor:

```typescript
export const schema = makeSchema({
  ...,
  shouldExitAfterGenerateArtifacts: process.argv.includes('--nexus-exit'),
  // if the --nexus-exit param is given to the process, exit after the schema artifacts have been generated
});
```

And using the following script to generate the type artifacts:
```json
"scripts": {
  "generate": "yarn ts-node ./src/server --nexus-exit",
},
```

This script will run the schema up to the point where the artifacts are generated, and then exit. This is useful behavior when working on the schema since the correctness of the types can only truly be ascertained after the new artifacts are generated. This script would also be useful to run as a step in your CI process, as it allows you to remove the generated artifacts from your git repository improving clarity of pull requests.

Sometimes you will need to make changes to your schema, or how some field should be resolved. This can be a hassle if you've already generated the type artifacts for the schema earlier, and you're running on *ts-node*. To resolve this issue, use the following script:

```json
"scripts": {
  "generate:force": "yarn ts-node --log-error ./src/server --nexus-exit",
},
```

With the *--log-error* flag set, *ts-node* will find just any type errors and still execute the script. This means, you can generate your new type artifacts even when in the middle of making a large set of changes where your server won't compile correctly until all changes are finalized, giving you invaluable type checking for the changes you've already made. The errors reported by the script should generally be ignored though, since they will be based on the old type artifacts.

# Resources
A demo project utilizing all the techniques described can be found [here](https://github.com/IT-Minds-ApS/graphql-nexus-demo).