Building a type-safe Fullstack Application with GraphQL codegen

What is GraphQL codegen?

There are two approaches to defining your schema in GraphQL: schema-first and code-first.

In schema-first, you write .graphql (or .gql) files while in code-first you write resolver-like types in Javascript (or Typescript).

Now, if you are using Typescript, you might find yourself having to write types again for other purposes - your resolvers for example. That can quickly become a problem, not only because it feels like a waste of time, but also because it makes it hard to maintain. If your codebase and your schema grow in complexity, and you have a whole team working on it, a small type definition update can cause a huge mess!

If we look at a fullstack Typescript application, we have to duplicate our type definitions at least 3 times:

  1. in the schema file
  2. for the backend (resolvers)
  3. for the frontend (GraphQL client)
  4. BONUS: for the ORM
Enters GraphQL code generator

GraphQL code generator is the solution to that exact problem: you write your schema file and the rest gets generated automatically 💨

Now let’s see how it actually works 👇

Example: Building a blog

Let’s take a classic example: building a blog... with Express, GraphQL, React and React Query! (lol, who’s using simple HTML + Markdown these days anyway 🤷‍♂️)

Another over-engineered blog
Graph Relations

As mentioned above, with this stack we would normally have to:

  1. write our type definitions in a schema file,
  2. write types for our backend resolvers,
  3. write model definitions for our ORM (using Prisma for example),
  4. write types for React Query on the frontend.

Phew, that is a lot of work!

Now imagine if, 4 months later, we want to add tags to our posts. We would have to go through the same 4 steps to update the types!

But with GraphQL codegen, we have one single source of truth the schema file!

Alright, I hope I teased you enough by now so let’s jump into the code.

Backend with Express and Express GraphQL

If you start from scratch, you can simply install Express, Express GraphQL and Typescript:

$ npm i express express-graphql @graphql-tools/schema cors import-graphql-node
$ npm i -D @types/express

Then we can very easily setup the server:

import 'import-graphql-node'
import express from 'express'
import {GraphQLHTTP} from 'express-graphql'
import cors from 'cors'
import {makeExecutableSchema} from '@graphql-tools/schema'
import * as typeDefs from './schema.graphql'

const app = express()
app.use(cors())

const schema = makeExecutableSchema({
	typeDefs
})

app.use('/', GraphQLHTTP({
	context: {db},
	schema: schema,
	graphiql: true
}))
server/src/index.ts

Note here that I’m using import-graphql-node to import .graphql files.

Checkout the repo for more details.

Frontend with React and React Query

We can bootstrap a React project with Typescript very easily thanks to the Create-React-App template:

$ npx create-react-app client --template typescript

Next, let’s add React Query:

$ npm i react-query

and set it up:

import "./style.css";
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import { QueryClient, QueryClientProvider } from "react-query";

const client = new QueryClient();

ReactDOM.render(
    <React.StrictMode>
        <QueryClientProvider client={client}>
            <App />
        </QueryClientProvider>
    </React.StrictMode>,
    document.getElementById("root")
);
client/src/index.tsx

Setting up GraphQL codegen

Setting up GraphQL codegen is super easy! First, install the CLI:

$ npm i -D @graphql-codegen/cli

Then launch the initialisation process:

$ npx graphql-codegen init

This will prompt a series of questions to set it up for your needs. It’s not super important as it’s very easy to update the configuration later.

graphql-codegen installation process

Here is (approximately) the config file that you’ll end up with:

Default codegen.yml config file

Let’s go over each field to explain what it does and configure them exactly how we need them.

Schema

This should point to your schema definition. By default, it uses your GraphQL endpoint, but in general, it’s easier to put the path to your actual schema file.

schema: "server/schema.graphql"
codegen.yml

Documents

This is part of the frontend configuration. Documents should point to some schema definition of your operations (queries and mutations). Here’s an example:

query AllPosts {
    allPosts {
        id
        author {
            displayName
            picture
        }
        title
        publishedAt
        content
        comments {
            id
            text
            username
        }
    }
}
client/src/operations.graphql
documents: "client/**/*.graphql"
codegen.yml

The React Query Plugin

The installation process does not have an option for React Query, but we can easily integrate it thanks to the huge plugin hub!

First, we need to install the right plugin:

$ npm i -D @graphql-codegen/typescript-react-query

Then we configure it in the `codegen.yml` configuration file by adding it to the plugins of the frontend section:

generates:
    client/src/generated.tsx:
        documents: "client/**/*.graphql" # where the queries are written
        plugins:
            - "typescript"
            - "typescript-operations"
+           - "typescript-react-query"
codegen.yml

What’s amazing about this plugin is that it’s also going to take care of configuring the React Query client (endpoint, fetcher, etc) so that we can just use simple hooks, e.g., useGetAllPostsQuery().

In order to make this work, we need to provide some configuration such as the GraphQL endpoint, but we can also add other things, e.g, an authorization header (with environment variables, how cool is that!):

plugins:
    - "typescript"
    - "typescript-operations"
    - "typescript-react-query"
config:
    fetcher:
        endpoint: "process.env.API_URI"
        fetchParams:
            headers:
                Content-Type: "application/json"
                Authorization: "Bearer process.env.HEADER_AUTH_TOKEN"
codegen.yml

Putting everything together

We are now ready to go!

To generate the types, we can simply run the command:

$ npm run codegen

Use the generated types in the backend resolvers:

import type {QueryAuthorArgs} from "/generated"
import type {Context} from "./context"

const resolvers = {
	Query: {
		author: (
			_parent: null,
			{ id }: QueryAuthorArgs,
			context: Context) => {
			// Do what you got to do to get the author...
		}
	}
	Mutation: {
		createPost: (
      _parent: null,
      { input }: MutationCreatePostArgs,
      ctx: Context
    ) => {
      // Save the post in the database!
    },
	}
}
server/src/resolvers.ts

And use the generated hooks in the frontend like so:

import { useAllPostsQuery } from "./generated";

function App() {
    const { status, error, data } = useAllPostsQuery();
		...
client/src/App.tsx

Conclusion

If you decide to go down the code-first route (blue pill) good for you, but many teams decide to pick a schema-first approach to build their GraphQL API, and even though it’s a great option it can quickly become a burden to test and maintain your code.

But fortunately, graphql-codegen is an elegant solution to remediate to that problem of code duplication, making the schema file your single source of truth!

GraphQL Security

As we’ve explained in a [previous post, GraphQL has no security in place by default](https://blog.escape.tech/9-graphql-security-best-practices/). We found that most GraphQL APIs are hence subject to the most basic attacks (brute force, DoS, etc).

To remedy this lack of security in the GraphQL ecosystem, we built a quick scan that will get you started on your journey to shipping more secure applications.

You can scan your endpoints against a dozen of GraphQL security vulnerabilities for free (no login required) at graphql.security.

Achraf Ait Sidi Hammou

Achraf Ait Sidi Hammou

Fullstack Dev – Writing about GraphQL
Paris