Best Practices
Monorepo Setup
Share types between frontend and backend in a monorepo — pure RPC, no route metadata.
Silgi works like tRPC — the client uses the router's tree structure to call procedures directly. You only need to share the router type between packages. No route metadata, no JSON files, no build-time extraction.
Project structure
src/rpc.ts
src/router.ts
src/server.ts
package.json
src/client.ts
package.json
package.json
pnpm-workspace.yaml
Two packages. The server defines procedures and exports the router type. The frontend imports only the type.
| Package | Responsibility | Dependencies |
|---|---|---|
api-server | Procedures and router | silgi, zod |
frontend | Consumes the API | silgi (client only) |
1. Define the server
import { silgi } from 'silgi'
export const s = silgi({
context: (req) => ({
db: getDB(),
headers: Object.fromEntries(req.headers),
}),
})import { z } from 'zod'
import { s } from './rpc'
const listUsers = s
.$input(z.object({ limit: z.number().min(1).max(100).optional() }))
.$resolve(({ input, ctx }) => ctx.db.users.findMany({ take: input.limit ?? 10 }))
const createUser = s
.$input(z.object({ name: z.string().min(1), email: z.string().email() }))
.$errors({ CONFLICT: 409 })
.$resolve(({ input, ctx, fail }) => {
if (ctx.db.users.exists(input.email)) fail('CONFLICT')
return ctx.db.users.create(input)
})
export const appRouter = s.router({
users: {
list: listUsers,
create: createUser,
},
})
// Export the type — this is all the frontend needs
export type AppRouter = typeof appRouterimport { s } from './rpc'
import { appRouter } from './router'
s.serve(appRouter, { port: 3000, scalar: true })2. Consume on the frontend
The frontend imports only the router type. No server code, no route metadata — just the type for autocomplete and type checking.
import { createClient } from 'silgi/client'
import { createLink } from 'silgi/client/ofetch'
import type { AppRouter } from 'api-server/src/router' // type-only, erased at build
const link = createLink({
url: 'http://localhost:3000',
})
const client = createClient<AppRouter>(link)
// Full autocomplete and type checking — calls use POST + tree path
const users = await client.users.list({ limit: 10 })
// → POST /users/listUse import type for the router. This ensures the import is erased at build time — no server code is bundled into the
frontend.
Workspace configuration
packages:
- 'packages/*'{
"name": "api-server",
"dependencies": {
"silgi": "latest",
"zod": "latest"
}
}{
"name": "frontend",
"dependencies": {
"silgi": "latest"
}
}What's next?
- Client — client setup with links, interceptors, and binary mode
- Testing — test procedures with the server client
- SSR Optimization — prefetch data server-side