---
title: "Import an OpenAPI spec in TypeScript"
description: "OpenAPI describes your API, but the types usually come from a code generator. With openapi-import and TypeScript 7.1, you import the spec like any other file and get the types without generated files."
canonical_url: "https://stijnvanhulle.be/blog/import-openapi-specs-in-typescript"
---

> Index of every page on this site: [llms.txt](https://stijnvanhulle.be/llms.txt)

An [OpenAPI](https://www.openapis.org/) spec already describes your whole API. Yet before TypeScript knows any of it, most projects run a generator and commit a folder of output. [`openapi-import`](https://stijnvanhulle.be/projects/openapi-import) drops that step. You import the spec, and TypeScript reads the types from it:

```ts
import type { Pet } from './petStore.openapi.yaml'
```

## What OpenAPI gives you

An OpenAPI document is a YAML or JSON file with three parts that matter for a front end:

- `paths`: the endpoints, their methods, parameters, and responses.
- `components.schemas`: the data shapes, such as `Pet` or `Order`.
- `servers`: the base URLs.

```yaml [petStore.openapi.yaml]
openapi: 3.1.0
info:
  title: Pet Store
  version: 1.0.0
paths:
  /pets/{petId}:
    get:
      operationId: getPetById
      parameters:
        - name: petId
          in: path
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: A pet
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Pet'
components:
  schemas:
    Pet:
      type: object
      required: [id, name]
      properties:
        id:
          type: integer
        name:
          type: string
        status:
          type: string
          enum: [available, pending, sold]
```

Back end and front end both build against this file. If your types drift from it, you find out at run time, usually from a bug report.

## Why generated files get in the way

Tools such as [Kubb](https://stijnvanhulle.be/projects/kubb) turn a spec into TypeScript types, Zod schemas, and clients. I have built Kubb around that idea for years. It works, but the output is a second copy of the spec, and copies drift:

- You run the generator every time the spec changes, or the types go stale.
- You choose between committing the files and generating them in CI.
- Go to definition lands in generated code, not in the spec.

## Import the spec instead

TypeScript 7.1 adds content mappers. A content mapper is a package that tells the compiler how to read a file type it does not know. `openapi-import` is a content mapper for OpenAPI. Install it with Kubb and the TypeScript beta:

```bash [Terminal]
pnpm add -D kubb openapi-import typescript@beta
```

Name your spec `<name>.openapi.yaml` and register that extension in `tsconfig.json`:

```json [tsconfig.json]
{
  "contentMappers": [
    {
      "package": "openapi-import",
      "extensions": [".openapi.yaml", ".openapi.yml"]
    }
  ]
}
```

Now the spec is a module. Import a schema type and use it:

```ts [pets.ts]
import type { Pet } from './petStore.openapi.yaml'

const pet: Pet = {
  id: 1,
  name: 'Rex',
  status: 'lost', // Type '"lost"' is not assignable to type '"available" | "pending" | "sold"'.
}
```

The editor autocompletes `status` from the enum in the spec and flags `'lost'` as an error. Hover shows the `Pet` type, and go to definition jumps to `Pet` in the YAML. Rename the type and the key in the spec follows. That last one still surprises me every time.

## Add Zod schemas

Without a config, `openapi-import` gives you the types from Kubb's `pluginTs`. Add a `kubb.config.ts` to get the output of more plugins, such as Zod schemas, from the same import:

```ts [kubb.config.ts]
import { defineConfig } from 'kubb/config'
import { pluginTs } from '@kubb/plugin-ts'
import { pluginZod } from '@kubb/plugin-zod'

export default defineConfig({
  output: { path: './src/gen' },
  plugins: [pluginTs(), pluginZod()],
})
```

```ts [pets.ts]
import { type Pet, petSchema } from './petStore.openapi.yaml'

const response = await fetch('https://petstore3.swagger.io/api/v3/pet/1')
const pet: Pet = petSchema.parse(await response.json())
```

The types come from the content mapper. The code that runs comes from [`unplugin-kubb`](https://www.npmjs.com/package/unplugin-kubb), which loads the same module in Vite, webpack, Rollup, and other bundlers.

## Check it in CI

The editor and the build use the same compiler, so CI reports the same errors you see while typing:

```bash [Terminal]
tsc --runExternalCode
```

When someone changes the spec, every file that no longer matches it fails the type check, before the code ships.

## Get started

Install `openapi-import`, add one entry to `contentMappers`, and import your `*.openapi.yaml` spec. The [project page](https://stijnvanhulle.be/projects/openapi-import) has a demo, and the [repository](https://github.com/kubb-labs/openapi-import) takes issues and feedback.

## Sitemap

See the full [sitemap](https://stijnvanhulle.be/sitemap.md) for all pages.
