Create UI package structure
You have 6 duplicate card divs in the features page. Before extracting them into a shared component, you need a place to put that component. You'll create packages/ui - a workspace package that apps can import from.
Outcome
Create the packages/ui workspace package with proper TypeScript configuration, ready to hold shared React components.
Fast track
- Create
packages/ui/srcdirectory structure - Configure
package.jsonwith named exports pattern - Add TypeScript config extending base settings
- Link the workspace with
pnpm install
Hands-on exercise 2.2
Set up the packages/ui package structure and configuration for shared React components.
Requirements:
- Create
packages/ui/srcdirectory - Create
package.jsonwith named exports pattern and peer dependencies - Add
tsconfig.jsonextending from base TypeScript config - Create empty
src/index.tsexport file - Run
pnpm installto link the workspace
Implementation hints:
- Use
@geniusgarage/uias package name (scoped for consistency) - Configure named exports pattern:
"./card": "./src/card.tsx" - React should be a peerDependency (apps provide it, package uses it)
- TypeScript config should extend from
@tsconfig/nextjsfor consistency - Empty index.ts now, components added in next lesson
Files to create:
packages/ui/package.json(workspace package config)packages/ui/tsconfig.json(TypeScript settings)packages/ui/src/index.ts(empty export file)
Create package directory
Create the packages directory and ui package:
mkdir -p packages/ui/srcYour monorepo now has:
production-monorepos-starter/
├── apps/
│ └── web/
├── packages/ # ← New!
│ └── ui/ # ← New!
│ └── src/ # ← New!
├── package.json
└── pnpm-workspace.yaml
The pnpm-workspace.yaml already includes packages/*, so pnpm will automatically detect this as a workspace package.
Configure package.json
Create packages/ui/package.json with this configuration:
{
"name": "@geniusgarage/ui",
"version": "1.0.0",
"main": "./src/index.ts",
"types": "./src/index.ts",
"exports": {},
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
},
"devDependencies": {
"@types/react": "^19",
"@types/react-dom": "^19",
"typescript": "^5"
}
}Breaking it down:
name: @geniusgarage/ui
- Namespaced package name
- Apps will import from
@geniusgarage/ui/component-name
exports: Empty object for now
- You'll add named exports as you create components
- Modern pattern: each component gets its own export path
- Example:
"./card": "./src/card.tsx"lets apps import@geniusgarage/ui/card
peerDependencies: React versions
- Apps provide React, not the package
- Prevents multiple React instances
- Supports both React 18 and 19
devDependencies: TypeScript and React types
- Needed for TypeScript to compile JSX components
- Only used during development/build
- Not shipped to consuming apps
Modern packages use named exports:
Named export (what we're doing):
import { Card } from '@geniusgarage/ui/card'Barrel export (old pattern):
import { Card } from '@geniusgarage/ui'Named exports are better for:
- Tree-shaking (only bundle what you import)
- Avoiding circular dependencies
- Explicit imports (you know exactly where components come from)
Add TypeScript config
Create packages/ui/tsconfig.json:
{
"compilerOptions": {
"jsx": "react-jsx",
"target": "ES2017",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"declaration": true
},
"include": ["src"],
"exclude": ["node_modules"]
}Key settings:
jsx: "react-jsx"- Modern JSX transform (no need to import React in every file)declaration: true- Generate.d.tstype definition filesmoduleResolution: "bundler"- Modern resolution for build tools like Next.js
Create index file (empty)
Create packages/ui/src/index.ts:
// Components will be exported from hereThis file will export all components. Right now it's empty. You'll add exports as you create components.
Register the new package
Tell pnpm to scan the workspace and recognize the new package:
pnpm installYou'll see:
Scope: all 3 workspace projects
Already up to date
Even though no external packages were installed, pnpm scanned your packages/ directory and registered @geniusgarage/ui as a workspace package. It's now available to import from other apps in the monorepo.
Verify the package is recognized:
pnpm list --depth 0You should see @geniusgarage/ui listed alongside @geniusgarage/web.
You now have a properly configured UI package:
- Namespaced package name (
@geniusgarage/ui) - Named exports configuration (ready to add components)
- TypeScript setup
- Linked in the workspace
Next lesson: extract the Card component into this package.
Commit
Save your work:
git add .
git commit -m "feat(ui): create ui package structure"Done-when
Verify your package structure:
- Directory
packages/ui/srcexists packages/ui/package.jsonexists with name@geniusgarage/ui- package.json has
exportsfield configured - package.json has React as peerDependency (not dependency)
packages/ui/tsconfig.jsonexists and extends@tsconfig/nextjspackages/ui/src/index.tsexists (empty for now)- Ran
pnpm installsuccessfully pnpm -r exec pwdshows both web and ui packages- No TypeScript or build errors
- Ready to add components in next lesson
What you built
You created the foundation for shared components:
packages/ui/
├── src/
│ └── index.ts # Empty exports file
├── package.json # Package config with named exports
└── tsconfig.json # TypeScript config
No components yet - just the structure. This separation is intentional: understand the package setup before adding components.
What's Next
Next lesson: Extract Card Component - you'll create packages/ui/src/card.tsx, add it to exports, and update the features page to import from the shared package. You'll see workspace dependencies in action - change the Card once, all apps using it update instantly.
Was this helpful?