nitrogql logonitrogql

nitrogql 1.0 release

Published at September 3, 2023

Today, we are happy to announce the first stable release of nitrogql!

nitrogql is a toolchain for using GraphQL in TypeScript projects. In this post, we will go over the main features of nitrogql and how to get started with it.

What is nitrogql?

Currently, nitrogql has two main features: code generation and static checking.

Code generation

Code generation is the process of generating TypeScript code from GraphQL code (both schema and operations). This is useful because it allows you to use GraphQL operations in your TypeScript code with type safety and without having to write any boilerplate code.

This is known as the schema-first approach to GraphQL. In this approach, you write your GraphQL schema first, and then you generate code from it. This is the opposite of the code-first approach, where you write your code first, and then you generate a schema from it.

For example, if you have a GraphQL operation like this:

# getPosts.graphql
query getPosts {
  posts {
    id
    title
  }
}

Then you can use it in your TypeScript code like this (after running nitrogql generate):

import { useQuery } from "@apollo/client";
import getPostsQuery from "./getPosts.graphql";

export const Posts: React.FC = () => {
  const { data } = useQuery(getPostsQuery);
  return (
    <ul>
      {data.posts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
};

Notably, nitrogql allows you to import GraphQL operations from .graphql files. Thanks to TypedDocumentNode, the imported operation knows the shape of the data it returns. This is why the type of data is also derived from the GraphQL operation, namely as { posts: { id: string; title: string; }[]; }.

For maximum safety, nitrogql provides a webpack loader and a Rollup plugin that let you import .graphql files directly from your TypeScript code as shown above. This way, nitrogql can be in charge of both runtime and type-level behavior of your .graphql files so that nitrogql can ensure that they are always in sync.

Static checking

While code generation provides TypeScript-level safety, it is still possible to make mistakes in your GraphQL code. For example, you might try to query a field that does not exist in the schema, or you might forget to pass a required argument to a field. These in-schema errors and schema-operation-mismatch errors are not covered by above code generation because it is all about using known-good GraphQL code in TypeScript.

This is where static checking comes in. Static checking is the process of checking your GraphQL code for errors before you generate any TypeScript code from it. This GraphQL-level safety guards you against seeing runtime errors in your application that comes from mistakes in your GraphQL code.

The following is an example of a GraphQL-level error that nitrogql can catch:

# getPosts.graphql
query getPosts {
  posts { # forgot to pass an argument
    id
    tite # typo here
  }
}

When you run nitrogql check, nitrogql will report the following error:

query getPosts {
  posts {
  ^
  Required argument 'filter' is not specified

    # forgot to pass an argument
    id
    tite # typo here
    ^
    Field 'tite' is not found on type 'Post'
  }
}

Why nitrogql?

You might already be familiar with other tools that provide similar features to nitrogql. Particularly, GraphQL Code Generator is a popular tool that provides code generation for GraphQL.

Then, why did we create nitrogql instead of using GraphQL Code Generator? There are a few reasons:

Let's go over each of these reasons in detail.

Source maps

Source maps are a very helpful feature when one is using code generation. They allow you to see the original GraphQL code that generated a particular TypeScript code. This is very useful when you use a Go to Definition feature of your editor. For example, if you use the Go to Definition feature on getPostsQuery in the following code:

import { useQuery } from "@apollo/client";
import getPostsQuery from "./getPosts.graphql";

export const Posts: React.FC = () => {
  const { data } = useQuery(getPostsQuery);
  return (
    <ul>
      {data.posts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
};

Then, you will be taken to the getPosts.graphql file instead of the generated getPostsQuery.d.graphql.ts file. This is because nitrogql generates source maps that point to the original GraphQL code.

GraphQL Code Generator does not support source maps. Actually, I tried to add source map support to GraphQL Code Generator before creating nitrogql, but I found that the architecture of GraphQL Code Generator is making it very difficult to add source map support. You know, source map support must be built into the core of a code generation tool. Adding it later is seriously hard. So, I decided to create a new tool that has source map support from the beginning.

near-operation-file preset

The near-operation-file preset was the recommended way to use GraphQL Code Generator. This preset generates one TypeScript file per GraphQL operation file. It is a very nice feature that allows you to keep your GraphQL code and TypeScript code separate from each other.

However, while it is still fully supported, the near-operation-file preset is no longer recommended as GraphQL Code Generator's default. Instead, the client-preset is now the default.

I don't really like the new default because it requires you to put GraphQL code and TypeScript code in the same file. Especially, it examines GraphQL code in your TypeScript code and generates types from it. Then, the generated code affects that TypeScript code. I don't quite like this circular process.

nitrogql is the opposite of this. It requires you to put GraphQL code in .graphql files, completely separated from TypeScript code. This is similar to what the near-operation-file preset does. nitrogql will keep going with this approach. If you like the near-operation-file preset, you will like nitrogql too.

Maximum safety as a default

GraphQL Code Generator is a very flexible tool. It allows you to configure many things. What is unfortunate is that some of the options are footguns that decrease type safety.

For example, the types for custom scalars are any by default. Unless you set strictScalars to true, GraphQL Code Generator does't even warn you about this.

Also, if you use GraphQL Code Generator for generating resolver type definitions, it is very easy to forget to define one resolver and another, and then you will see a runtime error. I wrote in details about this in another article (in Japanese).

nitrogql is designed to be as safe as possible by default. For example, the types for custom scalars must be explicitly defined. Failing to do so will result in a compile error. There is no way to disable this behavior.

While nitrogql still has some configuration options, they are for different use cases. Loosening type safety is not one of them.

Getting started

While we have a Getting Started page in this site, let's go over the basics here too.

nitrogql is a CLI tool. You can install it with:

npm install --save-dev @nitrogql/cli @graphql-typed-document-node/core

Note: @graphql-typed-document-node/core is required because it is depended by generated code.

Then you need to create a configuration file named graphql.config.yaml in the root of your project. The following is an example of a configuration file:

schema: ./schema/*.graphql
documents: ./src/**/*.graphql
extensions:
  nitrogql:
    generate:
      schemaOutput: ./src/generated/schema.d.ts

This configuration file tells nitrogql to look for GraphQL schema files in ./schema and GraphQL operation files in ./src. Note that they have different syntaxes and cannot be mixed.

Then, you can run nitrogql generate to generate TypeScript code from your GraphQL code. This command will generate .d.graphql.ts files next to your .graphql files. With the help of these generated files, you can import .graphql files from your TypeScript code in a type-safe manner.

You can also run nitrogql check to check your GraphQL code for errors.

For more information, please refer to the Configuration Options page.

Migrating from GraphQL Code Generator

If you are already using GraphQL Code Generator, you might want to migrate to nitrogql. Migration will be a tough process because they are not fully compatible. However, we have a detailed migration guide for you.

Check-only usage

If you are interested in using nitrogql but can't fully switch from GraphQL Code Generator yet, you can use nitrogql only as a linter. Just run nitrogql check in CI to check your GraphQL code for errors. This way, you can get the benefits of static checking without having to migrate to nitrogql completely.

Configuration for check-only usage is very simple. You just need to specify the schema and documents option in your configuration file.

schema: ./schema/*.graphql
documents: ./src/**/*.graphql

Then run nitrogql check in CI. If there are any errors in your GraphQL code, it will exit with a non-zero exit code. That's it!

Please note, however, that nitrogql's static checking follows the GraphQL specification strictly. This means that it will report errors for code that may actually work with GraphQL Code Generator. For example, if you define a fragment, nitrogql does not allow you to refer to it from other files. Fragments are local to the file where they are defined. This means that nitrogql is (currently) not compatible with some fragment collocation techniques used in the community. We are still investigating how to support them.

Development of nitrogql

Although it is a side story, I would like to talk about the development of nitrogql. I created several things in order to make nitrogql work.

Fundamentals of any language processing system include ASTs and parsers. I created both of them from scratch for use in nitrogql. This enabled me to implement the source map support.

nitrogql is written in Rust. I chose Rust because I knew that it is good for writing language processors. Then I had to somehow distribute nitrogql as an npm package. A popular way to do this is to prepare pre-built binaries for each platform. However, I didn't want to maintain multiple build systems for each platform. So, I decided to compile nitrogql to WebAssembly and let it run in Node.js. Fortunately, Node.js already had an experimental support for WASI, which is a set of APIs that enables a WASM module to access the file system and other OS features.

Well, I thought I was fortunate. But it turned out that the WASI support in Node.js was broken. It was obvious that no one had ever considered using Node.js WASI implementation seriously. It is okay as an experimental feature, but the issues I reported didn't look like they would be fixed anytime soon. Therefore I ended up creating a userland WASI implementation just for use in nitrogql. Yes, I created such a lot of things for nitrogql.

What's next?

nitrogql is still in its early days. Currently it has only two features mentioned above that are considered stable from today. We will keep improving them, but we also have many ideas for the future. Here are some of them.

Generating resolver types. Currently, nitrogql only generates types for GraphQL operations. This means that nitrogql helps you with the client-side of GraphQL, but not the server-side. We will work on generating types for resolvers too, so that you can get the benefits of type safety on both the client-side and the server-side.

Plugin system. Currently, nitrogql has a small set of configuration options. Some of the feature we want to add are kind of opinionated, so we don't want to add them as configuration options. Instead, we want to add a plugin system so that various use cases can be supported by nitrogql.

Conclusion

nitrogql is a toolchain for using GraphQL from TypeScript projects. It provides code generation and static checking. If you liked GraphQL Code Generator's near-operation-file preset, nitrogql is a nice alternative for you with source map support and more safety.

Visit GitHub for more information.

nitrogql is developed by uhyo. Contribution is more than welcome!