packages/shared
Where types + schemas live.
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/āāā 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
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.
# 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/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/orapps/admin/components/(or a separatepackages/uiif 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
Try it
Add a formatCurrency() helper to packages/shared:
- Create
packages/shared/src/utils/currency.tsexportingformatCurrency(amount: number): string. - Import it into
apps/web/app/(app)/dashboard/page.tsxand use it to render a price. - Import the same function into one admin page.
- 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