While recently working on a GraphQL project I had a task where the schema (part of the Infrastructure-as-Code) had to be exported to the frontend and the backend in TypeScript. I quickly realised that since my schema evolves as I’m introducing features, I’m loosing time for a) manual actions and b) comparing types’ version between the schema, the front and the back which could introduce errors. So I came up with a quick and easy solution.

First of all, let’s generate the types from the schema. For that we will use the graphql-codegen tool, install it in the repo with the GraphQL schema first:

npm -i @graphql-codegen/typescript @graphql-codegen/cli --save-dev

And configure it with a codegen.ts in the root of the same repo:

import type { CodegenConfig } from '@graphql-codegen/cli';

const config: CodegenConfig = {
  overwrite: true,
  schema: "path/to/your/schema.graphql",
  generates: {
    "graphql-types.ts": {
      plugins: ["typescript"]
    },
  }
};

export default config;

Give it a try to verify that it runs well: graphql-codegen --config codegen.ts. After that let’s create a generate-types.ts bash script that creates a local npm package:

#!/bin/bash

# Usage: ./generate-types.sh <version>
# Example: ./generate-types.sh 1.2.3

# Check if a version argument is provided
if [ -z "$1" ]; then
  echo "Error: Version not specified."
  echo "Usage: ./generate-types.sh <version>"
  exit 1
fi

VERSION=$1

# Configuration variables
PACKAGE_NAME="@my-project/graphql-types"
FILENAME="graphql-types.ts"

# Generate graphql-types.ts
echo "Generating $FILENAME..."
graphql-codegen --config codegen.ts

# Prepare a temporary directory for publishing
echo "Preparing package $PACKAGE_NAME for version $VERSION..."
mkdir -p ./dist
mv ./$FILENAME ./dist/index.d.ts

# Create a minimal package.json in the dist folder with the specified version
echo "{
  \"name\": \"$PACKAGE_NAME\",
  \"version\": \"$VERSION\",
  \"types\": \"index.d.ts\"
}" > ./dist/package.json

# Create the package
echo "Packaging $PACKAGE_NAME version $VERSION..."
npm pack ./dist

# Clean up
echo "Cleaning up..."
rm -rf ./dist

echo "$PACKAGE_NAME@$VERSION generated successfully!"

I added this script to my infrastructure repository. One could say that it would be better to have a separate package.json which is true, but I prefer to keep all the stuff related to this in one place. Calling the script with the version will generate a package of that version. I prefer to git commit that package with the version number.

Let’s import it to our frontend or backend repo. I obviously wrote a script for that too:

#!/bin/bash

# Usage: ./import-types.sh <version>
# Example: ./import-types.sh 1.2.3

# Define the paths to the repositories
EXPORTER_DIR="./../my-project"
IMPORTER_DIR="."

# Check if a version argument is provided
if [ -z "$1" ]; then
  echo "Error: Version not specified."
  echo "Usage: ./import-package.sh <version>"
  exit 1
fi

# Set the package name with the provided version
PACKAGE_NAME="my-project-graphql-types"
PACKAGE_VERSION="$1"
PACKAGE_FILE="$PACKAGE_NAME-$PACKAGE_VERSION.tgz"

# Check if the exporter directory exists
if [ ! -d "$EXPORTER_DIR" ]; then
    echo "Error: Exporter folder $EXPORTER_DIR not found."    exit 1
fi

# Check if the package file exists
if [ ! -f "$EXPORTER_DIR/$PACKAGE_FILE" ]; then
    echo "Error: Package $PACKAGE_FILE not found in $EXPORTER_DIR."
    exit 1
fi

# Navigate to the importer directory
echo "Navigating to $IMPORTER_DIR..."
cd "$IMPORTER_DIR" || { echo "Error: Could not navigate to $IMPORTER_DIR."; exit 1; }

echo "Cleaning existing versions of $PACKAGE_NAME..."
rm -f "${PACKAGE_NAME}-"*.tgz

# Copy package to importer directory
echo "Copying $PACKAGE_FILE to $IMPORTER_DIR..."
cp "$EXPORTER_DIR/$PACKAGE_FILE" . || { echo "Error: Could not copy $PACKAGE_FILE to $IMPORTER_DIR."; exit 1; }

# Install the package from the exporter repository
echo "Installing package $PACKAGE_FILE..."
npm install "$PACKAGE_FILE"

# Check if the installation was successful
if [ $? -eq 0 ]; then
    echo "Package $PACKAGE_FILE installed successfully in $IMPORTER_DIR!"
else
  echo "Error: Failed to install package $PACKAGE_FILE."
  exit 1
fi

We shall commit this script in the frontend and backend repository and run it in the same way as the previous one - with the version in the argument. Now your IDE shall import the types:

import { MyType } from '@my-project/graphql-types';

function somewhereInFrontend(obj: MyType) {}

Obviously this process can be improved by selecting the latest version in the repository generating the schema and adding proper npm publishing (This way there is no need for the second script). But this has already improved my workflow:

  • one command in the infrastructure repository
  • one command in the frontend and backend repository
  • automated types generation
  • version tracking

Hope this helps and improves your workflows too.