Silgi
Best Practices

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

src/index.ts
package.json
src/server.ts
package.json
src/client.ts
package.json
package.json
pnpm-workspace.yaml

Three packages, each with a single responsibility:

PackageResponsibilityDependencies
api-contractShared types and schemassilgi, zod
api-serverImplements the contractapi-contract, silgi, zod
frontendConsumes the APIapi-contract, silgi

1. Define the contract

The contract package exports input/output schemas and the router type. No server logic lives here.

packages/api-contract/src/index.ts
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.

packages/api-server/src/server.ts
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.

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

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

On this page