This is an alpha, sneak peek of Monorepo Maestros. For this iteration, I'm getting all of my thoughts down. In the future, we'll have better information architecture, graphics, and other awesomeness. Your feedback is welcome!

Next.js

Using Next.js in a monorepo is a great way to give your Next.js applications even more superpowers.

Want easy mode?

If you prefer, you can clone the monorepo for Maestros (remember to star the repo while you're there!) and use the application found in apps/starter-nextjs. Remove any apps you don't want and you're good to go!

Getting started

In your apps directory, use create-next-app to create a new Next.js app:

Terminal
cd apps
npx create-next-app@latest
Terminal
cd apps
npx create-next-app@latest

Add a turbo.json to your new app's directory, configured for Next.js:

apps/my-app/turbo.json
{
"extends": ["//"],
"pipeline": {
"build": {
"outputs": ["./next/**", "!./next/cache/**"],
"dotEnv": [
".env.production.local",
".env.local",
".env.production",
".env"
],
"env": [
// Add your environment variables!
]
},
"dev": {
"persistent": true,
"dotEnv": [
".env.development.local",
".env.local",
".env.development",
".env"
]
},
"lint": {
"outputs": ["node_modules/.cache/.eslintcache"]
},
"lint:fix": {
"outputs": ["node_modules/.cache/.eslintcache"],
"cache": false
},
"format": {
"outputs": ["node_modules/.cache/.prettiercache"]
},
"format:fix": {
"outputs": ["node_modules/.cache/.prettiercache"],
"cache": false
},
"typecheck": {
"outputs": ["node_modules/.cache/tsbuildinfo.json"],
"outputMode": "errors-only"
}
}
}
apps/my-app/turbo.json
{
"extends": ["//"],
"pipeline": {
"build": {
"outputs": ["./next/**", "!./next/cache/**"],
"dotEnv": [
".env.production.local",
".env.local",
".env.production",
".env"
],
"env": [
// Add your environment variables!
]
},
"dev": {
"persistent": true,
"dotEnv": [
".env.development.local",
".env.local",
".env.development",
".env"
]
},
"lint": {
"outputs": ["node_modules/.cache/.eslintcache"]
},
"lint:fix": {
"outputs": ["node_modules/.cache/.eslintcache"],
"cache": false
},
"format": {
"outputs": ["node_modules/.cache/.prettiercache"]
},
"format:fix": {
"outputs": ["node_modules/.cache/.prettiercache"],
"cache": false
},
"typecheck": {
"outputs": ["node_modules/.cache/tsbuildinfo.json"],
"outputMode": "errors-only"
}
}
}

Note: Please refer to the docs at turbo.build/repo/docs/reference/configuration for details on the keys seen here.

Then, use the install command for your package manager. Your new app is now a part of your graph!

Adding your configurations

If you've followed the Prettier, ESLint, and TypeScript pages here in Maestros, you'll want to integrate the configuration from these packages into your Next.js app. Let's get those incorporated in one by one.

Prettier

First, add the scripts to package.json:

apps/my-app/package.json
{
"scripts": {
"format": "prettier . --check --cache --cache-location='node_modules/.cache/prettiercache'",
"format:fix": "prettier . --write --cache --cache-location='node_modules/.cache/prettiercache' --log-level=warn"
},
"devDependencies": {
// Replace latest with most recent version
"prettier": "latest"
}
}
apps/my-app/package.json
{
"scripts": {
"format": "prettier . --check --cache --cache-location='node_modules/.cache/prettiercache'",
"format:fix": "prettier . --write --cache --cache-location='node_modules/.cache/prettiercache' --log-level=warn"
},
"devDependencies": {
// Replace latest with most recent version
"prettier": "latest"
}
}

Prettier will use the configuration found in the root of your monorepo - but it won't respect the .prettierignore found there. That's okay, though, because we can add one within our application's directory so we can make sure we don't spend time formatting files we don't need to.

apps/my-app/.prettierignore
.env*
node_modules
.next
apps/my-app/.prettierignore
.env*
node_modules
.next

TypeScript

First, install your TSConfig presets to your application and making a type checking script:

apps/my-app/package.json
{
"scripts": {
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"@repo/tsconfig": "workspace:*", // or "*" for npm and yarn
// Replace latest with most recent version
"typescript": "latest"
}
}
apps/my-app/package.json
{
"scripts": {
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"@repo/tsconfig": "workspace:*", // or "*" for npm and yarn
// Replace latest with most recent version
"typescript": "latest"
}
}

create-next-app will generate a tsconfig.json for you (as long as you picked TypeScript) so we will extend off of our root configurations there:

apps/my-app/tsconfig.json
{
"extends": "@repo/tsconfig/nextjs.json",
"compilerOptions": {
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json",
"plugins": [{ "name": "next" }],
"baseUrl": ".",
"paths": {
"#/*": ["./*"]
}
},
"include": [
"next-env.d.ts",
"**/*.js",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
"next.config.js"
],
"exclude": ["node_modules", ".next"]
}
apps/my-app/tsconfig.json
{
"extends": "@repo/tsconfig/nextjs.json",
"compilerOptions": {
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json",
"plugins": [{ "name": "next" }],
"baseUrl": ".",
"paths": {
"#/*": ["./*"]
}
},
"include": [
"next-env.d.ts",
"**/*.js",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
"next.config.js"
],
"exclude": ["node_modules", ".next"]
}

ESLint

Using our Next.js preset we can quickly have ESLint set up exactly like the rest of our Next.js apps.

Add ESLint and its scripts to your application's package.json:

apps/my-app/package.json
{
"scripts": {
"lint": "eslint . --cache --cache-location 'node_modules/.cache/.eslintcache' --max-warnings 0",
"lint:fix": "eslint . --fix --cache --cache-location 'node_modules/.cache/.eslintcache' --max-warnings 0"
},
"devDependencies": {
"@repo/lint": "workspace:*", // or "*" for npm and yarn
// Replace latest with most recent version
"eslint": "latest"
}
}
apps/my-app/package.json
{
"scripts": {
"lint": "eslint . --cache --cache-location 'node_modules/.cache/.eslintcache' --max-warnings 0",
"lint:fix": "eslint . --fix --cache --cache-location 'node_modules/.cache/.eslintcache' --max-warnings 0"
},
"devDependencies": {
"@repo/lint": "workspace:*", // or "*" for npm and yarn
// Replace latest with most recent version
"eslint": "latest"
}
}

Then, integrate your configuration into eslintrc.js:

apps/my-app/eslintrc.js
/** @type {import("eslint").Linter.Config} */
module.exports = {
root: true,
extends: [require.resolve('@repo/lint/next.js')],
parserOptions: {
project: true,
},
plugins: ['@typescript-eslint'],
};
apps/my-app/eslintrc.js
/** @type {import("eslint").Linter.Config} */
module.exports = {
root: true,
extends: [require.resolve('@repo/lint/next.js')],
parserOptions: {
project: true,
},
plugins: ['@typescript-eslint'],
};

Note: If you're not using TypeScript, you can remove parserOptions and plugins.

Using your UI package

If you're following the Just-in-Time Package Pattern, you'll want to make sure that Next.js knows to compile your UI components on the fly. To do this, we'll use the transpilePackages property in our Next.js configuration.

next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
transpilePackages: ['@repo/ui'],
};

module.exports = nextConfig;
next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
transpilePackages: ['@repo/ui'],
};

module.exports = nextConfig;

Now, our Next.js app will hot reload any changes in our UI workspace, seamlessly integrating our components into our Next.js development server.

Using framework-specific components in your UI package

You may find moments where you'd like to use Next.js-specific components as a part of your UI kit. Rather than installing Next.js into your UI component workspace, it's better to keep your components unaware of Next.js and pass the Next.js components as props to your UI component.

As an example, let's say you wanted to use next/link in your application using styles you've written out in your UI components. You'd first need to create a UI component that can accept Link from 'next/link' as a prop:

packages/ui/src/MyLink.ts
export const MyLink = ({ linkComponent, children, ...props }) => {
const Component = linkComponent;
return <Component {...props}>{children}</Component>;
};
packages/ui/src/MyLink.ts
export const MyLink = ({ linkComponent, children, ...props }) => {
const Component = linkComponent;
return <Component {...props}>{children}</Component>;
};

And use it in your app like this:

apps/my-app/app/page.tsx
import { MyLink } from '@repo/ui';
import Link from 'next/link';

export default function Page() {
return (
<MyLink linkComponent={Link} href="/about">
Click me!
</MyLink>
);
}
apps/my-app/app/page.tsx
import { MyLink } from '@repo/ui';
import Link from 'next/link';

export default function Page() {
return (
<MyLink linkComponent={Link} href="/about">
Click me!
</MyLink>
);
}

With Tailwind

We like to let the application compile Tailwind classes for us to keep things simple. We've written more about this pattern and why we do it that way on the Tailwind page.