Enforcing Boundaries with ESLint and Nx

By Cristian Araya | Development | August 28, 2024

One of the most important aspects of any architecture is boundaries. These boundaries separate software elements from one another and restrict those on one side from knowing about those on the other. In development, it's too common to break these boundaries simply because there are usually no mechanisms implemented to prevent a developer from doing so. Fortunately for JavaScript, the "@nx/eslint-plugin" comes to the rescue.

The @nx/enforce-module-boundaries Rule

In case you're wondering, Nx is a build system optimized for monorepos, though it can be used in standalone projects as well. One of its plugins is the @nx/eslint-plugin that allows you to define constraints in the files dependencies. Let’s see an example, the next Vertical Slice Architecture define three vertical layers (Patient, Appointment and Doctor) and two horizontal (Entities and Services):

/images/enforcing boundaries layers.webp

We need to ensure that files in each vertical layer can only depend on other files within the same layer. Additionally, within these layers, the entities cannot depend on any other components. To see this in action, clone this GitHub repository and examine the .eslintrc.js file. Pay particular attention to the @nx/enforce-module-boundaries rule:

'@nx/enforce-module-boundaries': [
      'error',
      {
        allow: [],
        depConstraints: [
          {
            sourceTag: 'type:entities',
            onlyDependOnLibsWithTags: ['type:entities'],
          },
          {
            sourceTag: 'type:service',
            onlyDependOnLibsWithTags: ['type:service', 'type:entities'],
          },
          {
            sourceTag: 'scope:appointment',
            onlyDependOnLibsWithTags: ['scope:appointment'],
          },
          {
            sourceTag: 'scope:doctor',
            onlyDependOnLibsWithTags: ['scope:doctor'],
          },
          {
            sourceTag: 'scope:patient',
            onlyDependOnLibsWithTags: ['scope:patient'],
          },
        ],
      },
    ],

Inside the depConstraints array, we define the layers and their boundaries. Each item has a sourceTag property that defines a layer, and an onlyDependOnLibsWithTags property that defines the boundary or constraint.

However, we need to specify the source tags somewhere. This is where the project.json files come into play. If you examine the folders inside the src directory, you'll find a project.json file in each one with content like this:

{
  "name": "appointment-entities",
  "$schema": "../../../node_modules/nx/schemas/project-schema.json",
  "sourceRoot": "src/appointment/entities",
  "projectType": "library",
  "tags": ["scope:appointment", "type:entities"]
}

Here you can define the name of the layer, the path, and the tags. In this case, the tag "scope:appointment" corresponds to the vertical layer Appointment, and the tag "type:entities" to the horizontal layer Entities. If we examine the entities inside the doctor folder, we'll see the following:

{
  "name": "doctor-entities",
  "$schema": "../../../node_modules/nx/schemas/project-schema.json",
  "sourceRoot": "src/doctor/entities",
  "projectType": "library",
  "tags": ["scope:doctor", "type:entities"]
}

Notice how we also have the "type:entities" tag, but instead of "scope:appointment", we have the tag of the vertical layer for these entities: "scope:doctor".

To see the rule in action, install the project dependencies with the pnpm install command. Next, build the Nx project using pnpm nx build (this will create a .nx folder). Finally, uncomment the import of the Doctor entity in the create-appointment.ts file:

import { Appointment } from '@/appointment/entities/appointment';
// Uncomment the next line to see the linter error
import { Doctor } from '@/doctor/entities/doctor';

export function createAppoinment(appointment: Appointment) {
  return appointment;
}

The linter will display the following error (run pnpm lint if you don't have the ESLint extension installed in your code editor):

A project tagged with "scope:appointment" can only depend on libs tagged with "scope:appointment" eslint@nx/enforce-module-boundaries

This effectively enforces the boundaries between the layers of the project. With just a few configuration steps, we've defined our architecture with clear layers and boundaries. Now, you can be confident that the linter in your workflow will raise an error if any of these constraints are violated.

Related Posts