Next.js Monorepo : A Way to become master
A Comprehensive Guide with 10 Real-World Scenarios
Table of contents
- Setting Up a Basic Next.js Monorepo
- Scenario 1: Multiple Next.js Applications
- Scenario 2: Shared Components Library
- Scenario 4: TypeScript Integration
- Scenario 5: Third-Party Authentication (Auth0)
- Scenario 6: Microfrontends with Module Federation
- Scenario 7: E2E Testing with Cypress
- Scenario 8: Internationalization (i18n)
- Scenario 9: State Management with Redux Toolkit
- Scenario 10: GraphQL API with Apollo Server
- 13. Advanced Monorepo Management
- 14. Conclusion
Introduction to Monorepos :
A monorepo is a version-controlled code repository that holds many projects. For Next.js applications, this approach can be particularly beneficial when managing multiple interconnected projects, shared libraries, and microservices.Benefits of monorepos:
Simplified dependency management
Easier code sharing and reuse
Atomic commits across projects
Simplified CI/CD pipelines
Setting Up a Basic Next.js Monorepo
Let's start by setting up a basic Next.js monorepo using Yarn workspaces.Folder structure:
nextjs-monorepo/
├── package.json
├── yarn.lock
├── packages/
│ ├── app1/
│ │ ├── package.json
│ │ └── ...
│ ├── app2/
│ │ ├── package.json
│ │ └── ...
│ └── shared/
│ ├── package.json
│ └── ...
└── .gitignore
Root package.json:
{
"private": true,
"workspaces": [
"packages/*"
],
"scripts": {
"dev": "yarn workspaces run dev",
"build": "yarn workspaces run build"
}
}
Scenario 1: Multiple Next.js Applications
In this scenario, we'll set up two Next.js applications within our monorepo.Folder structure:
nextjs-monorepo/
├── packages/
│ ├── app1/
│ │ ├── pages/
│ │ │ └── index.js
│ │ ├── package.json
│ │ └── next.config.js
│ └── app2/
│ ├── pages/
│ │ └── index.js
│ ├── package.json
│ └── next.config.js
└── package.json
app1/package.json:
{
"name": "@myorg/app1",
"version": "1.0.0",
"scripts": {
"dev": "next dev -p 3000",
"build": "next build"
},
"dependencies": {
"next": "^11.0.0",
"react": "^17.0.2",
"react-dom": "^17.0.2"
}
}
app2/package.json:
{
"name": "@myorg/app2",
"version": "1.0.0",
"scripts": {
"dev": "next dev -p 3001",
"build": "next build"
},
"dependencies": {
"next": "^11.0.0",
"react": "^17.0.2",
"react-dom": "^17.0.2"
}
}
Scenario 2: Shared Components Library
Create a shared component library that can be used across multiple Next.js applications.Folder structure:
nextjs-monorepo/
├── packages/
│ ├── shared/
│ │ ├── components/
│ │ │ ├── Button.js
│ │ │ └── Header.js
│ │ ├── package.json
│ │ └── index.js
│ ├── app1/
│ │ ├── pages/
│ │ │ └── index.js
│ │ └── package.json
│ └── app2/
│ ├── pages/
│ │ └── index.js
│ └── package.json
└── package.json
shared/package.json:
{
"name": "@myorg/shared",
"version": "1.0.0",
"main": "index.js",
"dependencies": {
"react": "^17.0.2"
}
}
shared/index.js:
export { default as Button } from './components/Button';
export { default as Header } from './components/Header';
app1/package.json:
{
"name": "@myorg/app1",
"version": "1.0.0",
"dependencies": {
"@myorg/shared": "1.0.0",
"next": "^11.0.0",
"react": "^17.0.2",
"react-dom": "^17.0.2"
}
}
app1/pages/index.js:
import { Button, Header } from '@myorg/shared';
export default function Home() {
return (
<div>
<Header />
<h1>Welcome to App 1</h1>
<Button>Click me</Button>
</div>
);
}
- Scenario 3: API Services with Express
Add an Express.js API service to your monorepo.Folder structure:
nextjs-monorepo/
├── packages/
│ ├── api/
│ │ ├── src/
│ │ │ ├── routes/
│ │ │ │ └── users.js
│ │ │ └── index.js
│ │ ├── package.json
│ │ └── nodemon.json
│ ├── app1/
│ │ └── ...
│ └── app2/
│ └── ...
└── package.json
api/package.json:
{
"name": "@myorg/api",
"version": "1.0.0",
"scripts": {
"dev": "nodemon src/index.js",
"start": "node src/index.js"
},
"dependencies": {
"express": "^4.17.1"
},
"devDependencies": {
"nodemon": "^2.0.7"
}
}
api/src/index.js:
const express = require('express');
const usersRouter = require('./routes/users');
const app = express();
const port = process.env.PORT || 4000;
app.use('/api/users', usersRouter);
app.listen(port, () => {
console.log(`API server running on port ${port}`);
});
Scenario 4: TypeScript Integration
Convert the monorepo to use TypeScript.Folder structure:
nextjs-monorepo/
├── packages/
│ ├── shared/
│ │ ├── components/
│ │ │ ├── Button.tsx
│ │ │ └── Header.tsx
│ │ ├── package.json
│ │ └── index.ts
│ ├── app1/
│ │ ├── pages/
│ │ │ └── index.tsx
│ │ ├── package.json
│ │ └── tsconfig.json
│ └── app2/
│ ├── pages/
│ │ └── index.tsx
│ ├── package.json
│ └── tsconfig.json
├── package.json
└── tsconfig.base.json
tsconfig.base.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": "preserve"
},
"exclude": ["node_modules"]
}
app1/tsconfig.json:
{
"extends": "../../tsconfig.base.json",
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}
Scenario 5: Third-Party Authentication (Auth0)
Implement Auth0 authentication in your Next.js applications.Folder structure:
nextjs-monorepo/
├── packages/
│ ├── auth/
│ │ ├── src/
│ │ │ ├── withAuth.ts
│ │ │ └── AuthProvider.tsx
│ │ └── package.json
│ ├── app1/
│ │ ├── pages/
│ │ │ ├── index.tsx
│ │ │ └── api/
│ │ │ └── auth/
│ │ │ └── [...auth0].ts
│ │ ├── package.json
│ │ └── .env.local
│ └── app2/
│ └── ...
└── package.json
auth/package.json:
{
"name": "@myorg/auth",
"version": "1.0.0",
"main": "src/index.ts",
"dependencies": {
"@auth0/nextjs-auth0": "^1.5.0"
}
}
auth/src/AuthProvider.tsx:
import { UserProvider } from '@auth0/nextjs-auth0';
import React from 'react';
export const AuthProvider: React.FC = ({ children }) => {
return <UserProvider>{children}</UserProvider>;
};
app1/.env.local:
AUTH0_SECRET='your-auth0-secret'
AUTH0_BASE_URL='http://localhost:3000'
AUTH0_ISSUER_BASE_URL='https://your-domain.auth0.com'
AUTH0_CLIENT_ID='your-auth0-client-id'
AUTH0_CLIENT_SECRET='your-auth0-client-secret'
app1/pages/api/auth/[...auth0].ts:
import { handleAuth } from '@auth0/nextjs-auth0';
export default handleAuth();
Scenario 6: Microfrontends with Module Federation
Implement microfrontends using Webpack 5's Module Federation.Folder structure:
nextjs-monorepo/
├── packages/
│ ├── host/
│ │ ├── next.config.js
│ │ ├── pages/
│ │ │ └── index.js
│ │ └── package.json
│ ├── remote1/
│ │ ├── next.config.js
│ │ ├── pages/
│ │ │ └── index.js
│ │ └── package.json
│ └── remote2/
│ ├── next.config.js
│ ├── pages/
│ │ └── index.js
│ └── package.json
└── package.json
host/next.config.js:
const { NextFederationPlugin } = require('@module-federation/nextjs-mf');
module.exports = {
webpack(config, options) {
config.plugins.push(
new NextFederationPlugin({
name: 'host',
remotes: {
remote1: 'remote1@http://localhost:3001/_next/static/chunks/remoteEntry.js',
remote2: 'remote2@http://localhost:3002/_next/static/chunks/remoteEntry.js',
},
shared: {
react: {
singleton: true,
requiredVersion: false,
},
},
})
);
return config;
},
};
Scenario 7: E2E Testing with Cypress
Add end-to-end testing with Cypress to your monorepo.Folder structure:
nextjs-monorepo/
├── packages/
│ ├── app1/
│ │ └── ...
│ ├── app2/
│ │ └── ...
│ └── e2e/
│ ├── cypress/
│ │ ├── integration/
│ │ │ ├── app1.spec.js
│ │ │ └── app2.spec.js
│ │ └── support/
│ │ └── commands.js
│ ├── cypress.json
│ └── package.json
└── package.json
e2e/package.json:
{
"name": "@myorg/e2e",
"version": "1.0.0",
"scripts": {
"test": "cypress run",
"test:open": "cypress open"
},
"devDependencies": {
"cypress": "^7.3.0"
}
}
e2e/cypress/integration/app1.spec.js:
describe('App1', () => {
it('should load the home page', () => {
cy.visit('http://localhost:3000');
cy.contains('Welcome to App 1');
});
});
Scenario 8: Internationalization (i18n)
Implement internationalization across your Next.js applications.Folder structure:
nextjs-monorepo/
├── packages/
│ ├── i18n/
│ │ ├── locales/
│ │ │ ├── en.json
│ │ │ └── es.json
│ │ ├── index.ts
│ │ └── package.json
│ ├── app1/
│ │ ├── pages/
│ │ │ └── index.tsx
│ │ ├── next.config.js
│ │ └── package.json
│ └── app2/
│ └── ...
└── package.json
i18n/package.json:
{
"name": "@myorg/i18n",
"version": "1.0.0",
"main": "index.ts",
"dependencies": {
"next-i18next": "^8.1.0"
}
}
i18n/index.ts:
import { useTranslation, withTranslation, Trans } from 'next-i18next';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
export { useTranslation, withTranslation, Trans, serverSideTranslations };
app1/next.config.js:
const { i18n } = require('./next-i18next.config');
module.exports = {
i18n,
};
app1/pages/index.tsx:
import { useTranslation } from '@myorg/i18n';
export default function Home() { const { t } = useTranslation('common');
return ( <div> <h1>{t('welcome')}</h1> </div> ); }
export const getStaticProps = async ({ locale }) =>
({ props: { ...(await serverSideTranslations(locale, ['common'])), }, });
Scenario 9: State Management with Redux Toolkit
Implement global state management using Redux Toolkit across your applications. Folder structure:
nextjs-monorepo/
├── packages/
│ ├── state/
│ │ ├── src/
│ │ │ ├── slices/
│ │ │ │ └── userSlice.ts
│ │ │ ├── store.ts
│ │ │ └── hooks.ts
│ │ └── package.json
│ ├── app1/
│ │ ├── pages/
│ │ │ └── index.tsx
│ │ └── package.json
│ └── app2/
│ └── ...
└── package.json
state/package.json:
{
"name": "@myorg/state",
"version": "1.0.0",
"main": "src/index.ts",
"dependencies": {
"@reduxjs/toolkit": "^1.5.1",
"react-redux": "^7.2.4"
}
}
state/src/slices/userSlice.ts:
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
interface UserState {
name: string;
email: string;
}
const initialState: UserState = {
name: '',
email: '',
};
export const userSlice = createSlice({
name: 'user',
initialState,
reducers: {
setUser: (state, action: PayloadAction<UserState>) => {
state.name = action.payload.name;
state.email = action.payload.email;
},
},
});
export const { setUser } = userSlice.actions;
export default userSlice.reducer;
state/src/store.ts:
import { configureStore } from '@reduxjs/toolkit';
import userReducer from './slices/userSlice';
export const store = configureStore({
reducer: {
user: userReducer,
},
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
Scenario 10: GraphQL API with Apollo Server
Add a GraphQL API using Apollo Server to your monorepo.Folder structure:
nextjs-monorepo/
├── packages/
│ ├── graphql-api/
│ │ ├── src/
│ │ │ ├── schema.ts
│ │ │ ├── resolvers.ts
│ │ │ └── index.ts
│ │ └── package.json
│ ├── app1/
│ │ ├── pages/
│ │ │ └── index.tsx
│ │ └── package.json
│ └── app2/
│ └── ...
└── package.json
graphql-api/package.json:
{
"name": "@myorg/graphql-api",
"version": "1.0.0",
"main": "src/index.ts",
"scripts": {
"start": "ts-node src/index.ts"
},
"dependencies": {
"apollo-server": "^3.0.0",
"graphql": "^15.5.0"
},
"devDependencies": {
"ts-node": "^10.0.0",
"typescript": "^4.3.2"
}
}
graphql-api/src/schema.ts:
import { gql } from 'apollo-server';
export const typeDefs = gql
type User {
id: ID!
name: String!
email: String!
}
type Query {
users: [User!]!
user(id: ID!): User
}
type Mutation {
createUser(name: String!, email: String!): User!
}
;
graphql-api/src/resolvers.ts:
const users = [
{ id: '1', name: 'John Doe', email: 'john@example.com' },
{ id: '2', name: 'Jane Smith', email: 'jane@example.com' },
];
export const resolvers = {
Query: {
users: () => users,
user: (_, { id }) => users.find(user => user.id === id),
},
Mutation: {
createUser: (_, { name, email }) => {
const newUser = { id: String(users.length + 1), name, email };
users.push(newUser);
return newUser;
},
},
};
graphql-api/src/index.ts:
import { ApolloServer } from 'apollo-server';
import { typeDefs } from './schema';
import { resolvers } from './resolvers';
const server = new ApolloServer({ typeDefs, resolvers });
server.listen().then(({ url }) => {
console.log(`🚀 GraphQL API ready at ${url}`);
});
13. Advanced Monorepo Management
To manage your monorepo more effectively, consider using tools like: - Lerna: For managing multiple packages in a monorepo - Nx: For adding intelligent build system and CLI to your monorepo - Changesets: For managing versioning and changelogs
14. Conclusion
In this comprehensive guide, we've explored ten different scenarios for building and managing a Next.js monorepo. From setting up multiple applications to implementing shared libraries, API services, and various frontend technologies, you now have a solid foundation for creating scalable and maintainable monorepo projects. Remember that the key to a successful monorepo is finding the right balance between shared code and application-specific implementations. As your project grows, continue to refactor and optimize your monorepo structure to ensure it remains manageable and efficient.