Skip to main content

Nx Deployments

This document outlines the process for deploying Nx apps.

tip

For additional context on Nx.

Description

Nx is a powerful open-source build system that provides tools and techniques for enhancing developer productivity, optimizing CI performance, and maintaining code quality. It can be used to build and deploy applications, libraries, and tools across different platforms and environments within a monorepo context.

Nx Configuration

In the context of Nx, a "stack" typically refers to a collection of pre-configured tools, libraries, and best practices that work well together for a specific type of project or technology. Nx offers several stacks that developers can use as starting points for their projects typically including: predefined project structure, build tools, linting, testing, and deployment configurations.

How can we deploy affected stacks using Nx

If a monorepo is configured with Nx, there may only be certain areas where changes are made and therefore should be focused on for deployment. In Nx, the affected command can be used to identify the affected stacks that need to be deployed, and the deploy command can then be used to deploy the affected stacks, saving time and resources in the CI/CD pipeline.

An example of the affected command being used can be found in the Event Management project, where as part of the testing script, we only trigger the command for the stack where changes have been identified:

"test": "NX_REJECT_UNKNOWN_LOCAL_CACHE=0 nx affected --base=origin/main --target=coverage --parallel"

If a frontend stack is affected, then the above command will only trigger to run the coverage tests for the frontend, thus saving resources on running covergae reports for other stacks where no changes have been made.

It is also possible to set up configuration settings in a nx.json file, which can be used to set the default values for multiple commands.

For example, the following configuration has the default base set to the main branch when running the affected command, and the cacheableOperations option set to build, test, and coverage, which will cache the results of these operations for faster subsequent runs.

{
"tasksRunnerOptions": {
"default": {
"runner": "nx/tasks-runners/default",
"options": {
"cacheableOperations": ["build", "test", "coverage"]
}
}
},
"affected": {
"defaultBase": "origin/main"
},
"targetDefaults": {
"build": {
"dependsOn": ["^build"],
"outputs": [
"{projectRoot}/.vercel",
"{projectRoot}/dist-lambda",
"{projectRoot}/dist"
]
},
"test": {
"dependsOn": ["build"]
},
"coverage": {
"dependsOn": ["build"],
"outputs": ["{workspaceRoot}/coverage"]
}
},
"pluginsConfig": {
"@nx/js": {
"projectsAffectedByDependencyUpdates": "auto"
}
}
}

Project configuration

Nx uses a centralized configuration system, commonly found in project.json files, which defines the structure, dependencies, and build settings of projects within the monorepo. To apply commands to stacks locally, the run command can be used for example, which will run the specified command in the context of the specified stack. To run the build command for a frontend stack (depending on the naming of projects in the monorepo), the following command can be used:

nx run frontend:build

It is important to note that the commands utilised have to be defined either in the nx.json file for the stack or in the package.json file for the given project.

Each project has specific settings for tasks like building, serving, and testing. For example, a project.json file for an application might look like this (full example can be found here):

{
"root": "packages/frontend",
"targets": {
"deploy": {
"dependsOn": ["^build"],
"executor": "nx:run-commands",
"options": {
"commands": [
...
],
"parallel": false
},
"outputs": [
...
],
"cache": true
},
"deploy-prod": {
"dependsOn": ["^build"],
"executor": "nx:run-commands",
"options": {
"commands": [
...
],
"parallel": false
},
"outputs": [
...
],
"cache": true
}
}
}

As you can see, this configuration allows us to define a number of operations:

  • The build command to be run in parallel, which can speed up the build process.
  • The cache option is set to true, which means that the results of the build command will be cached for faster subsequent runs.
  • The outputs option specifies the files that should be included in the deployment, and the commands option specifies the custom commands that should be run to deploy the application. For example, the deploy command can be used to deploy the application to a staging environment, while the deploy-prod command can be used to deploy the application to a production environment.

This helps to ensure that the deployment process is consistent across multiple environments and that the same commands are run for the same stacks. You can see an example of this within a GitHub Actions workflow here:

- name: Install modules
run: |
npm ci
- name: cache Nx
uses: actions/cache@v4
with:
path: .nx/cache
key: cache-nx-${{ hashFiles('package-lock.json') }}-${{ inputs.environment }}-deploy-frontend-${{ github.sha }}
restore-keys: |
cache-nx-${{ hashFiles('package-lock.json') }}-${{ inputs.environment }}-deploy-frontend-
- name: Deploy vercel project
env:
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
NX_REJECT_UNKNOWN_LOCAL_CACHE: 0
run: |
npx nx run frontend:deploy-prod --args="--deploy_env ${{ inputs.environment }} --branch ${{ github.head_ref || github.ref_name }} --token=${{ secrets.VERCEL_TOKEN }}" --verbose

We can also see in more early stages of Online Payments Services, the use of Nx to deploy the application to a staging environment:

{
"extends": "nx/presets/npm.json",
"$schema": "./node_modules/nx/schemas/nx-schema.json",
"tasksRunnerOptions": {
"default": {
"runner": "nx/tasks-runners/default"
}
},
"namedInputs": {
"noMarkdown": ["!{projectRoot}/**/*.md"],
"noTests": ["!{projectRoot}/**/*.test.ts"],
"noJs": ["!{projectRoot}/**/*.js", "!{projectRoot}/*.js"]
},
"release": {
"changelog": {
"automaticFromRef": true,
"projectChangelogs": {
"createRelease": "github"
}
},
"projects": ["apps/*", "packages/*"],
"projectsRelationship": "independent",
"version": {
"conventionalCommits": true,
"generatorOptions": {
"fallbackCurrentVersionResolver": "disk"
}
}
},
"targetDefaults": {
"build": {
"inputs": ["noMarkdown", "^noMarkdown", "noTests", "^noTests"],
"dependsOn": ["^build"],
"outputs": ["{projectRoot}/dist-lambda"],
"cache": true
},
"lint": {
"inputs": ["{workspaceRoot}/**/*"],
"cache": true
},
"test": {
"inputs": ["noMarkdown", "^noMarkdown"],
"cache": true
},
"dev:deploy": {
"dependsOn": [
{
"projects": ["common-infra"],
"target": "dev:deploy"
}
]
},
"deploy": {
"inputs": ["noMarkdown", "^noMarkdown", { "externalDependencies": [] }],
"cache": true
}
},
"plugins": [
{
"plugin": "@nx/eslint/plugin",
"outputs": ["{options.outputFile}"],
"options": {
"targetName": "lint",
"eslintConfig": "eslint.config.mjs"
}
}
],
"pluginsConfig": {
"@nx/js": {
"projectsAffectedByDependencyUpdates": "auto"
}
}
}

In this example, the configuration extends from the npm preset and uses the default Nx task runner with custom filters to exclude unnecessary files. The build and test processes are configured with caching enabled and output to a output folder which in this case is dist-lambda.

The configuration includes two defined deployment targets:

  • dev:deploy: This target is specifically configured to ensure the common infrastructure (common-infra) is deployed first, demonstrating the ability to establish a dependency chain that guarantees the proper deployment order in development environments.
  • deploy: A general deployment target that excludes markdown files and enables caching for faster subsequent deployments. Unlike dev:deploy, it doesn't enforce the common-infra dependency because production deployments typically happen through CI/CD pipelines where infrastructure changes are managed separately or through different mechanisms.

The setup also includes automated changelog generation and versioning through conventional commits, while maintaining build efficiency through strategic caching.

Advice

Nx is best utilised if your project fits the common structure of a monorepo. The configuration options for Nx can be seen as heavily extensible however this allows for a lot of flexibility in how applications can be deployed. Using the built in commands for caching, parallel execution can reduce the amount of code that needs to be written to achieve the same result but requires consistency and regular maintenance in order to fully leverage the benefits of the tool.

Rationale

It can be quite complex to configure, maintain and integrate various tools and frameworks when it comes to ensuring the scalability and maintainability of a project therefore integrating a monorepo tool like Nx can help to reduce the complexity of the project and make it easier to maintain and scale.

Examples

The Events Management project and Online Payments Services can be used as examples for products that are currently using Nx.

References & Further Reading