# Typespec Openapi

> Write TypeSpec specifications that compile to OpenAPI 3.x. Use when the user wants to define APIs using TypeSpec (.tsp files), scaffold a TypeSpec project, add models/operations/routes, or generate OpenAPI output. Trigger phrases include "write typespec", "create typespec", "typespec spec", "typespec for", "define API with typespec", "typespec openapi".

- Skill: `g-kartik/typespec-openapi` (Agent Skill)
- Install (CLI): `npx skillmds@latest add g-kartik/typespec-openapi`
- Raw SKILL.md: https://api.skillmd.com/api/skills/g-kartik/typespec-openapi/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- Author: g-kartik (https://skillmd.com/u/g-kartik)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/g-kartik/typespec-openapi

---


You are writing TypeSpec specifications that compile to OpenAPI 3.x using `@typespec/openapi3`.

## TypeSpec Fundamentals

TypeSpec is a language for describing APIs. Files use the `.tsp` extension. The compiler emits OpenAPI (and other formats) via emitters configured in `tspconfig.yaml`.

### Core imports

```typespec
import "@typespec/http";
import "@typespec/rest";

using TypeSpec.Http;
using TypeSpec.Rest;
```

Emitters (`@typespec/openapi3`) are never imported in `.tsp` files — they are declared only in `tspconfig.yaml`.

### Project structure

```
<project>/
├── package.json         # npm deps for the compiler and emitters
├── tspconfig.yaml       # compiler + emitter config
├── main.tsp             # entrypoint (imports all others)
├── models/
│   └── *.tsp            # data models
└── routes/
    └── *.tsp            # operations grouped by resource
```

### package.json

```json
{
  "devDependencies": {
    "@typespec/compiler": "latest",
    "@typespec/http": "latest",
    "@typespec/rest": "latest",
    "@typespec/openapi3": "latest"
  }
}
```

Run `npm install` before compiling.

### tspconfig.yaml

```yaml
emit:
  - "@typespec/openapi3"
options:
  "@typespec/openapi3":
    output-file: openapi.yaml
```

## Patterns

### Namespace and service

```typespec
@service({ title: "My API" })
@server("https://api.example.com", "Production")
namespace MyApi;
```

For versioned APIs, add `@typespec/versioning` to `package.json` and use `@versioned(Versions)` + a `enum Versions` — `@service` does not accept a `version` property.

### Models

```typespec
model Widget {
  id: string;
  name: string;
  count: int32;
  createdAt: utcDateTime;
  tags?: string[];
}

model WidgetCreate {
  name: string;
  count?: int32;
  tags?: string[];
}

// For PATCH: all fields optional
model WidgetUpdate {
  name?: string;
  count?: int32;
  tags?: string[];
}
```

Numeric constraints (`@minValue`, `@maxValue`) require `import "@typespec/http"` and `using TypeSpec.Http` — confirm the decorator is in scope before using it.

### Standard CRUD interface

```typespec
@route("/widgets")
interface Widgets {
  @get list(): Widget[];
  @get @route("{id}") read(@path id: string): Widget | NotFoundResponse;
  @post create(@body body: WidgetCreate): CreatedResponse & Widget;
  @patch @route("{id}") update(@path id: string, @body body: WidgetUpdate): Widget | NotFoundResponse;
  @delete @route("{id}") delete(@path id: string): NoContentResponse | NotFoundResponse;
}
```

Use `NoContentResponse` (204) for successful deletes, not `void`. Use `CreatedResponse` (201) for successful creates. Both are built-ins from `TypeSpec.Http`.

### Error responses

```typespec
@error
model ApiError {
  code: string;
  message: string;
}

@error
model NotFoundError extends ApiError {
  @statusCode _: 404;
}

alias NotFoundResponse = NotFoundError;
```

Always use a named `@error` model rather than an anonymous model literal in an alias — the named model produces a proper schema component in the OpenAPI output.

### Authentication

```typespec
// Bearer token
@useAuth(BearerAuth)
namespace MyApi;

// API key
@useAuth(ApiKeyAuth<ApiKeyLocation.header, "X-API-Key">)
namespace MyApi;

// OAuth2
@useAuth(OAuth2Auth<[{
  type: OAuth2FlowType.authorizationCode;
  authorizationUrl: "https://auth.example.com/oauth/authorize";
  tokenUrl: "https://auth.example.com/oauth/token";
  scopes: ["read", "write"];
}]>)
namespace MyApi;
```

### Documentation decorators

```typespec
@doc("Returns a list of widgets.")
@summary("List widgets")
@get list(): Widget[];
```

- `@summary` → OpenAPI `summary` (short, one line)
- `@doc` → OpenAPI `description` (longer explanation, supports markdown)

### File imports in main.tsp

```typespec
import "@typespec/http";
import "@typespec/rest";

import "./models/widget.tsp";
import "./routes/widgets.tsp";

using TypeSpec.Http;
using TypeSpec.Rest;

@service({ title: "My API" })
namespace MyApi;
```

### Pagination

```typespec
model PagedResponse<T> {
  items: T[];
  total: int32;
  page: int32;
  pageSize: int32;
}

@get list(@query page?: int32, @query pageSize?: int32): PagedResponse<Widget>;
```

### Enums

```typespec
enum Status {
  Active: "active",
  Inactive: "inactive",
  Pending: "pending",
}
```

### Discriminated unions

```typespec
@discriminator("kind")
union Shape {
  circle: Circle,
  rectangle: Rectangle,
}

model Circle {
  kind: "circle";
  radius: float32;
}

model Rectangle {
  kind: "rectangle";
  width: float32;
  height: float32;
}
```

## Workflow

1. **Understand the API** — ask the user for resource names, operations, auth method, and any special requirements if not already described.

2. **Scaffold the project** (if starting fresh):
   - Create `package.json` with compiler + emitter devDependencies, run `npm install`
   - Create `tspconfig.yaml`
   - Create `main.tsp` with the `@service` decorator, namespace, and file imports
   - Create `models/` and `routes/` subdirectories

3. **Write models first** — define all request/response shapes in `models/`.

4. **Write operations** — group by resource in `routes/`, use `interface` blocks.

5. **Wire up** — import route and model files into `main.tsp`.

6. **Compile and verify** (if `tsp` CLI is available):
   ```bash
   npx tsp compile .
   ```
   Fix any diagnostics before reporting done.

## Rules

- Always use `@typespec/http` + `@typespec/rest` for HTTP APIs — never hand-roll decorators.
- Separate models from routes — never define models inline inside interface blocks.
- Use `alias` for reusable response unions (e.g., `NotFoundResponse`), not repeated inline unions.
- Prefer `utcDateTime` over `string` for timestamps.
- Use `@doc` for longer descriptions and `@summary` for one-line operation summaries — both appear in OpenAPI output. Never use `//` comments for user-visible documentation.
- Use `NoContentResponse` (204) for delete, `CreatedResponse` (201) for create — never use bare `void`.
- For PATCH operations, always use a separate `*Update` model with all fields optional, not the same model as POST.
- Never define error responses as anonymous model literals in aliases — always use a named `@error` model.
- Never import emitters (e.g., `@typespec/openapi3`) in `.tsp` files — emitters belong only in `tspconfig.yaml`.
- Never output raw OpenAPI YAML — always write TypeSpec source. The emitter handles OpenAPI generation.
- If the user asks to add a field or route to an existing spec, read the relevant `.tsp` files first before editing.
- Keep `main.tsp` as an entrypoint only — no model or operation definitions there, only imports and the `@service` namespace.

