Monorepo Setup
Share types between frontend and backend in a monorepo.
The contract-first pattern lets your frontend import types from a shared package without pulling in any server code.
Project structure
Three packages, each with a single responsibility:
| Package | Responsibility | Dependencies |
|---|---|---|
api-contract | Shared types and schemas | silgi, zod |
api-server | Implements the contract | api-contract, silgi, zod |
frontend | Consumes the API | api-contract, silgi |
1. Define the contract
The contract package exports input/output schemas and the router type. No server logic lives here.
import { } from "silgi"
import { } from "zod"
const = ()
// Input schemas — shared between server and client
export const = .({
: .().(1),
: .().(),
})
export const = .({
: .().(1).(100).(),
})
// Procedure definitions — schemas only, no implementation
const = .().(() => {} as any)
const = .().(() => {} as any)
export const = .({
: { : , : },
})
// The type the frontend imports
export type = typeof The contract uses placeholder resolvers (() => {} as any). They exist only for type inference — the actual implementation lives in api-server.
2. Implement on the server
The server package imports the schemas from the contract and provides real implementations.
import { } from 'silgi'
import { , } from 'api-contract'
const = ({
: () => ({
: getDB(),
: .(.),
}),
})
const = .().(({ , }) => ..users.findMany({ : .limit ?? 10 }))
const = k
.$use(auth)
.$input()
.$resolve(({ , }) => .db.users.create())
const = .({
: { : , : },
})
.(, { : 3000 })3. Consume on the frontend
The frontend imports only the AppRouter type from the contract. No server code leaks into the client bundle.
import { } from 'silgi/client'
import { } from 'silgi/client/ofetch'
import type { } from 'api-contract'
const = ({ : 'http://localhost:3000' })
const = <>()
// Full autocomplete and type checking
const = await .users.list({ : 10 })Use import type for the router. This ensures the import is erased at build time and no server code is bundled into
the frontend.
InferContractClient
For stricter typing without importing the full router, use InferContractClient:
import type { } from 'silgi'
import type { } from 'api-contract'
type = <>
// Client.users.list: (input: { limit?: number }) => Promise<User[]>
// Client.users.create: (input: { name: string; email: string }) => Promise<User>This is useful for typing props, return values, or utility functions that work with the client.
Workspace configuration
packages:
- 'packages/*'{
"name": "api-server",
"dependencies": {
"api-contract": "workspace:*",
"silgi": "latest",
"zod": "latest"
}
}{
"name": "frontend",
"dependencies": {
"api-contract": "workspace:*",
"silgi": "latest"
}
}What's next?
- Client — full client setup with links, interceptors, and binary mode
- Testing — test procedures from the shared contract
- SSR Optimization — prefetch data server-side to avoid waterfalls