DEV Community

Cover image for Creating a Monorepo with NextJS and Yarn Workspaces: A How-to Guide
Radzion Chachura
Radzion Chachura

Posted on • Originally published at radzion.com

Creating a Monorepo with NextJS and Yarn Workspaces: A How-to Guide

Watch on YouTube | 🐙 GitHub

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. Further, you could also use 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.

yarn set version stable
Enter fullscreen mode Exit fullscreen mode

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

{
  "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"
  }
}
Enter fullscreen mode Exit fullscreen mode

For this tutorial, we will use the name reactkit to maintain consistency with my GitHub Template. 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.

{
  "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"]
}
Enter fullscreen mode Exit fullscreen mode

To customize Prettier, include a .prettierrc file:

{
  "singleQuote": true,
  "semi": false,
  "tabWidth": 2
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

{
  "*.{js,ts,tsx,json}": ["yarn format"]
}
Enter fullscreen mode Exit fullscreen mode

Below is a generic Eslint config for our monorepo:

{
  "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": {}
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Subsequently, generate a new git repository with git init:

git init
Enter fullscreen mode Exit fullscreen mode

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

npx husky-init
Enter fullscreen mode Exit fullscreen mode

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

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

yarn lint-staged
Enter fullscreen mode Exit fullscreen mode

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:

nodeLinker: node-modules
Enter fullscreen mode Exit fullscreen mode

Now, you can install dependencies with:

yarn
Enter fullscreen mode Exit fullscreen mode

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 to our monorepo.

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

npx create-next-app@latest app
Enter fullscreen mode Exit fullscreen mode

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

Setup NextJS App

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

/** @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
Enter fullscreen mode Exit fullscreen mode

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

yarn add styled-components@^5.3.5 next-sitemap
yarn add --dev @types/styled-components@^5.1.25
Enter fullscreen mode Exit fullscreen mode

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

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

Also, update the build command for the app:

{
  "scripts": {
    "build": "next build && next-sitemap"
  }
}
Enter fullscreen mode Exit fullscreen mode

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

{
  "compilerOptions": {
    "baseUrl": "."
  }
}
Enter fullscreen mode Exit fullscreen mode

For efficient use of local storage, we'll copy the state folder from ReactKit. To understand more about the implementation, you can check this post.

We'll do the same thing with the ui folder 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.

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.

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
Enter fullscreen mode Exit fullscreen mode

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

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
Enter fullscreen mode Exit fullscreen mode

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

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

To deploy a static NextJS app to AWS S3 and CloudFront, you can follow this post.

Top comments (0)