packages/shared

Where types + schemas live.

5 mineasy

packages/shared is the glue. Zod schemas, generated TS types, route constants — anything web and admin both need. One source of truth; the two apps stay aligned forever.

What lives in it

packages/shared/src/
packages/shared/src/
ā”œā”€ā”€ schemas/ Zod schemas — both apps validate the same way
│ ā”œā”€ā”€ user.ts
│ ā”œā”€ā”€ auth.ts
│ └── product.ts
ā”œā”€ā”€ types/ TypeScript types — generated by grit sync
│ ā”œā”€ā”€ user.ts from internal/models/user.go
│ ā”œā”€ā”€ product.ts from internal/models/product.go
│ └── index.ts
ā”œā”€ā”€ constants/ shared constants
│ ā”œā”€ā”€ routes.ts API path constants used by both apps
│ ā”œā”€ā”€ roles.ts UserRole = "user" | "staff" | "admin"
│ └── plans.ts
└── index.ts re-exports everything

Importing into web / admin

apps/web/components/login-form.tsx
import { LoginSchema } from '@workspace/shared/schemas/auth'
import type { User } from '@workspace/shared/types/user'
import { API_ROUTES } from '@workspace/shared/constants/routes'
// LoginSchema validates the form input.
// User typed the response.
// API_ROUTES gives '/api/auth/login' as a constant — no typos.

Importing from @workspace/shared uses pnpm's workspace symlinks. TypeScript follows the symlink and resolves types correctly. No build step needed.

The grit sync round-trip

Reminder from earlier courses: when you change a Go struct, run grit sync to regenerate packages/shared/src/types/. Both apps pick up the new types instantly.

Terminal
# After editing apps/api/internal/models/user.go
$grit sync
# Now web + admin both see the new field

Why not just publish to npm?

You could. But the workspace pattern means changes propagate instantly without a publish cycle. Edit a schema; both apps see it on next reload. For internal types this is ideal.

When you graduate to public types (SDK for third parties), THEN publish to npm — but for internal monorepo work, workspaces win.

Constants beat magic strings. Every API path lives in constants/routes.ts. API_ROUTES.AUTH_LOGIN beats '/api/auth/login' sprinkled across 12 files. Rename once, references update.

What NOT to put in shared

  • React components — they belong in apps/web/components/ or apps/admin/components/ (or a separate packages/ui if you want a design system).
  • App-specific business logic — that should stay in the relevant app.
  • Anything that imports from apps/* — would create circular dependencies.

Quick check

You need a `formatCurrency()` helper used by web (price display) AND admin (totals). Where does it go?

Try it

Add a formatCurrency() helper to packages/shared:

  1. Create packages/shared/src/utils/currency.ts exporting formatCurrency(amount: number): string.
  2. Import it into apps/web/app/(app)/dashboard/page.tsx and use it to render a price.
  3. Import the same function into one admin page.
  4. In notes.md, paste both import statements + where you used it.

That completes chapter 1's assignment.

What's next

Chapter 2 — The Public Site. Landing page, marketing pages, SEO + Open Graph. The customer-facing surface.

Spot a typo? Have an idea?

Help us improve this lesson. One click opens a GitHub issue with the lesson URL pre-filled — suggest clearer wording, report a bug, or request more depth. The course keeps improving thanks to learners like you.

Suggest an improvement on GitHub