Silgi
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.

PackageResponsibilityDependencies
api-serverProcedures and routersilgi, zod
frontendConsumes the APIsilgi (client only)

1. Define the server

packages/api-server/src/rpc.ts
import { silgi } from 'silgi'

export const s = silgi({
  context: (req) => ({
    db: getDB(),
    headers: Object.fromEntries(req.headers),
  }),
})
packages/api-server/src/router.ts
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 appRouter
packages/api-server/src/server.ts
import { 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.

packages/frontend/src/client.ts
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/list

Use import type for the router. This ensures the import is erased at build time — no server code is bundled into the frontend.

Workspace configuration

pnpm-workspace.yaml
packages:
  - 'packages/*'
packages/api-server/package.json
{
  "name": "api-server",
  "dependencies": {
    "silgi": "latest",
    "zod": "latest"
  }
}
packages/frontend/package.json
{
  "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

On this page