Silgi

Code Generation

Generate Silgi routes, schemas, and a ready-to-implement server from any OpenAPI 3.x spec — with Zod, Valibot, or ArkType.

Silgi can generate a fully typed server from an OpenAPI specification. Pass a spec, get route files with $route, $input, $output, $errors, and an inline $resolve stub — one file per operation, grouped by domain. Re-running the generator preserves your implementations while updating metadata from the spec.

Quick start

Generate from spec

generate.ts
import { generateFromSpec } from 'silgi/codegen'

await generateFromSpec({
  spec: './openapi.json',
  outDir: './src/api',
})

Run it:

npx tsx generate.ts

Implement your resolvers

Open any route file and replace the // TODO stub:

src/api/routes/users/listUsers.ts
export const listUsers = s
  .$route({
    path: '/users',
    method: 'GET',
    summary: 'List all users',
    tags: ['users'],
    operationId: 'listUsers',
  })
  .$input(schemas.listUsersInputSchema)
  .$output(schemas.listUsersOutputSchema)
  .$resolve(async ({ input, ctx }) => {
    return ctx.db.users.findMany({
      take: input.limit ?? 20,
    })
  })

Use the router

src/server.ts
import { router } from './api/router.gen.ts'
import { silgi } from 'silgi'

const s = silgi({
  context: (req) => ({ db: getDB() }),
})

s.serve(router, { port: 3000, scalar: true })

Output structure

schemas.gen.ts
router.gen.ts
listUsers.ts
createUser.ts
getUser.ts
getInventory.ts
placeOrder.ts
FileRegenerated?Description
schemas.gen.tsAlwaysComponent + operation schemas (Zod/Valibot/ArkType)
router.gen.tsAlwaysRoot router — imports and combines all operations
routes/<group>/<op>.tsSmartOne file per operation with inline $resolve

Schema targets

The generator supports any Standard Schema compatible library:

await generateFromSpec({
  spec: './openapi.json',
  outDir: './src/api',
  schema: 'zod',
})
schemas.gen.ts
import { z } from 'zod'

export const UserSchema = z.object({
  id: z.string().uuid(),
  name: z.string().min(1).max(100),
  email: z.string().email().optional(),
})
await generateFromSpec({
  spec: './openapi.json',
  outDir: './src/api',
  schema: 'valibot',
})
schemas.gen.ts
import * as v from 'valibot'

export const UserSchema = v.object({
  id: v.pipe(v.string(), v.uuid()),
  name: v.pipe(v.string(), v.minValue(1), v.maxValue(100)),
  email: v.optional(v.pipe(v.string(), v.email())),
})
await generateFromSpec({
  spec: './openapi.json',
  outDir: './src/api',
  schema: 'arktype',
})
schemas.gen.ts
import { type } from 'arktype'

export const UserSchema = type({
  ['id']: type('string.uuid'),
  ['name']: type('string').atLeast(1).atMost(100),
  ['email?']: type('string.email'),
})

Incremental generation

The default smart strategy uses oxc-parser to parse existing route files, extract your $resolve implementation by AST, and preserve it while regenerating everything else.

1. Generate from spec v1         → stub files
2. Implement $resolve bodies     → your code
3. Spec changes (v2)             → re-run codegen
4. Result:
   ✅ $route metadata updated    (path, method, summary, tags)
   ✅ $input/$output updated     (new fields from spec)
   ✅ $errors updated            (new error codes)
   ✅ $resolve preserved         (your implementation kept)
   ✅ New operations              (generated as stubs)

If a $resolve body still contains Not implemented, it's treated as a stub and overwritten with a fresh stub. Only real implementations are preserved.

Route strategies

StrategyBehavior
smart (default)Preserve $resolve body, regenerate metadata
skipNever touch existing route files
overwriteRegenerate everything, discard implementations
await generateFromSpec({
  spec: './openapi.json',
  outDir: './src/api',
  routeStrategy: 'smart',
})

Options

await generateFromSpec({
  // Required
  spec: './openapi.json', // path or parsed object

  // Output
  outDir: './src/api', // default: './generated'
  schema: 'zod', // 'zod' | 'valibot' | 'arktype'

  // Grouping
  groupBy: 'tag', // 'tag' | 'path' | 'flat'

  // Regeneration
  routeStrategy: 'smart', // 'smart' | 'skip' | 'overwrite'

  // Customization
  instanceName: 's', // Silgi instance variable name
  header: '// Auto-generated', // Comment at top of each file
})
OptionDefaultDescription
specPath to OpenAPI JSON/YAML file, or a parsed spec object
outDir./generatedOutput directory
schemazodTarget validation library
groupBytagHow to organize route folders: by OpenAPI tag, first URL segment, or flat
routeStrategysmartHow to handle existing route files on re-generation
instanceNamesVariable name for the silgi() instance in generated code

Programmatic API

Use generate() for in-memory generation without writing files:

import { generate } from 'silgi/codegen'
import spec from './openapi.json'

const { schemas, router, routes, operations } = generate(spec, {
  schema: 'valibot',
  groupBy: 'tag',
})

// schemas: string   — schemas.gen.ts content
// router: string    — router.gen.ts content
// routes: Map<string, string>  — 'group/operationId' → file content
// operations: ParsedOperation[]

YAML support

YAML specs are supported if the yaml package is installed:

bash pnpm add -D yaml
bash npm install -D yaml
bash bun add -D yaml

Then pass a .yaml or .yml path:

await generateFromSpec({
  spec: './openapi.yaml',
  outDir: './src/api',
})

What's next?

  • OpenAPI — generate an OpenAPI spec from your router (the reverse direction)
  • Procedures$input(), $output(), $errors(), $route() API
  • Server — serve your router with Scalar UI

On this page