Typescript app development using Nx

In this article I will talk about using Nx to develop web applications in Typescript.

Two years ago I wrote an article on a medium – Building a Typescript App with Webpackwhere he shared his solution for building a simple Typescript application using webpack.

And everything would be fine if it weren’t outdated. Everything described is still working, but there are already more advanced solutions, one of which will be discussed further.

Problems of own solutions

More than three years ago, I got a job in one small fin. tech startup Angular developer. And as different tasks were performed, there was a need for a solution that would satisfy the following requirements:

  • Typescript development language;

  • Livereload when making changes;

  • There is a “prod” build that optimizes the source code.

In a word, everything that is in modern JS frameworks.

I looked at several projects on github, but none of the solutions worked for me, because they were either not supported anymore or they were too complicated. I needed something simple.

I was familiar with webpack and it was not difficult for me to sketch out a few modules and assemble a build that met the requirements described above.

The solution worked great, with one exception that it was hard to update. Due to the fact that it was just an application template that was copied and changed, there was no clear desire to update 5 – 10 created projects.

Since the projects lived in parallel, it was always necessary to migrate projects, copying pieces from one solution to another. At one point, I even thought about creating my own CLI, but the very thought of it clearly screamed that I was doing something wrong.

Around the same time, I started using Nx for my Angular projects.

About Nx

Nx is a set of utilities for creating and managing a monorepository. Nx can be found at official documentation.

Since the article is devoted to the development of typescript applications, the following section is of interest – Nx and TypeScript.

In due time, part of the Angular Team creates its own company Nrwlin which they begin to make their own tools to simplify the development of applications on Angular. Consulting with Fortune 500 companies expanded the toolkit to include not only Angular, but also React, Vue, and pure Typescript.

If you’d rather watch than read, the Nx team regularly releases videos reviewing and tweaking the current toolkit. For example, a video dedicated to the first setup of the application:

Here are a few features of Nx:

  • all the power of typescript along with eslint;

  • a monorepository that allows you to develop multiple applications with a single codebase;

  • generators – a family of console commands to simplify the creation of new files and solutions, with the ability to create your own templates and commands;

  • configurable builds – for example, release versions with source code optimization;

  • migrations – when changing and updating system dependencies, Nx will independently correct the configuration files and bring all the data to the required form;

  • the ability to use all modern React, Angular, Vue and Svetle frameworks.

A complete list can be found in the official Nx documentation.

In other words, NX is a set of utilities that does everything for you, and does it well, but within reasonable limits, of course.

presetting

To work with nx you will need NodeJS installed and one of the package managers such as npm or yarn.

You can get acquainted with NodeJS on the site – https://nodejs.org/en/docs.

Before using nx, you can install nx cli globally:

yarn global add /cli

This will allow you to run nx commands without package managers (yarn or npm):

nx g lib mylib

Otherwise you have to write:

yarn nx g lib mylib

Creating a workspace for a typescript application

In order to create a new Nx workspace, you need to run the command:

npx create-nx-workspace@latest

If you are using yarnthen you can run the following command:

yarn create nx-workspace --package-manager=yarn 
Run command to create Nx workspace
Run command to create Nx workspace

Enter the name of the workspace – boobs:

Create boobs Nx workspace
Create boobs Nx workspace

When creating a workspace, you can select a project type (angular, react, node or typescript). This option determines which dependencies will be included in package.json.

We will choose as a project ts:

Nx project type selection
Nx project type selection

Stop using the cloud:

Nx Cloud Usage Selection Option
Nx Cloud Usage Selection Option

If you are not afraid to become pimpleyou can try using the cloud.

Next, Nx will install the required dependencies:

Nx installed the dependencies, where he immediately offered to go through the tutorial 🙂

Let’s go to the folder with the created project:

cd boobs

Let’s see what Nx created by running the command ls:

But since nothing is clear from the console, open the project in your favorite IDE:

jetbrainsto my great regret, does not want adjust give me a free version. Looks like we’ll have to switch to VSCode.

Formally, Nx will create the following structure:

boobs/
├── packages/
├── tools/
├── workspace.json
├── nx.json
├── package.json
└── tsconfig.base.json

Let’s open package.json:

{
  "name": "boobs",
  "version": "0.0.0",
  "license": "MIT",
  "scripts": {},
  "private": true,
  "dependencies": {},
  "devDependencies": {
    "@nrwl/cli": "13.8.2",
    "@nrwl/js": "13.8.2",
    "@nrwl/tao": "13.8.2",
    "@nrwl/workspace": "13.8.2",
    "@types/node": "16.11.7",
    "prettier": "^2.5.1",
    "typescript": "~4.5.2"
  }
}

As you can see from the content, Nx created a new, empty project with a minimum of dependencies. Packages @nrwl are clear from the name, where they are connected clior jsor carry out the work of workspace.

Consider a file nx.json:

{
  "extends": "@nrwl/workspace/presets/core.json",
  "npmScope": "boobs",
  "affected": {
    "defaultBase": "main"
  },
  "cli": {
    "defaultCollection": "@nrwl/workspace"
  },
  "tasksRunnerOptions": {
    "default": {
      "runner": "@nrwl/workspace/tasks-runners/default",
      "options": {
        "cacheableOperations": [
          "build",
          "lint",
          "test",
          "e2e"
        ]
      }
    }
  }
}

In this case, we have 3 sections:

  • affected – the branch against which the changes in the monorepository will be compared. This is necessary in order to understand which tests to run, and which parts of the project (applications and libraries) were affected by the latest edits.

  • cli – CLI setting. Usually, default collections are specified there. In this case, we use the nx collections. If you are a yarn fanatic, you can add a property "packageManager": "yarn"to always yarn was used.

  • tasksRunnerOptions – a set of rules for launching applications and libraries. For basic setup, you can skip this for now.

File workspace.json will contain configuration paths for libraries and applications:

{
  "version": 2,
  "projects": {}
}

The last file is tsconfig.base.json:

{
  "compileOnSave": false,
  "compilerOptions": {
    "rootDir": ".",
    "sourceMap": true,
    "declaration": false,
    "moduleResolution": "node",
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "importHelpers": true,
    "target": "es2015",
    "module": "esnext",
    "lib": ["es2017", "dom"],
    "skipLibCheck": true,
    "skipDefaultLibCheck": true,
    "baseUrl": ".",
    "paths": {}
  },
  "exclude": ["node_modules", "tmp"]
}

Everything is standard here. You can bump es to the latest version.

The only thing that stands out is the absence eslint.

Let’s add eslint in workspace:

yarn add --dev @nrwl/linter
yarn add --dev @nrwl/eslint-plugin-nx

You can skip this step, because when creating an application, Nx will do everything for you, and will add eslint and jest itself.

Working with NX workspace

You can create at least two types of packages in a monorep:

  • libraries – regular packages that can be placed in npmjs

  • applications are the same as libraries, only the application has ways to trigger change tracking.

For example, let’s create a new application – store:

nx generate @nrwl/js:app store

After creating the application, several files appeared globally:

  • .eslintrc.json – linter settings

  • jest.config.js, jest.preset.js – setup for unit testing with jest

Workspace.json a new project has been added:

{
  "version": 2,
  "projects": {
    "store": "packages/store"
  }
}

Also, due to the lack of jest in the project, Nx kindly installed it by updating package.json:

{
  "name": "boobs",
  "version": "0.0.0",
  "license": "MIT",
  "scripts": {},
  "private": true,
  "dependencies": {
    "tslib": "^2.0.0"
  },
  "devDependencies": {
    "@nrwl/cli": "13.8.2",
    "@nrwl/eslint-plugin-nx": "^13.8.2",
    "@nrwl/jest": "13.8.2",
    "@nrwl/js": "13.8.2",
    "@nrwl/linter": "^13.8.2",
    "@nrwl/tao": "13.8.2",
    "@nrwl/workspace": "13.8.2",
    "@types/jest": "27.0.2",
    "@types/node": "16.11.7",
    "@typescript-eslint/eslint-plugin": "~5.10.0",
    "@typescript-eslint/parser": "~5.10.0",
    "eslint": "~8.7.0",
    "eslint-config-prettier": "8.1.0",
    "jest": "27.2.3",
    "prettier": "^2.5.1",
    "ts-jest": "27.0.5",
    "typescript": "~4.5.2"
  }
}

Of course, it is not clear why it was impossible to immediately add the application launch command to package.json, but oh well.

Let’s start the project:

nx serve store

As you can see, livereload works. If you change the files in the monorepository, then the application will change.

If you open the created project, then there is the following structure:

packages/store
├── jest.config.js
├── package.json
├── project.json
├── README.md
├── src
│   ├── app
│   │   ├── store.spec.ts
│   │   └── store.ts
│   └── index.ts
├── tsconfig.app.json
├── tsconfig.json
└── tsconfig.spec.json

Tsconfig files override the TS global configuration rules.

For example, tsconfig.spec.ts – will create an environment for testing.

Project.json describes the configuration of the application.

{
  "root": "packages/store",
  "sourceRoot": "packages/store/src",
  "projectType": "application",
  "targets": {
    "build": {
      "executor": "@nrwl/js:tsc",
      "outputs": ["{options.outputPath}"],
      "options": {
        "outputPath": "dist/packages/store",
        "main": "packages/store/src/index.ts",
        "tsConfig": "packages/store/tsconfig.app.json",
        "assets": ["packages/store/*.md"]
      }
    },
    "serve": {
      "executor": "@nrwl/js:node",
      "options": {
        "buildTarget": "store:build"
      }
    },
    "lint": {
      "executor": "@nrwl/linter:eslint",
      "outputs": ["{options.outputFile}"],
      "options": {
        "lintFilePatterns": ["packages/store/**/*.ts"]
      }
    },
    "test": {
      "executor": "@nrwl/jest:jest",
      "outputs": ["coverage/packages/store"],
      "options": {
        "jestConfig": "packages/store/jest.config.js",
        "passWithNoTests": true
      }
    }
  },
  "tags": []
}

In this case, the rules for launching and compiling the project are visible, there are commands for checking styles (lint) and testing (test).

The application itself includes only one file – store.ts:

export function store(): string {
  return 'store';
}

Now let’s create the api library:

nx generate @nrwl/js:library api

Library structure:

├── jest.config.js
├── package.json
├── project.json
├── README.md
├── src
│   ├── index.ts
│   └── lib
│       ├── api.spec.ts
│       └── api.ts
├── tsconfig.json
├── tsconfig.lib.json
└── tsconfig.spec.json

One of the few differences between the library configuration and the application is project.json:

{
  "root": "packages/api",
  "sourceRoot": "packages/api/src",
  "projectType": "library",
  "targets": {
    "build": {
      "executor": "@nrwl/js:tsc",
      "outputs": ["{options.outputPath}"],
      "options": {
        "outputPath": "dist/packages/api",
        "main": "packages/api/src/index.ts",
        "tsConfig": "packages/api/tsconfig.lib.json",
        "assets": ["packages/api/*.md"]
      }
    },
    "lint": {
      "executor": "@nrwl/linter:eslint",
      "outputs": ["{options.outputFile}"],
      "options": {
        "lintFilePatterns": ["packages/api/**/*.ts"]
      }
    },
    "test": {
      "executor": "@nrwl/jest:jest",
      "outputs": ["coverage/packages/api"],
      "options": {
        "jestConfig": "packages/api/jest.config.js",
        "passWithNoTests": true
      }
    }
  },
  "tags": []
}

The only difference is the presence of a block with serve:

{
  "root": "packages/store",
  "sourceRoot": "packages/store/src",
  "projectType": "application",
  "targets": {
    ...
    "serve": {
      "executor": "@nrwl/js:node",
      "options": {
        "buildTarget": "store:build"
      }
    },
    ...
  }
}

Using the library api in the application strore:

import { api } from "@boobs/api";

export function store(): string {
  console.log(api());

  return 'store';
}

Let’s assemble the project:

nx run store:build --with-deps

Let’s start the project:

nx serve store

As you can see in the screenshot above, a method from the library was called apibefore running the application store.

Node and Nx

If you need to go further and start developing your application in NodeJs, then Nx has a great package – @nrwl/node. Learn more about development at Node with NX.

Let’s add the node package to the workspace:

yarn add -D @nrwl/node

Now let’s create an application with node:

nx g @nrwl/node:application node-store

The structure of the created application:

packages/node-store
├── jest.config.js
├── project.json
├── src
│   ├── app
│   ├── assets
│   ├── environments
│   │   ├── environment.prod.ts
│   │   └── environment.ts
│   └── main.ts
├── tsconfig.app.json
├── tsconfig.json
└── tsconfig.spec.json

Almost everything is the same, but with a slightly improved structure.

Like all other libraries and applications, the main file is project.json:

{
  "root": "packages/node-store",
  "sourceRoot": "packages/node-store/src",
  "projectType": "application",
  "targets": {
    "build": {
      "executor": "@nrwl/node:build",
      "outputs": ["{options.outputPath}"],
      "options": {
        "outputPath": "dist/packages/node-store",
        "main": "packages/node-store/src/main.ts",
        "tsConfig": "packages/node-store/tsconfig.app.json",
        "assets": ["packages/node-store/src/assets"]
      },
      "configurations": {
        "production": {
          "optimization": true,
          "extractLicenses": true,
          "inspect": false,
          "fileReplacements": [
            {
              "replace": "packages/node-store/src/environments/environment.ts",
              "with": "packages/node-store/src/environments/environment.prod.ts"
            }
          ]
        }
      }
    },
    "serve": {
      "executor": "@nrwl/node:execute",
      "options": {
        "buildTarget": "node-store:build"
      }
    },
    "lint": {
      "executor": "@nrwl/linter:eslint",
      "outputs": ["{options.outputFile}"],
      "options": {
        "lintFilePatterns": ["packages/node-store/**/*.ts"]
      }
    },
    "test": {
      "executor": "@nrwl/jest:jest",
      "outputs": ["coverage/packages/node-store"],
      "options": {
        "jestConfig": "packages/node-store/jest.config.js",
        "passWithNoTests": true
      }
    }
  },
  "tags": []
}

As you can see from the example, there are already environments, there are also various types of assemblies, such as development And production.

The attentive reader will notice that the further the complexity of the structure goes, the more and more it begins to resemble the structure of Angular projects.
You can’t get this enterprise out of your head.

Express and Nx

Let’s go ahead and create a project with express. Let’s add the appropriate Nx package:

yarn add -D @nrwl/express

Let’s create an express application:

 nx g @nrwl/express:app express-store

When running commands, Nx checks the configurations, and in the case of adding express, it added a few dependencies globally to package.json:

{
  "name": "boobs",
  "version": "0.0.0",
  "license": "MIT",
  "scripts": {
    "start": "nx serve store",
    "build": "nx run store:build --with-deps"
  },
  "private": true,
  "dependencies": {
    "express": "4.17.2",
    "tslib": "^2.0.0"
  },
  "devDependencies": {
    "@nrwl/cli": "13.8.2",
    "@nrwl/eslint-plugin-nx": "^13.8.2",
    "@nrwl/express": "^13.8.3",
    "@nrwl/jest": "13.8.2",
    "@nrwl/js": "13.8.2",
    "@nrwl/linter": "^13.8.2",
    "@nrwl/node": "^13.8.3",
    "@nrwl/tao": "13.8.2",
    "@nrwl/workspace": "13.8.2",
    "@types/express": "4.17.13",
    "@types/jest": "27.0.2",
    "@types/node": "16.11.7",
    "@typescript-eslint/eslint-plugin": "~5.10.0",
    "@typescript-eslint/parser": "~5.10.0",
    "eslint": "~8.7.0",
    "eslint-config-prettier": "8.1.0",
    "jest": "27.2.3",
    "prettier": "^2.5.1",
    "ts-jest": "27.0.5",
    "typescript": "~4.5.2"
  }
}

Hence the answer to the question why each of the packages generated by Nx has its own package.json.

Let’s start the project:

nx serve express-store

Let’s open the browser:

Magic, don’t say anything.

If we compare a project on node and express, then, surprisingly, they differ only in the presence of express in main.ts:

/**
 * This is not a production server yet!
 * This is only a minimal backend to get started.
 */

import * as express from 'express';

const app = express();

app.get('/api', (req, res) => {
  res.send({ message: 'Welcome to express-store!' });
});

const port = process.env.port || 3333;
const server = app.listen(port, () => {
  console.log(`Listening at http://localhost:${port}/api`);
});
server.on('error', console.error);

Web and Nx

The last package you can consider is a package for creating web applications – @nrwl/web

Let’s add the package to the workspace:

yarn add -D @nrwl/web

The package includes about 300 packages for web development.

Let’s create a web application:

nx g @nrwl/web:app web-store

Let’s choose SCSS:

Let’s run the application:

nx serve web-store

Open a browser – http://localhost:4200

The above solution is an analogue of my self-written solution, which can now be thrown into the trash.

The next step is to create full-fledged web applications in Angular and React.

Everything is the same there. Add a package, create an application.

yarn add -D @nrwl/angular
nx g @nrwl/angular:app angular-store

However, it is worth noting that it is better to use presets with applications when creating a workspace. This will prevent you from doing a lot of unnecessary work.

Sources

Everything described above can be found in the repository – https://github.com/Fafnur/boobs:

Summary

In this article, I talked about such a tool as Nx and its use in developing web applications. He gave a brief overview of the structure of applications on Nx, and also gave commands for generating libraries and running applications.

In conclusion, I would like to say that web development tools are developing very strongly. It’s sad to see projects still using gulp as their main build system. There are no complaints about gulp, grunt and webpack, it’s just that for a long time many tasks that the assemblers solved have been solved and optimized. And the developer does not need to create and maintain his own toolkit. A good example is Nx, which will take care of all the front-ops and give you the ability to do what a developer should be doing – developing applications, rather than endlessly tweaking configs and changing them when updating vendors.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *