Next.js Multi-Zone Monorepo

Project structure

  • apps

    • web

    • docs

    • store

  • packages

    • components (Shared codebase. Design system, shared react components, custom libraries, …)

    • tsconfig (Typescript configuration)

    • eslint-config-custom (Linter configuration)

  NextJs apps are fully autonomous NextJS applications that runs next to each other as zones. Master app(web) handles the zone configuration and user doesn't know that the app/zone he interacts with has changed.

Zone configuration

The master app/zone hold the configuration. Officially user interacts with this app only and we use the power of rewrites to serve user with a different zone.

// /apps/web/next.config.js
const {
  DOCS_URL = 'http://localhost:3001',
  STORE_URL = 'http://localhost:3002',
} = process.env
module.exports = {
  transpilePackages: ['@shared/components'],
  async rewrites() {
    return [
      {
        source: '/:path*',
        destination: `/:path*`,
      },
      // Docs
      {
        source: '/docs',
        destination: `${DOCS_URL}/docs`,
      },
      {
        source: '/docs/:path*',
        destination: `${DOCS_URL}/docs/:path*`,
      },
      // Store
      {
        source: '/store',
        destination: `${STORE_URL}/store`,
      },
      {
        source: '/store/:path*',
        destination: `${STORE_URL}/store/:path*`,
      },
    ]
  },
}

Every time user goes to subsection of /docs route it will get served with the docs application and so on. Each of the zones need to have a basePath set to that path.


// /apps/docs/next.config.js
module.exports = {
  basePath: '/docs',
}

NPM workspace configuration


// Root package.json
{
   "name": "my-monorepo",
   "version": "1.0.0",
   "description": "",
   "devDependencies": {},
   "dependencies": {},
   "workspaces": [
     "apps/*",
     "packages/*"
   ]
}

This simple configuration will add everything in apps and packages folder to the workspace. NPM will then do its magic and link the subproject and shared dependencies.

Sub apps that want to use that shared codebase now only need to add it to package.json


// /apps/docs/package.json
{
   "name": "docs",
   "dependencies": {
    "@shared/components": "*",
   },
}

and to the NextJS config.


// /apps/docs/next.config.js
module.exports = {
   basePath: '/docs',
   transpilePackages: ['@shared/components'],
}

This simple configuration will allow you to have multi-zone NextJS monorepo.

Turborepo

Turborepo is a build system for JS codebases. It allows for easier control of the packages as well as running custom scripts. Turborepo The configuration is quite straightforward.

// root turbo.json
{
   "$schema": "https://turbo.build/schema.json",
   "pipeline": {
     "build": {
       "dependsOn": ["^build"],
       "outputs": ["dist/**", ".next/**"]
     },
     "tsc": {
      "outputs": []
     },
     "test": {
      "outputs": []
     },
     "lint": {
      "outputs": []
     },
     "dev": {
      "cache": false
     },
     "start": {
      "cache": false
     }
    }
   }

You can specify commands that will run in each of the sub-projects and cache the results for faster second run if nothing has changed.

Shared configuration

Typescript

3 different configs. One is a shared base between environments and 2 for each environment.

Base config

Shared base config

// packages/tsconfig/base.json
{
   "$schema": "https://json.schemastore.org/tsconfig",
   "display": "Default",
   "compilerOptions": {
     "resolveJsonModule": true,
     "esModuleInterop": true,
     "allowSyntheticDefaultImports": true,
     "module": "CommonJS",
     "lib": [
       "es2017",
       "dom"
     ],
     "target": "es5",
     "moduleResolution": "node",
     "skipLibCheck": true,
     "isolatedModules": true,
     "strict": true,
     "incremental": true
   },
   "include": [
     "**/*.ts",
     "**/*.tsx"
   ],
   "exclude": [
    "./node_modules/*"
   ]
}

NextJS

Configuration for NextJs environment  


 // packages/tsconfig/nextjs.json
 {
   "$schema": "https://json.schemastore.org/tsconfig",
   "display": "Next.js",
   "extends": "@shared/tsconfig/base.json",
   "compilerOptions": {
     "experimentalDecorators": true,
     "emitDecoratorMetadata": true,
     "sourceMap": true,
     "keyofStringsOnly": true,
     "noEmit": true,
     "target": "es6",
     "jsx": "preserve",
     "allowJs": true,
     "forceConsistentCasingInFileNames": true,
     "strictNullChecks": true,
     "noImplicitAny": true
   },
   "include": [
     "next-env.d.ts"
     "**/*.ts",
     "**/*.tsx"
   ],
 }

 

React

Configuration for shared react library (Design System) and unit tests  


 // packages/tsconfig/react.json
 {
   "$schema": "https://json.schemastore.org/tsconfig",
   "display": "React",
   "extends": "@shared/tsconfig/base.json",
   "compilerOptions": {
     "declaration": true,
     "composite": true,
     "downlevelIteration": true,
     "outDir": "./dist",
     "jsx": "react",
     "baseUrl": "./src"
  }
 }

Projects then need to add "@shared/tsconfig": "*", into dev-dependencies and extend the config

// /apps/docs/tsconfig.json
// include & exclude need to be redefined to point to this directory
{
 "extends": "@shared/tsconfig/nextjs.json",
 "include": [
   "**/*.ts",
   "**/*.tsx",
   "next-endv.d.ts"
 ],
 "exclude": [
  "node_modules"
 ]
}

Linter

It is required that the name of the package starts with eslint-config- We are using prettier and eslint mix.

// /packages/eslint-config-custom/package.json
{
 "name": "eslint-config-custom",
 "version": "0.0.0",
 "main": "index.js",
 "license": "MIT",
 "dependencies": {
   "eslint": "7.14.0",
   "@next/eslint-plugin-next": "12.1.0",
   "eslint-config-prettier": "6.15.0",
   "eslint-plugin-import": "2.22.1",
   "eslint-plugin-prettier": "3.1.4",
   "eslint-plugin-react": "7.21.5",
   "prettier": "2.2.1",
   "@typescript-eslint/eslint-plugin": "5.27.1",
   "@typescript-eslint/parser": "5.27.1"
 },
 "files": [
   "prettier.json",
   "eslint"
 ],
 "publishConfig": {
  "access": "public"
 }
}
// /packages/eslint-config-custom/index.js
module.exports = {
   parser: "@typescript-eslint/parser",
   plugins: ["import", "@typescript-eslint"],
   ignorePatterns: [
     "node_modules/**",
     "storybook-static/**",
     ".next/**",
     "out/**",
     "dist/**",
   ],
   extends: ["plugin:prettier/recommended", "plugin:@next/next/recommended"],
   parserOptions: {
     ecmaVersion: 2018,
     sourceType: "module",
   }
};
// /packages/eslint-config-custom/prettier.js
module.exports = {
   printWidth: 80,
   singleQuote: true,
   trailingComma: "all",
   parser: "typescript",
};
// /.eslintrc.js
module.exports = {
   root: true,
   extends: ["custom"],
   settings: {
     next: {
     rootDir: ["apps/*/"],
  },
 },
};

Apps then need to add "eslint-config-custom": "*", into dev-dependencies and extend the config

// /apps/docs/.eslintrc.js
module.exports = {
   root: true,
   extends: ['custom'],
}
// /apps/docs/.prettierrc.js
module.exports = {
  …require("eslint-config-custom/prettier.js"),
};

Kudos

Architecture, Code and Configs come from Style Space

Connect with expert stylists, over 1-on-1 video styling sessions for clothing, hair and makeup/skincare styling. Elevate your style, simplify getting ready and save time and money.

Links