Go Back

Setting up a multi-package project

Git, Project Setup
Setting up a multi-package project

Photo by Emile Perron on Unsplash

This is a simple project setup guide for npm-based multi-package projects. It takes advantage of yarn workspaces for the multi-package handling part (we'll be using yarn as package manager).

For this example, we'll setup everything for two sub-packages (frontend & backend). Create your packages accordingly, following this basic folder structure:

my-project
└── packages
    ├── backend
    │   └── package.json
    └── frontend
        └── package.json

Assuming we have already created both base sub-packages, from the project root folder run the following command:

1rm -rf packages/**/node_modules/ packages/**/package-lock.json packages/**/yarn.lock

This will get rid of all previously installed dependencies per sub-package and existing lock files.

Afterwards, we'll initialize our project by running the following command from the project root folder:

1yarn init -yp

This will create a package.json file with default values provided by Yarn, with "private": true added.

We'll do some basic changes to the package.json file we're left with:

1// my-project/package.json 2{ 3 // We can get rid of "main" 4 "name": "my-project", // The name of your project. Optionally removable 5 "version": "1.0.0", // Or any version you desire. Optionally removable 6 "private": true, // By having the project be private, we enable yarn workspaces 7 "scripts": { 8 "build": "yarn workspaces run build", 9 // Commands to run FE & BE directly from project root 10 "dev:frontend": "yarn workspace @my-project/frontend start:dev", 11 "dev:backend": "yarn workspace @my-project/backend start:dev" 12 }, 13 "engines": { 14 "node": "^16.16.0" 15 }, 16 // Our base packages (workspaces) folder 17 "workspaces": { 18 "packages": ["packages/*"] 19 }, 20 "license": "MIT" // Or any license you'll be using for your project. Optionally removable 21}

Once these changes are completed, we'll also need to update the respective package.json files for all sub-packages with the following:

1// my-project/packages/{package-name}/package.json 2{ 3 "name": "@my-project/{package-name}", // e.g. "name": "@my-project/frontend", 4 "private": true, // Ensure this line exists to enable yarn workspaces 5 "scripts": { 6 // ... 7 "start:dev": "Command to start development", // e.g. "start:dev": "next dev", 8 "build": "Command to run build" // e.g. "build": "next build", 9 // ... 10 } 11 // ... 12}

We'll also need to add the respective .gitignore files to prevent undesired file tracking. Here's a basic example for the project root:

# node
node_modules/

# logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.log/

# dotenv
.env

# local editors
.vscode
.DS_Store

Finally, we run the yarn command from the root folder, and voilá!, all sub-package dependencies are now installed.

We can now run yarn dev:{package-name} from our project root folder to start the desired sub-package development server or yarn build to build all sub-packages.

Formatting & linting (Optional)

This section will contain additional project configuration focused towards project linting & formatting. If you desire to proceed without this, feel free to skip to the next section.

We'll use ESLint with some plugins, alongside Prettier for project linting & file formatting.

We'll also add Husky and git-format-staged (a sweet dependency by Jesse Hallett) for even nicer file formatting configuration.

Run the following command to install all additional dependencies:

1yarn add -DW \ 2 cross-env \ 3 eslint \ 4 eslint-config-prettier \ 5 eslint-plugin-flowtype \ 6 eslint-plugin-import \ 7 eslint-plugin-prettier \ 8 eslint-plugin-promise \ 9 git-format-staged \ 10 husky \ 11 prettier

After the installation is completed, we'll create a custom yarn script to add a fix for Husky hooks in sub-packages. Do so with the following command:

1mkdir scripts && touch scripts/yarn-prepare.sh

This sould result in the following folder structure:

my-project
├── node_modules
├── package.json
├── packages
│   ├── backend
│   │   └── package.json
│   └── frontend
│       └── package.json
├── scripts
│   └── yarn-prepare.sh
└── yarn.lock

Add the following to the contents of the script file:

1# yarn-prepare.sh 2#! /bin/bash 3 4set -e 5 6DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" 7 8HUSKY_LOCAL_SH="$DIR/../.git/hooks/husky.local.sh" 9if [[ -f "$HUSKY_LOCAL_SH" ]]; then 10 sed -i'' -E 's/cd \".+\"/cd \".\"/' "$HUSKY_LOCAL_SH"; 11fi

Next, we'll add three extra commands to our root package.json

1// my-project/package.json 2{ 3 // ... 4 "scripts": { 5 // ... 6 "prepare": "cross-env-shell scripts/yarn-prepare.sh", 7 "lint": "yarn workspaces run lint", 8 "fix": "yarn workspaces run lint --fix" 9 } 10 // ... 11}

We'll also need to add the corresponding commands for each sub-package:

1// my-project/packages/{package-name}/package.json 2{ 3 // ... 4 "scripts": { 5 // ... 6 "lint": "eslint --cache {your-file-selection}", 7 // Examples: 8 // "lint": "eslint --cache --ext .ts,.tsx .", 9 // "lint": "eslint --cache \"{src,apps,libs,test}/**/*.{ts,tsx}\"", 10 "prepare": "cross-env-shell ../../scripts/yarn-prepare.sh" 11 // ... 12 } 13 // ... 14}

Once all changes are put in, we can proceed with our Husky hook. We'll create a file called .huskyrc.json at our project root and add the following contents to it:

1// my-project/.huskyrc.json 2{ 3 "hooks": { 4 "pre-commit": "git-format-staged -f 'prettier --stdin-filepath \"{}\"' {your-file-extensions}" 5 // e.g. "pre-commit": "git-format-staged -f 'prettier --stdin-filepath \"{}\"' '*.ts' '*.tsx' '*.json' '*.md' '*.markdown'" 6 } 7}

This will ensure all selected file extensions get properly formatted.

After that, we create our Prettier configuration file. We'll call it .prettierrc and also place it at our project root folder. You can add any custom configuration you desire from the Prettier Configuration Options. Here's an example provided:

1// my-project/.prettierrc 2{ 3 "arrowParens": "always", 4 "semi": false, 5 "trailingComma": "all", 6 "useTabs": true, 7 "tabWidth": 4, 8 "singleQuote": false, 9 "bracketSpacing": true 10}

Finally, with the addition of ESLint, when linting, we'll craete .eslintcache files. We'll want to add those to our .gitignore files to avoid tracking them:

# my-project/.gitignore

# eslint
.eslintcache

Finally, we'll want to add the corresponding .eslintrc.js files, with the desired rules for each case. You can get a better ideaa on how to achieve this by taking a look at ESLint's Configuration Guide. We can even create a base .eslintrc.base.js and place it inside our packages and have sub-packages' configuration files extend from it:

1// my-projects/packages/.eslintrc.base.js 2module.exports = { 3 extends: [ 4 "plugin:prettier/recommended", 5 "plugin:flowtype/recommended", 6 "plugin:import/recommended", 7 "plugin:promise/recommended", 8 ], 9};

Here's an example configuration file for a Next.js application with TypeScript

1// my-project/packages/frontend/.eslintrc.js 2module.exports = { 3 extends: "../.eslintrc.base.js", // Here's how we extend from our base configuration file 4 parserOptions: { 5 tsconfigRootDir: __dirname, 6 project: "./tsconfig.json", 7 }, 8 env: { 9 node: true, 10 }, 11 rules: { 12 "@typescript-eslint/no-unused-vars": "off", 13 "react/react-in-jsx-scope": "off", 14 "jsx-a11y/anchor-is-valid": "off", 15 "@next/next/no-img-element": "off", 16 "@next/next/no-css-tags": "off", 17 }, 18 overrides: [ 19 { 20 files: ["*.tsx"], 21 rules: { 22 "@typescript-eslint/explicit-function-return-type": ["off"], 23 "@typescript-eslint/explicit-module-boundary-types": ["off"], 24 }, 25 }, 26 ], 27};

And that's it! We now have a fully functional multi-packages project configured! (maybe even formatting/linting enabled, if you didn't skip that part)

You can take a look at an example repository containing all that was done in this GitHub repo, and the version with the formatting/linting changes on this branch.