belgattitude / nextjs-monorepo-example

Collection of monorepo tips & tricks
MIT License
1.62k stars 214 forks source link
example monorepo nextjs tutorial typescript vercel vite workspace workspaces yarn

NextJs Monorepo

Monorepo concepts, tips and tricks oriented around NextJs

GitHub branch checks state Codefactor Maintainability Techdebt Codacy grade LoC GitHub top language Licence

Howtos for monorepo. New to monorepos ? check this FAQ. This example is managed by turborepo and yarn 4 with a / typescript path aliases approach. Not the only way to do.

Useful to

Sponsors ❤️

If you are enjoying some of my OSS work in your company, I'd really appreciate a sponsorship, a coffee or a dropped star. That gives me some more time to improve it to the next level.

Special thanks to

Jetbrains logo Jetbrains logo Vercel logo
JetBrains Embie.be Vercel

Install

corepack enable
yarn install

Structure

Open in Gitpod

.
├── apps
│   ├── nextjs-app  (i18n, ssr, api, vitest)
│   └── vite-app
└── packages
    ├── common-i18n         (locales...)
    ├── core-lib
    ├── db-main-prisma
    ├── eslint-config-bases (to shared eslint configs)
    └── ui-lib              (emotion, storybook)

Example apps

Apps should not depend on apps, they can depend on packages

Example shared packages

Apps can depend on packages, packages can depend on each others...

Folder overview

Detailed folder structure ``` . ├── apps │ ├── vite-app (Vite app as an example) │ │ ├── src/ │ │ ├── package.json (define package workspace:package deps) │ │ └── tsconfig.json (define path to packages) │ │ │ └── nextjs-app (NextJS app with api-routes) │ ├── e2e/ (E2E tests with playwright) │ ├── public/ │ ├── src/ │ │ └── pages/api (api routes) │ ├── CHANGELOG.md │ ├── next.config.mjs │ ├── package.json (define package workspace:package deps) │ ├── tsconfig.json (define path to packages) │ └── vitest.config.ts │ ├── packages │ ├── core-lib (basic ts libs) │ │ ├── src/ │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ └── tsconfig.json │ │ │ ├── db-main-prisma (basic db layer with prisma) │ │ ├── e2e/ (E2E tests) │ │ ├── prisma/ │ │ ├── src/ │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ └── tsconfig.json │ │ │ ├── eslint-config-bases │ │ ├── src/ │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ └── tsconfig.json │ │ │ └── ui-lib (basic design-system in react) │ ├── src/ │ ├── CHANGELOG.md │ ├── package.json │ └── tsconfig.json │ ├── static (no code: images, json, locales,...) │ ├── assets │ └── locales ├── docker (docker...) │ ├── .dockerignore │ ├── docker-compose.yml (compose specific for nextjs-app) │ ├── docker-compose.db.yml (general services like postgresql...) │ └── Dockerfile (multistage build for nextjs-app) ├── .yarnrc.yml ├── package.json (the workspace config) └── tsconfig.base.json (base typescript config) ```

Howto

1. Enable workspace support

Root package.json with workspace directories ```json5 { "name": "nextjs-monorepo-example", // Set the directories where your apps, packages will be placed "workspaces": ["apps/*", "packages/*"], //... } ``` _The package manager will scan those directories and look for children `package.json`. Their content is used to define the workspace topology (apps, libs, dependencies...)._

2. Create a new package

Create a folder in ./packages/ directory with the name of your package.

Create the package folder ```bash mkdir packages/magnificent-poney mkdir packages/magnificent-poney/src cd packages/magnificent-poney ```

Initialize a package.json with the name of your package.

Rather than typing yarn init, prefer to take the ./packages/ui-lib/package.json as a working example and edit its values.

Example of package.json ```json5 { "name": "@your-org/magnificent-poney", "version": "0.0.0", "private": true, "scripts": { "clean": "rimraf ./tsconfig.tsbuildinfo", "lint": "eslint . --ext .ts,.tsx,.js,.jsx", "typecheck": "tsc --project ./tsconfig.json --noEmit", "test": "run-s 'test:*'", "test:unit": "echo \"No tests yet\"", "fix:staged-files": "lint-staged --allow-empty", "fix:all-files": "eslint . --ext .ts,.tsx,.js,.jsx --fix", }, "devDependencies": { "@your-org/eslint-config-bases": "workspace:^", }, } ```

3. Using the package in app

Step 3.1: package.json

First add the package to the app package.json. The recommended way is to use the workspace protocol supported by yarn and pnpm.

cd apps/my-app
yarn add @your-org/magnificent-poney@'workspace:^'

Inspiration can be found in apps/nextjs-app/package.json.

package.json ```json5 { "name": "my-app", "dependencies": { "@your-org/magnificient-poney": "workspace:^", }, } ```

Step 3.2: In tsconfig.json

Then add a typescript path alias in the app tsconfig.json. This will allow you to import it directly (no build needed)

Inspiration can be found in apps/nextjs-app/tsconfig.json.

Example of tsonfig.json ```json5 { "compilerOptions": { "baseUrl": "./src", "paths": { // regular app aliases "@/components/*": ["./components/*"], // packages aliases, relative to app_directory/baseUrl "@your-org/magnificent-poney/*": [ "../../../packages/magnificent-poney/src/*", ], "@your-org/magnificent-poney": [ "../../../packages/magnificent-poney/src/index", ], }, }, } ``` > PS: > > - Don't try to set aliases in the global tsonfig.base.json to keep strict with > graph dependencies. > - The **star** in `@your-org/magnificent-poney/*` allows you to import subfolders. If you use > a barrel file (index.ts), the alias with star can be removed.

Step 3.3: Next config

Edit your next.config.mjs and enable the experimental.externalDir option. Feedbacks here.

const nextConfig = {
  experimental: {
    externalDir: true,
  },
};
Using a NextJs version prior to 10.2.0 ? If you're using an older NextJs version and don't have the experimental flag, you can simply override your webpack config. ```js const nextConfig = { webpack: (config, { defaultLoaders }) => { // Will allow transpilation of shared packages through tsonfig paths // @link https://github.com/vercel/next.js/pull/13542 const resolvedBaseUrl = path.resolve(config.context, "../../"); config.module.rules = [ ...config.module.rules, { test: /\.(tsx|ts|js|jsx|json)$/, include: [resolvedBaseUrl], use: defaultLoaders.babel, exclude: (excludePath) => { return /node_modules/.test(excludePath); }, }, ]; return config; }, }; ```

PS: If your shared package make use of scss bundler... A custom webpack configuration will be necessary or use next-transpile-modules, see FAQ below.

Step 3.4: Using the package

The packages are now linked to your app, just import them like regular packages: import { poney } from '@your-org/magnificent-poney'.

4. Publishing

Optional

If you need to share some packages outside of the monorepo, you can publish them to npm or private repositories. An example based on microbundle is present in each package. Versioning and publishing can be done with atlassian/changeset, and it's simple as typing:

$ yarn g:changeset

Follow the instructions... and commit the changeset file. A "Version Packages" P/R will appear after CI checks. When merging it, a github action will publish the packages with resulting semver version and generate CHANGELOGS for you.

PS:

  • Even if you don't need to publish, changeset can maintain an automated changelog for your apps. Nice !
  • To disable automatic publishing of some packages, just set "private": "true" in their package.json.
  • Want to tune the behaviour, see .changeset/config.json.

4. Monorepo essentials

Monorepo scripts

Some convenience scripts can be run in any folder of this repo and will call their counterparts defined in packages and apps.

Name Description
yarn g:changeset Add a changeset to declare a new version
yarn g:codegen Run codegen in all workspaces
yarn g:typecheck Run typechecks in all workspaces
yarn g:lint Display linter issues in all workspaces
yarn g:lint --fix Attempt to run linter auto-fix in all workspaces
yarn g:lint-styles Display css stylelint issues in all workspaces
yarn g:lint-styles --fix Attempt to run stylelint auto-fix issues in all workspaces
yarn g:test Run unit and e2e tests in all workspaces
yarn g:test-unit Run unit tests in all workspaces
yarn g:test-e2e Run e2e tests in all workspaces
yarn g:build Run build in all workspaces
yarn g:clean Clean builds in all workspaces
yarn g:check-dist Ensure build dist files passes es2017 (run g:build first).
yarn g:check-size Ensure browser dist files are within size limit (run g:build first).
yarn clean:global-cache Clean tooling caches (eslint, jest...)
yarn deps:check --dep dev Will print what packages can be upgraded globally (see also .ncurc.yml)
yarn deps:update --dep dev Apply possible updates (run yarn install && yarn dedupe after)
yarn install:playwright Install playwright for e2e
yarn dedupe Built-in yarn deduplication of the lock file

Why using : to prefix scripts names ? It's convenient in yarn 3+, we can call those scripts from any folder in the monorepo. g: is a shortcut for global:. See the complete list in root package.json.

Maintaining deps updated

The global commands yarn deps:check and yarn deps:update will help to maintain the same versions across the entire monorepo. They are based on the excellent npm-check-updates (see options, i.e: yarn check:deps -t minor).

After running yarn deps:update, a yarn install is required. To prevent having duplicates in the yarn.lock, you can run yarn dedupe --check and yarn dedupe to apply deduplication. The duplicate check is enforced in the example github actions.

5. Quality

5.1 Linters

See an example in ./apps/nextjs-app/.eslintrc.js and our eslint-config-bases.

5.2 Hooks / Lint-staged

Check the .husky folder content to see what hooks are enabled. Lint-staged is used to guarantee that lint and prettier are applied automatically on commit and/or pushes.

5.3 Tests

Tests relies on ts-jest or vitest depending on the app. All setups supports typescript path aliases. React-testing-library is enabled whenever react is involved.

Configuration lives in the root folder of each apps/packages. As an example see

5.4 CI

You'll find some example workflows for github action in .github/workflows. By default, they will ensure that

Each of those steps can be opted-out.

To ensure decent performance, those features are present in the example actions:

6. Editor support

6.1 VSCode

The ESLint plugin requires that the eslint.workingDirectories setting is set:

"eslint.workingDirectories": [
    {
        "pattern": "./apps/*/"
    },
    {
        "pattern": "./packages/*/"
    }
],

More info here

7. Deploy

Vercel

Vercel support natively monorepos, see the vercel-monorepo-deploy document.

Docker

There's a basic example for building a docker image, read the docker doc.

Others

Netlify, aws-amplify, k8s-docker, serverless-nextjs recipes might be added in the future. PR's welcome too.

FAQ

Monorepo

Benefits

Drawbacks

Exact vs semver dependencies

Apps dependencies and devDependencies are pinned to exact versions. Packages deps will use semver compatible ones. For more info about this change see reasoning here and our renovabot.json5 configuration file.

To help keeping deps up-to-date, see the yarn deps:check && yarn deps:update scripts and / or use the renovatebot.

When adding a dep through yarn cli (i.e.: yarn add something), it's possible to set the save-exact behaviour automatically by setting defaultSemverRangePrefix: "" in yarnrc.yml. But this would make the default for packages/* as well. Better to handle yarn add something --exact on per-case basis.

License

FOSSA Status