C

Create Multi-Module Project

1.Why a multi-module project?

Throughout this course we will build many small React projects — one per lesson — to demonstrate different concepts in isolation. Instead of keeping each project as a standalone repository with its own node_modules and its own copy of every dependency, we will organize them as a monorepo using Bun workspaces.

A multi-module setup gives us several benefits:

  • Single source of truth for dependency versionsreact, vite, typescript and all other tools are pinned once in the root package.json. Every child project consumes the same version, so lessons stay consistent with each other.
  • One shared node_modules — Bun hoists dependencies to the workspace root, so installing a new lesson project does not download the same packages again.
  • A single bun install — running install at the root installs dependencies for every project in the workspace at once.
  • Easy cross-project references — later lessons can depend on code from earlier lessons by name, without publishing anything to a registry.

2.What is a Bun workspace?

A Bun workspace is a set of related packages managed from a single root package.json. The root declares which sub-directories are members of the workspace via the workspaces field:

{
"workspaces": ["*"]
}

The "*" pattern means “every direct subdirectory that contains a package.json is a workspace member”. Each member keeps its own package.json, but all dependencies are resolved and deduplicated from the root.

When a child package lists a dependency with version "*", Bun resolves that dependency from the version declared at the root. This is how we keep versions consistent across projects.

3.Convert the project

We will now convert the single react-project created in the previous lesson into a workspace member of a parent project.

Create the root `package.json`

In the parent directory that contains react-project/, create a new package.json. It declares the workspace and holds the concrete versions of every dependency shared by child projects.

package.json
{
"name": "react-course",
"private": true,
"workspaces": ["*"],
"dependencies": {
"react": "^19.2.4",
"react-dom": "^19.2.4"
},
"devDependencies": {
"@eslint/js": "^9.39.4",
"@types/node": "^24.12.0",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.1",
"eslint": "^9.39.4",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.5.2",
"globals": "^17.4.0",
"typescript": "~5.9.3",
"typescript-eslint": "^8.57.0",
"vite": "^8.0.1"
}
}

Key points:

  • "private": true — the workspace root is never published to a registry.
  • "workspaces": ["*"] — every direct subdirectory with a package.json (currently just react-project) becomes a workspace member.
  • dependencies and devDependencies — the single source of truth for versions. Moving them here from the child package.json means every child project will use the exact same version.

Update the child `package.json`

In react-project/package.json, replace every concrete dependency version with "*". This tells Bun to resolve the dependency from the workspace root rather than from the npm registry.

package.json
{
"name": "react-project",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"react": "*",
"react-dom": "*"
},
"devDependencies": {
"@eslint/js": "*",
"@types/node": "*",
"@types/react": "*",
"@types/react-dom": "*",
"@vitejs/plugin-react": "*",
"eslint": "*",
"eslint-plugin-react-hooks": "*",
"eslint-plugin-react-refresh": "*",
"globals": "*",
"typescript": "*",
"typescript-eslint": "*",
"vite": "*"
}
}

Notice that:

  • "name", "version", "type" and "scripts" stay in the child project — each project still has its own identity and its own build/dev commands.
  • All dependency versions are now "*", which resolves to the version declared at the workspace root.

Install dependencies from the root

Delete the node_modules/ directory and bun.lock file inside react-project/ (if they exist from the previous lesson), then run bun install from the parent directory:

Terminal window
rm -rf react-project/node_modules react-project/bun.lock
bun install

Bun will:

  • Create a single node_modules/ at the workspace root with all hoisted dependencies.
  • Create a single bun.lock at the root that locks versions for every workspace member.
  • Link react-project into the workspace so it can import its dependencies normally.

Run the project

Child project scripts still work exactly the same. From inside react-project/:

Terminal window
cd react-project
bun run dev

You can also run a child script from the root using Bun’s --filter flag, which is convenient when you have multiple projects:

Terminal window
bun --filter react-project dev

4.Resulting structure

After the conversion the directory layout looks like this:

react/
├── package.json # workspace root (pinned versions, "workspaces": ["*"])
├── bun.lock # single lockfile for the whole workspace
├── node_modules/ # hoisted dependencies shared by all members
└── react-project/
├── package.json # workspace member ("*" versions)
├── vite.config.ts
├── tsconfig.json
└── src/

From now on, every new lesson project we create will be added as another subdirectory next to react-project/, and it will automatically join the workspace — no extra configuration required.