Creating a Monorepo with NextJS and Yarn Workspaces: A How-to Guide
Tags: monorepo, nextjs, yarnworkspaces, styledcomponents
Published: 2023-08-21 09:53:34

✨ [**Watch on YouTube**](https://youtu.be/T6pWiEHVhBE)  |  🐙 [GitHub](https://github.com/radzionc/reactkit)

These days, I use NextJS for my front-end work, and I prefer to do it within a monorepo. This approach allows me to store all the front-ends of the product in the same repository along with back-end services and shared libraries. This setup enables me to reuse code and finish work faster. I will guide you through how to establish such a monorepo using Yarn Workspaces and NextJS. The source code and commands can be found [here](https://github.com/radzionc/reactkit). Further, you could also use [ReactKit](https://github.com/radzionc/reactkit) as a template for your project instead of going through the post.

## Create a Monorepo with Yarn Workspaces

Firstly, verify that you have the latest stable version of Yarn installed. In case you do not have Yarn installed, follow the [instructions from their website](https://yarnpkg.com/getting-started/install).

```sh
yarn set version stable
```

Then, create a folder for your project and add a `package.json` with the subsequent content.

```json
{
  "name": "reactkit",
  "packageManager": "yarn@3.6.1",
  "engines": {
    "yarn": "3.x"
  },
  "private": true,
  "workspaces": ["./*"],
  "devDependencies": {
    "@typescript-eslint/eslint-plugin": "latest",
    "@typescript-eslint/parser": "latest",
    "eslint": "latest",
    "husky": "latest",
    "lint-staged": "latest",
    "prettier": "latest",
    "typescript": "latest"
  },
  "scripts": {
    "postinstall": "husky install",
    "format": "eslint --fix && prettier --write"
  }
}
```

For this tutorial, we will use the name `reactkit` to maintain consistency with my [GitHub Template](https://github.com/radzionc/reactkit). The `packageManager` and `engines` fields are optional; they can be employed to compel your team to utilize the same package manager. Our monorepo will not be published to NPM, so set `private` to `true`. We will incorporate a flat structure, thus you can consider everything in the root directory a potential Yarn workspace.

The `devDependencies` incorporate typescript and libraries for linting and formatting. The "scripts" section includes a `postinstall` script that installs Husky hooks, and a `format` script that formats and lints the entire project.

Next, add a `tsconfig.json` for TypeScript configuration.

```json
{
  "compilerOptions": {
    "target": "es5",
    "lib": ["dom", "dom.iterable", "ESNext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "ESNext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "react-jsx"
  },
  "exclude": ["node_modules"],
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "types.d.ts"]
}
```

To customize Prettier, include a `.prettierrc` file:

```json
{
  "singleQuote": true,
  "semi": false,
  "tabWidth": 2
}
```

It is optional to add a `.prettierignore` file, which will utilize the same syntax as `.gitignore` to exclude certain files from formatting.

```
.vscode
.yarn
.next
out
```

To execute Prettier and eslint upon commit, you can add a `.lintstagedrc.json` file with instructions for the `lint-staged` library:

```json
{
  "*.{js,ts,tsx,json}": ["yarn format"]
}
```

Below is a generic Eslint config for our monorepo:

```json
{
  "env": {
    "browser": true,
    "es2021": true
  },
  "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
  "parser": "@typescript-eslint/parser",
  "parserOptions": {
    "ecmaVersion": "latest",
    "sourceType": "module"
  },
  "plugins": ["@typescript-eslint"],
  "rules": {}
}
```

Let's include a `.gitignore` file to eliminate unnecessary yarn files and `node_modules`:

```
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
.DS_Store

node_modules
```

Subsequently, generate a new git repository with `git init`:

```sh
git init
```

To format files during the pre-commit phase, we will use the `husky` library. Initialize it with this command:

```sh
npx husky-init
```

For the pre-commit hook, we want to run lin-staged. So, modify the `.husky/pre-commit` file:

```sh
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

yarn lint-staged
```

Before creating our first workspace, you must decide whether to use Yarn's plug'n'play feature or not. Plug'n'play is the default method of managing dependencies in the latest Yarn, but it might not work for certain packages that rely on the traditional method of having all packages in the same `node_modules` directory and I found it to be quite troublesome. I believe the ecosystem is not quite ready for it yet, so it might be better to try it in a year. For now, we'll proceed with node_modules by adding the following line to the `.yarnrc.yml` file:

```yml
nodeLinker: node-modules
```

Now, you can install dependencies with:

```sh
yarn
```

## Add NextJS App to the Monorepo

Our NextJS app requires a UI package with a components system. So, let's copy the [ui folder from ReactKit](https://github.com/radzionc/reactkit) to our monorepo.

To establish a new NextJS project, we will use the `create-next-app` command:

```sh
npx create-next-app@latest app
```

We won't use server components in this tutorial, so there is no need for the `app router`.

![Setup NextJS App](/next.png)

Update the `next.config.js` file to add support for styled-components, static export, and to transpile our shared UI package:

```js
/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  compiler: {
    styledComponents: true,
  },
  output: "export",
  transpilePackages: ["@reactkit/ui"],
}

// eslint-disable-next-line no-undef
module.exports = nextConfig
```

Also, you should install `styled-component` and `next-sitemap` packages.

```sh
yarn add styled-components@^5.3.5 next-sitemap
yarn add --dev @types/styled-components@^5.1.25
```

To generate a sitemap, add a `next-sitemap.config.js` file. I prefer to provide siteUrl from the environment variable - `NEXT_PUBLIC_BASE_URL`

```js
/** @type {import('next-sitemap').IConfig} */
module.exports = {
  siteUrl: process.env.NEXT_PUBLIC_BASE_URL,
  generateRobotsTxt: true,
  generateIndexSitemap: false,
  outDir: "./out",
}
```

Also, update the `build` command for the `app`:

```json
{
  "scripts": {
    "build": "next build && next-sitemap"
  }
}
```

To use absolute imports within the app project, update the `tsconfig.json` file with the `compilerOptions` field:

```json
{
  "compilerOptions": {
    "baseUrl": "."
  }
}
```

For efficient use of local storage, we'll copy [the state folder](https://github.com/radzionc/reactkit/tree/main/app/state) from ReactKit. To understand more about the implementation, you can check [this post](https://radzion.com/blog/local-storage).

We'll do the same thing with [the ui folder](https://github.com/radzionc/reactkit/tree/main/app/ui) that gives support for the dark and light mode along with the theme for styled-components. You can watch a video on this topic [here](https://radzion.com/blog/theme).

We also need to update the `_document.tsx` file to support styled-components and add basic meta tags. To learn more about generating icons for PWA, you can check [this post](https://radzion.com/blog/pwa-icons).

```tsx
import Document, {
  Html,
  Main,
  NextScript,
  DocumentContext,
  Head,
} from "next/document"
import { ServerStyleSheet } from "styled-components"
import { MetaTags } from "@reactkit/ui/metadata/MetaTags"
import { AppIconMetaTags } from "@reactkit/ui/metadata/AppIconMetaTags"

class MyDocument extends Document {
  static async getInitialProps(ctx: DocumentContext) {
    const sheet = new ServerStyleSheet()
    const originalRenderPage = ctx.renderPage
    try {
      ctx.renderPage = () =>
        originalRenderPage({
          enhanceApp: (App) => (props) =>
            sheet.collectStyles(<App {...props} />), //gets the styles from all the components inside <App>
        })
      const initialProps = await Document.getInitialProps(ctx)
      return {
        ...initialProps,
        styles: (
          <>
            {initialProps.styles}
            {sheet.getStyleElement()}
          </>
        ),
      }
    } finally {
      sheet.seal()
    }
  }
  render() {
    return (
      <Html lang="en">
        <Head>
          <MetaTags
            title="ReactKit"
            description="A React components system for faster development"
            url={process.env.NEXT_PUBLIC_BASE_URL}
            twitterId="@radzionc"
          />
          <AppIconMetaTags />
        </Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    )
  }
}

export default MyDocument
```

We will be using the styled-component, so let's remove the `styles` folder and modify the `_app.tsx` file:

```tsx
import type { AppProps } from "next/app"
import { GlobalStyle } from "@reactkit/ui/ui/GlobalStyle"
import { ThemeProvider } from "ui/ThemeProvider"
import { Inter } from "next/font/google"

const inter = Inter({
  subsets: ["latin"],
  weight: ["400", "500", "600", "800"],
})

function MyApp({ Component, pageProps }: AppProps) {
  return (
    <ThemeProvider>
      <GlobalStyle fontFamily={inter.style.fontFamily} />
      <Component {...pageProps} />
    </ThemeProvider>
  )
}

export default MyApp
```

Finally, we can try to import one of the reusable components from the ui package to the `index.tsx` page:

```tsx
import { Center } from "@reactkit/ui/ui/Center"
import { Button } from "@reactkit/ui/ui/buttons/Button"

export default function Home() {
  return (
    <Center>
      <Button size="xl">ReactKit is Awesome!</Button>
    </Center>
  )
}
```

To deploy a static NextJS app to AWS S3 and CloudFront, you can follow [this post](https://radzion.com/blog/deploy-next).