@codewithagents/openapi-gen
Types, fetch client, and Zod from your OpenAPI spec. Learn more
@codewithagents/openapi-gen
Types, fetch client, and Zod from your OpenAPI spec. Learn more
@codewithagents/openapi-react-query
React Query v5 hooks, generated. Learn more
@codewithagents/openapi-server
Typed service interface and optional router for Hono, Express, or Fastify. Learn more
@codewithagents/api-errors
Map API errors to form field errors. Learn more
Each package is independent, but they compose. A single OpenAPI spec drives everything from server contract to form field error UX.
One source of truth. When the spec changes, re-run the generators and the compiler tells you exactly what broke.
Real output from the Petstore 3.0 spec, generated by Glue and committed to the repo. This is what CI compiles on every PR.
// models.ts - auto-generated by @codewithagents/openapi-genimport type { z } from 'zod'import type { OrderSchema, CategorySchema, UserSchema, TagSchema, PetSchema, ApiResponseSchema,} from './schemas.js'
export type Order = z.infer<typeof OrderSchema>export type Category = z.infer<typeof CategorySchema>export type User = z.infer<typeof UserSchema>export type Tag = z.infer<typeof TagSchema>export type Pet = z.infer<typeof PetSchema>export type ApiResponse = z.infer<typeof ApiResponseSchema>// schemas.ts - bootstrapped by @codewithagents/openapi-gen, then yours to extend.// Re-running the generator will NOT overwrite this file.import { z } from 'zod'
export const CategorySchema = z .object({ id: z.number().optional(), name: z.string().optional(), }) .passthrough()
export const TagSchema = z .object({ id: z.number().optional(), name: z.string().optional(), }) .passthrough()
export const PetSchema = z .object({ id: z.number().optional(), name: z.string(), category: CategorySchema.optional(), photoUrls: z.array(z.string()), tags: z.array(TagSchema).optional(), status: z.enum(['available', 'pending', 'sold']).optional(), }) .passthrough()
export const OrderSchema = z .object({ id: z.number().optional(), petId: z.number().optional(), quantity: z.number().optional(), shipDate: z.string().optional(), status: z.enum(['placed', 'approved', 'delivered']).optional(), complete: z.boolean().optional(), }) .passthrough()// client.ts - auto-generated by @codewithagents/openapi-genimport type { Pet, Order } from './models.js'import { getConfig, type ClientConfig } from './client-config.js'import { z } from 'zod'import { PetSchema, OrderSchema } from './schemas.js'
export class ApiError extends Error { constructor( public readonly status: number, public readonly body: unknown ) { super(`API error ${status}`) this.name = 'ApiError' }}
export async function addPet(body: Pet, config?: Partial<ClientConfig>): Promise<Pet> { PetSchema.parse(body) const res = await _request('POST', '/pet', { body }, config) return PetSchema.parse(await res.json())}
export async function findPetsByStatus( params: { status: 'available' | 'pending' | 'sold' }, config?: Partial<ClientConfig>): Promise<Pet[]> { const searchParams = new URLSearchParams() if (params?.status != null) searchParams.set('status', String(params.status)) const res = await _request('GET', '/pet/findByStatus', { searchParams }, config) return z.array(PetSchema).parse(await res.json())}
export async function getPetById(petId: string, config?: Partial<ClientConfig>): Promise<Pet> { const res = await _request('GET', `/pet/${encodeURIComponent(petId)}`, {}, config) return PetSchema.parse(await res.json())}// hooks.ts - auto-generated by @codewithagents/openapi-react-queryimport { useQuery, type UseQueryOptions, useMutation } from '@tanstack/react-query'import { addPet, findPetsByStatus, getPetById, type ApiError } from './client.js'
export const petKeys = { all: () => ['pet'] as const, findPetsByStatus: (params: Parameters<typeof findPetsByStatus>[0]) => ['pet', 'findPetsByStatus', params] as const, detail: (petId: string) => ['pet', petId] as const,}
export function useFindPetsByStatus( params: Parameters<typeof findPetsByStatus>[0], options?: Omit< UseQueryOptions<Awaited<ReturnType<typeof findPetsByStatus>>, ApiError>, 'queryKey' | 'queryFn' >) { return useQuery<Awaited<ReturnType<typeof findPetsByStatus>>, ApiError>({ queryKey: petKeys.findPetsByStatus(params), queryFn: () => findPetsByStatus(params), staleTime: 0, gcTime: 300000, ...options, })}
// Path-param hooks set enabled: false when id is nullish, no boilerplate at call sitesexport function useGetPetById( petId: string | undefined | null, options?: Omit< UseQueryOptions<Awaited<ReturnType<typeof getPetById>>, ApiError>, 'queryKey' | 'queryFn' >) { return useQuery<Awaited<ReturnType<typeof getPetById>>, ApiError>({ queryKey: petKeys.detail(petId!), queryFn: () => getPetById(petId!), staleTime: 0, gcTime: 300000, enabled: petId != null && (options?.enabled ?? true), ...options, })}// service.ts, auto-generated by @codewithagents/openapi-serverimport type { ApiResponse, Order, Pet, User } from './models.js'
export interface SwaggerPetstoreOpenAPI30Service { /** POST /pet */ addPet(body: Pet): Promise<Pet> /** PUT /pet */ updatePet(body: Pet): Promise<Pet> /** GET /pet/findByStatus */ findPetsByStatus(params: { status: string }): Promise<Pet[]> /** GET /pet/{petId} */ getPetById(petId: string): Promise<Pet> /** DELETE /pet/{petId} */ deletePet(petId: string): Promise<void> /** POST /store/order */ placeOrder(body: Order): Promise<Order> /** GET /store/order/{orderId} */ getOrderById(orderId: string): Promise<Order> /** POST /user */ createUser(body: User): Promise<User> /** GET /user/{username} */ getUserByName(username: string): Promise<User> /** PUT /user/{username} */ updateUser(username: string, body: User): Promise<void> /** DELETE /user/{username} */ deleteUser(username: string): Promise<void>}Install the generator(s) you need.
For the full pipeline (types, Zod, fetch client, React Query hooks):
npm install -D @codewithagents/openapi-gen @codewithagents/openapi-react-querynpm install @tanstack/react-queryOr just the core generator for types and a fetch client:
npm install -D @codewithagents/openapi-genCreate a config file in your project root.
{ "input_openapi": "./openapi.json", "output": "./src/api"}Point input_openapi at your spec file (JSON or YAML, local path or URL). The output directory is created automatically.
Run the generator and import the output.
npx openapi-genThen use the generated barrel directly:
import { configureClient, getPetById, useFindPetsByStatus } from './src/api'
// Call once at startupconfigureClient({ baseUrl: 'https://petstore3.swagger.io/api/v3' })
// Typed fetch functionconst pet = await getPetById('1')
// Or use the generated React Query hookconst { data, isLoading } = useFindPetsByStatus({ status: 'available' })128 real-world specs tested on every PR
Stripe, GitHub, Spotify, OpenAI, AWS, Adyen, and more. Every spec is generated and compiled in CI. Not benchmarked once and forgotten.
OpenAPI 3.1 first-class
Full support for $ref, allOf, anyOf, oneOf, nullable, and 3.1.1 keyword changes.
OpenAPI 3.0.x is best-effort. No OpenAPI 2.0/Swagger.
Validated end-to-end
Zod schemas are bootstrapped from your spec and yours to extend. The fetch client validates request and response bodies at runtime against those schemas, not just types.
Zero runtime wrapper deps
The generated client uses only native fetch. No axios, no wrapper libraries, nothing added to
your bundle by the codegen packages themselves.
ESM, TypeScript strict mode
Every generated file compiles with strict: true and "type": "module". Output passes
prettier --check with your own Prettier config.
Framework-agnostic
The service interface has no framework imports. Wire it to Hono, Express, Fastify, or any
router. The client works in any environment with fetch.
Quickstart
Go from a spec file to a working typed client in five minutes. Read the quickstart
GitHub
Source code, issues, and the compatibility matrix across 128 specs. View on GitHub