React skeleton
It may include:
- Config files
- Directory structure.
- Common scripts should be present.
- Dependencies are already chosen.
- Pre-setup, build tooling like Vite
Repo link
https://github.com/rtCamp/react-skeleton
Monorepo
A monorepo is a single repository containing multiple projects/packages (apps, libraries, tools, configs), but each is modular and independent.
root/
├── .github
├── .husky
├── apps/
│ ├── docs-design-system/
│ └── web/
├── packages/
│ ├── design-system/
│ ├── config-eslint/
│ └── config-typescript/
└── package.json
└── .nvmrc
└── pnpm-lock.yaml
└── pnpm-workspace.yaml
└── README.md
Why is monorepo better than monolith repo?
In general, think of a monorepo as a ‘One big pizza’ : hard to share, everyone must eat the same thing.
On the other hand, a monorepo is ‘A buffet’ : multiple dishes, you pick what you need, but everything is in one place.
- Scaling – As the app grows, it becomes slow to build, test, and deploy. Scaling is very hard. A monorepo, scales better
- Modularity – Unlike monolith, each package is modular. You can change or replace one without affecting others.
- Team collaboration – In a monolith app, all teams work on the same giant app. Merge conflicts and stepping on each other’s toes are common. With monorepo, teams can own different packages/apps. Changes are isolated, but still in one repo for visibility.
- Visibility – It gives you visibility of your company’s entire codebase without a need to trackdown and clone a bunch of repos.
- Consistency – It’s consistent. Easy to share code via
packages/(e.g.,design-system,config-eslint). No need to publish to npm separately.
Examples of monorepos
- Uber – ios monorepo https://www.uber.com/en-NL/blog/ios-monorepo/
- Shopify Polaris
Why use pnpm
| Feature | npm | pnpm |
| Disk Space Usage | Installs a full copy of every package in each project’s node_modules, leading to duplication. | Uses a global content-addressable store with symlinks, so packages are stored once and reused across projects. |
| Dependency Structure | Flat node_modules can cause phantom dependencies (using a package not explicitly installed). | Strict node_modules, prevents phantom dependencies and enforces proper installs. |
| Speed | Slower installs, especially for large projects and monorepos. | Much faster installs due to symlinks and global caching. |
| Workspaces/Monorepos | Workspace support exists, but limited and less mature. | Strong, built-in workspace support designed for monorepos. |
| CI/CD Performance | Heavier lockfile (package-lock.json) and slower caching. | Smaller pnpm-lock.yaml, better caching, supports pnpm fetch for optimized Docker builds. |
Pnpm solves Phantom Dependency
A phantom dependency is a package that your project can import and use, even though it is not listed in your project’s package.json.
This happens because of the way npm (and sometimes yarn) flattens the node_modules folder.
Example
{
"dependencies": {
"package-a": "1.0.0"
}
}
Inside package-a’s package.json
{
"dependencies": {
"lodash": "4.17.21"
}
}
What happens with npm
npminstallspackage-aand also installslodash(becausepackage-adepends on it).- Due to npm’s flat
node_modules,lodashmay end up directly inside your project’snode_modules/. - Now, you can do this in your code:
import _ from 'lodash'; Even though you never declaredlodashin your ownpackage.json. pnpmuses a strictnode_modulesstructure with symlinks.- Each package only gets access to its own declared dependencies.
- If you try to import
lodashwithout listing it in yourpackage.json, it will throw an error.
Use of packages
To maintain consistency and reusability across all apps in the monorepo, we have created separate configuration packages under packages/.
These include:
config-eslint– Shared ESLint rules for linting.config-jest– Shared Jest setup for testing.config-playwright– Shared Playwright setup for end-to-end tests.config-prettier– Shared Prettier configuration for code formatting.config-tailwind– Shared TailwindCSS configuration for styling.config-typescript– Shared TypeScript configuration for type checking.
Each package has its own script. When each package does its own work, we can make things dramatically faster,
Benefits of using packages and config packages in a monorepo
- Centralized configuration – Instead of duplicating ESLint, Jest, Prettier, Tailwind, and TypeScript configs in every app, you keep them in one place (packages/config-*). Updates are made once and flow to all apps, reducing drift and inconsistency.
- Consistency across projects – All apps (apps/web, apps/api, etc.) use the same coding standards, test setup, and build configurations. Developers don’t need to guess which rules apply where, everything is uniform.
- Easier maintenance – One change updates all consumers. Example: upgrading ESLint rules only requires updating config-eslint. Less chance of forgetting to update a config in one app.
- Flexibility with overrides – Each app can still override defaults if needed.
- Scales well for large teams – Multiple teams working on different apps don’t waste time maintaining separate configs. Reduces merge conflicts in duplicated config files.
Naming conventions
The folder structure and naming conventions in this monorepo follow industry best practices inspired by popular open-source monorepos (e.g., Next.js, Shopify’s monorepos, and others). The goal is to make the repo:
- Consistent – Anyone familiar with modern React monorepos should recognize it.
- Discoverable – Folder names clearly communicate their purpose.
- Scalable – Easy to extend as the project grows.
apps
- Contains application-level code (e.g., web, docs-ui).
- Each folder here is a deployable app.
packages
- Contains shared code and configuration that can be imported across apps.
- Subfolders:
- config-eslint, config-jest, config-tailwind, etc. are config packages that enforce consistency across apps.
- design-system : a shared UI library of reusable components.
- This naming convention (config-*) makes it explicit that these are configuration-only packages.
- We use design-system as the naming convention to emphasize that this package is a centralized system of reusable components. The term “design system” communicates its purpose clearly:
- It provides consistency in look and feel across apps.
- It enforces reusability by keeping all shared components in one place.
- It makes the package easy to identify and differentiate from app-specific code or utilities.
docs
- Dedicated to documentation assets.
- Keeps developer guides, design docs, and handbook content separate from runtime code.
.husky
- Contains Git hooks managed by Husky (e.g., pre-commit checks).
- Industry standard name when Husky is used.
Design tokens
Design tokens define the foundational design values of the system, such as colors, typography, spacing, and breakpoints, and ensure a consistent look and feel across all apps in the monorepo.
Current Setup – Tailwind + CSS
We are using TailwindCSS and defining our design tokens inside a shared stylesheet:
`packages/config-tailwind/shared-styles.css`
@import 'tailwindcss';
@import 'tw-animate-css';
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
}
These tokens are exposed as CSS custom properties (–variables), which are then consumed by Tailwind and the UI layer across apps.
This approach works well with Tailwind because:
- Tokens are centralized and reusable.
- They can be combined with utility classes for consistent styling.
- Dark/light mode support can be added via variants.







