Code Generation

We use code generation to automatically generate API schemas and type-safe clients for all API integrations. All generated files are ignored by git to reduce noise, make reviews easier on PRs, and only request code reviews from relevant teams.

You will need java installed on your machine to be able to run the yarn codegen command, more precisely the openapi-generator sub-command. Find more about the installation here.

Understanding how code generation works

We are only tracking file that are coming from an external source, e.g. contentfulTypes.d.ts which depends on Contentful to be generated. The same goes for openapi.yaml files from external services.

Running yarn codegen will generate all the schemas and clients for the project. It can take up to 10 minutes to run. The output of each step is cached by NX and subsequent runs will usually be much faster.

In our GitHub workflows, we are caching theses generated files to avoid to re-generate them at each push. However, the generated files have to be updated when some specific files are changed (e.g. *.resolvers.ts, *.dto.ts, etc). There is a hashFiles variable in our GitHub workflows which contain a list of file patterns which may affect code generation† and are used to invalidate the cache. You should follow this naming convention for files which need to invalidate the code generation cache.

scripts/codegen.js
libs/cms/src/lib/generated/contentfulTypes.d.ts
apps/air-discount-scheme/web/i18n/withLocale.tsx
apps/air-discount-scheme/web/components/AppLayout/AppLayout.tsx
apps/air-discount-scheme/web/components/Header/Header.tsx
apps/air-discount-scheme/web/screens/**.tsx
libs/application/types/src/lib/ApplicationTypes.ts
apps/**/codegen.yml
libs/**/codegen.yml
apps/**/*.model.ts
libs/**/*.model.ts
apps/**/*.enum.ts
libs/**/*.enum.ts
apps/**/queries/**/*.tsx?
libs/**/queries/**/*.tsx?
apps/**/*.resolver.ts
libs/**/*.resolver.ts
apps/**/*.service.ts
libs/**/*.service.ts
apps/**/*.dto.ts
libs/**/*.dto.ts
apps/**/*.input.ts
libs/**/*.input.ts
apps/**/*.module.ts
libs/**/*.module.ts
apps/**/*.controller.ts
libs/**/*.controller.ts

Code generation build targets

We have 3 different targets that can be configured inside project.json to generate schemas and types.

  • codegen/backend-client

  • codegen/backend-schema

  • codegen/frontend-client

These are designed with smart defaults and run in the correct order thanks to the NX dependency graph.

Generating schema and client types

If you are changing some API service, you may need to re-generate the API schema using:

yarn nx run <service-project>:codegen/backend-schema

With an updated API schema, you may need to re-generate client code with:

yarn nx run <client-project>:codegen/backend-client

If you have changed a public API schema, you may need to re-generate front-end client code with:

yarn nx run <frontend-project>:codegen/frontend-client

Configuring code generation

The following guides cover how to set up code generation in your project.

Generate OpenAPI schemas for your API project

First, you need to create an openApi.ts file to define the document builder. Add this file to the src directory of your project alongside the index.ts.

import { DocumentBuilder } from '@nestjs/swagger'

export const openApi = new DocumentBuilder()
  .setTitle('title')
  .setDescription('description')
  .setVersion('version')
  .addTag('application')
  // More Swagger configuration, i.e. `.addOAuth2(...) to add 
  // authorization to test the API in the Swagger UI
  .build()

Next, we need to create an buildOpenApi.ts that will consume the previous file and generate the openapi.yaml file.

import { buildOpenApi } from '@island.is/infra-nest-server'

import { AppModule } from './app/app.module'
import { openApi } from './openApi'

buildOpenApi({
  path: 'PATH/src/openapi.yaml',
  appModule: AppModule,
  openApi,
})

Finally, we set up a codegen/backend-schema script in project.json for the project.

    "codegen/backend-schema": {
      "executor": "@nx/workspace:run-commands",
      "options": {
        "command": "yarn ts-node -P PATH/tsconfig.app.json PATH/src/buildOpenApi.ts"
      },
      "outputs": ["{projectRoot}/src/openapi.yaml"]
    }

If your service is running a service like redis, you will need to ignore it for running the build-open-api script like follow in the project.json

  "command": "cross-env INIT_SCHEMA=true yarn ts-node ..."

and in the module where the redis manager is defined

if (process.env.INIT_SCHEMA === 'true') {
  CacheModule = NestCacheModule.register()
} else {
  CacheModule = NestCacheModule.register({
    store: redisStore,
    redisInstance: createNestJSCache({...}),
  })
}

Generate GraphQL schema for your API project

This is similar to generating OpenAPI schemas. Configure your app module to generate a schema file at startup like this:

const debug = process.env.NODE_ENV === 'development'
const playground = debug || process.env.GQL_PLAYGROUND_ENABLED === 'true'
const autoSchemaFile = environment.production
  ? true
  : 'APP-PATH/src/api.graphql'

@Module({
  imports: [
    GraphQLModule.forRoot({
      debug,
      playground,
      autoSchemaFile,
      // ...
    }),
    // ...
  ],
})
export class AppModule {}

For the codegen/backend-schema build target, you can use the build-graphql-schema.ts utility to load your AppModule and generate the GraphQL schema:

    "codegen/backend-schema": {
      "executor": "nx:run-commands",
      "options": {
        "command": "yarn ts-node -P PATH/tsconfig.json scripts/build-graphql-schema.ts PATH/src/app/app.module"
      },
      "outputs": ["{projectRoot}/src/api.graphql"]
    },

Generate an OpenAPI client for a monorepo API

You can use OpenAPI Generator and the OpenAPI schema generated above to generate a type-safe client library to integrate with it. Just create a client project and add the following target to the project.json.

    "codegen/backend-client": {
      "executor": "@nx/workspace:run-commands",
      "options": {
        "command": "yarn openapi-generator -o PATH/gen/fetch -i OTHER-PROJECT/src/openapi.yaml"
      },
      "outputs": ["{projectRoot}/gen/fetch"],
    }

You'll also need to add an implicit dependency from the client project to the API project (also in project.json. This is to make sure NX first runs the schema target of the API project before running the client target of the client project.

  "implicitDependencies": ["OTHER-PROJECT-NAME"]

Generate an OpenAPI client for an external REST API

Creating client libraries for external APIs is similar. You should ask the API provider for an OpenAPI schema and add it to our monorepo in your client project. Name it something like clientConfig.json (don't name it openapi.yaml which is ignored by git). Then add a codegen/backend-client build target project.json:

    "codegen/backend-client": {
      "executor": "@nx/workspace:run-commands",
      "options": {
        "command": "yarn openapi-generator -o PATH/gen/fetch -i OTHER-PROJECT/src/openapi.yaml"
      },
      "outputs": ["{projectRoot}/gen/fetch"],
    }

In many cases you can download the OpenAPI schema directly from running servers. In that case it is handy to define a update-openapi-document build target in the client project. Something like this:

    "update-openapi-document": {
      "executor": "nx:run-commands",
      "options": {
        "command": "curl API-URL/swagger.json > PROJECT-PATH/src/clientConfig.json",
      }
    },

In case the API is on X-Road / Straumurinn and you're running xroad proxy, you can fetch OpenAPI schemas for REST APIs with a build target like this:

    "update-openapi-document": {
      "executor": "nx:run-commands",
      "options": {
        "curl -H \"X-Road-Client: IS-DEV/GOV/10000/island-is-client\" http://localhost:8081/r1/XROAD-SERVICE-PATH/getOpenAPI?serviceCode=SERVICE-CODE > PROJECT-PATH/src/clientConfig.json",
      }
    },

Generate GraphQL types and hooks

You can use GraphQL Code Generator to generate all kinds of TypeScript types and even React hooks to integrate with GraphQL APIs.

Create a codegen.yml file in your project:

schema:
  - PATH/api.graphql
generates:
  PATH/src/schema.ts:
    plugins:
      - ...
hooks:
  afterAllFileWrite:
    - prettier --write

Finally, you can configure a codegen/frontend-client inside your project.json

"codegen/frontend-client": {
  "executor": "@nx/workspace:run-commands",
  "options": {
    "command": "graphql-codegen --config PATH/codegen.yml"
  },
  "outputs": ["PATH/src/schema.ts"]
}

You should use one of the following names for generated files so they're ignored by git:

  • schema.ts

  • possibleTypes.json

  • fragmentTypes.json

  • *.generated.ts

Instead of generating one big TypeScript file in each project, you might want to use the near-operation-file preset. It creates small TypeScript files with hooks and operation types for operations defined in graphql files. These can be next to where they're used.

schema:
  - PATH/api.graphql
documents:
  - UI-PATH/src/**/*.graphql
generates:
  UI-PATH/src/:
    preset: 'near-operation-file'
    presetConfig:
      baseTypesPath: '~@island.is/api/schema'
    plugins:
      - typescript-operations
      - typescript-react-apollo
hooks:
  afterAllFileWrite:
    - prettier --write

Make sure to configure the codegen/frontend-client outputs to match:

"codegen/frontend-client": {
  "executor": "@nx/workspace:run-commands",
  "options": {
    "command": "graphql-codegen --config PATH/codegen.yml"
  },
  "outputs": ["PATH/src/**/*.generated.ts"]
}

You can also use GraphQL Code Generator to integrate a GraphQL API inside a backend client project. You can explore which plugins and presets are available. In the least you can use it to validate your operations against the schema, and generate TypeScript types which you then use manually, eg with Enhanced Fetch.

The main thing is to configure the codegen build target as codegen/backend-client instead of codegen/frontend-client.

Last updated