One of the more annoying parts of code generators like Prisma, is that you need to run them - obviously. My silly little meat brain is just not capable of remembering what to run in which order before I see the glaring compiler errors in my terminal. But we are developers, so let’s automate it! In this blog post I’ll describe how to use Nx’s dependency system to auto-run any code generation job before building or serving a dev server.

Intro to Nx

For those unaware, Nx is a monorepo manager. A monorepo is a (Git) repository, that contains more than one application side by side. The biggest benefit of this is not having the overhead that sharing dependencies between apps usually incurs. To help with managing those larger repos, tools like Turborepo, Lerna, or - you guessed it - Nx exist.

However, Nx in particular has sophisticated tooling for detecting dependencies between apps, such that it is able to create a dependency tree. This dependency tree is then used to mark only projects as “dirty” that have either changed themselves, or have a dirty dependency. That way, only marked projects need to be rebuilt, retested, or redeployed.

I’m sure other tools like the ones mentioned above have similar capabilities. But Nx is what I’ve been using for a couple of years now, so it’s what I’m familiar with.

We’ll use this mechanism to let Nx do the brain work for us and only re-run prisma generate, when our schema changes.

The Setup

All the code I’m presenting here is also available at https://github.com/Simon-Hayden-Dev/nx-for-code-gen

We start with a very simple setup:

  • One app, I’ll call this simply api.
  • One library, which is called db. I like to keep the DB code properly scoped to it’s own library.

As you might have guessed, the api project has a dependency on the db library. The db library, in turn uses Prisma as ORM.

The project graph we get from this looks something like this:

A graph diagram showing an app called “api” being dependant on a library called “db”.

Adding a Nx Target for Prisma

First off, we need to write a schema.prisma. Because we are in a monorepo, I don’t want to have this file in the repo root. Instead, I’ll put this file into the libs/db/prisma folder.

To get started, I’ll use a very basic schema file:

generator client {
  provider = "prisma-client-js"
  // Using custom output folder, so that we can potentially have multiple DB
  // libraries in parallel
  output   = "../../../node_modules/@nx-for-code-gen/prisma/api-db"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model User {
  id    Int    @id @default(autoincrement())
  name  String
  email String
}

To test if the schema is working, you can cd into the libs/db folder and run prisma generate manually:

cd libs/db
pnpx prisma generate
✔ Generated Prisma Client (v6.4.1) to ./../../node_modules/@nx-for-code-gen/prisma/api-db in 14ms

Buf of course we don’t want to do this every time the schema changes, so let’s write an Nx target for it:

// libs/db/project.json
{
  // ...
  "targets": {
    "generate-client": {
      "command": "prisma generate",
      "dependsOn": [],
      // Don't re-run this command, when the schema.prisma file didn't change
      "inputs": ["{projectRoot}/prisma/schema.prisma"],
      "outputs": [
        // Tell Nx what folder contains the output of the command (for caching)
        "{workspaceRoot}/node_modules/@nx-for-code-gen/prisma/api-db"
      ],
      "cache": true,
      "options": {
        "cwd": "libs/db"
      }
    }
  }
}

Now you can simply run nx generate-client db to do the same as before, but without needing to cd into the correct folder:

nx generate-client db
> nx run db:generate-client

> prisma generate

Prisma schema loaded from prisma/schema.prisma

✔ Generated Prisma Client (v6.4.1) to ./../../node_modules/@nx-for-code-gen/prisma/api-db in 12ms

Start by importing your Prisma Client (See: https://pris.ly/d/importing-client)

Tip: Want to turn off tips and other hints? https://pris.ly/tip-4-nohints


——————————————————————————————————————————————————————————————————————————————————————————————————————————

 NX   Successfully ran target generate-client for project db (525ms)

Which is quite a bit slower than running prisma directly. However, the second run will be cached, which makes execution times a non-issue:

nx generate-client db
[...same as before...]

 NX   Successfully ran target generate-client for project db (25ms)

Nx read the output from the cache instead of running the command for 1 out of 1 tasks.

More important than raw performance, however, is executing this target on demand, when needed.

Adding the Nx Target as Dependency

One solution would be to just go into the apps/api/project.json, update the build and test target’s dependsOn config to include the db:generate-client. However, you’d need to do this for every app or library, that imports the db library.

Instead, what you can and should do is update the nx.json. It allows you to define default target configs, including the dependsOn.

// nx.json
{
  // ....
  "targetDefaults": {
    "build": {
      // The "^" basically means "run generate-client on all dependencies
      // of the current project".
      "dependsOn": ["^generate-client"]
    },
    "test": {
      "dependsOn": ["^generate-client"]
    }
  }
}

Going back to the Nx dependency graph - for tasks this time - you will see something like this:

A dependency graph showing the task “api:build” to depend on “db:generate-client”.

In other words, as soon as you run nx build api, Nx will run db:generate-client before.

However, you might think that generating the prisma client on every target might be wasteful. The frontend code, for example is not depending on anything related to the DB. That’s why I declared the dependency via ^generate-client instead of db:generate-client. The ^ tells Nx to run a specific target on all dependencies of the current project. Since the frontend code does not depend on the db lib, it therefore won’t execute the db:generate-client target. We can verify this in the Nx target graph:

A dependency graph showing, that the “build:client” target does not depend on the “db:generate-client” target.

Note About Executor Specific Configs

As I found out the hard way, the targetDefaults may also contain executor specific configs, like so:

{
  "targetDefaults": {
    "@nx/webpack:webpack": {
      "cache": true,
      "dependsOn": ["^build"],
      "inputs": ["production", "^production"]
    }
  }
}

If you have a project with a build target, that uses this executor, this config takes precedence over thetargetDefaults config of build. Meaning, you sadly have to add ^generate-client to every executor-specific targetDefaults config as well:

{
  "targetDefaults": {
    "@nx/webpack:webpack": {
      "cache": true,
      "dependsOn": ["^build", "^generate-client"],
      "inputs": ["production", "^production"]
    },
    "@nx/js:tsc": {
      "cache": true,
      "dependsOn": ["^build", "^generate-client"],
      "inputs": ["production", "^production"]
    },
    // etc.
  }
}