TypeSpec: API Design Before Code
Most teams design APIs one of two ways: write code first and extract OpenAPI from annotations, or write OpenAPI YAML directly and generate types from it. Both have the same problem — you end up maintaining two things (schema + code) that need to stay in sync.
TypeSpec is Microsoft's answer: a dedicated language for expressing API shapes, from which you generate OpenAPI, JSON Schema, Protobuf, client SDKs, and server scaffolding. One source of truth, multiple outputs.
Why a Dedicated API Language
OpenAPI YAML gets verbose fast. A simple CRUD endpoint with proper schemas, error responses, and pagination can run to hundreds of lines. Maintaining it by hand is error-prone; generating it from code annotations couples your API contract to your implementation choices.
TypeSpec treats API design as a first-class programming task:
import "@typespec/http";
using TypeSpec.Http;
model User {
id: string;
email: string;
name: string;
createdAt: utcDateTime;
}
@route("/users")
interface Users {
@get list(): User[];
@post create(@body user: OmitProperties<User, "id" | "createdAt">): User;
@get read(@path id: string): User | NotFoundResponse;
@patch update(@path id: string, @body patch: UpdateableProperties<User>): User;
@delete remove(@path id: string): void;
}
This generates a complete OpenAPI 3.0 spec, including proper response schemas and HTTP status codes.
Installation
npm install -g @typespec/compiler
# Initialize a new TypeSpec project
mkdir my-api && cd my-api
tsp init --template rest
# Or add to existing project
npm install --save-dev @typespec/compiler @typespec/http @typespec/rest @typespec/openapi3
Project structure:
my-api/
├── main.tsp # Main TypeSpec file
├── tspconfig.yaml # Compiler configuration
└── package.json
Basic TypeSpec Syntax
Models (Schemas)
// Simple model
model Product {
id: string;
name: string;
price: float64;
category: string;
inStock: boolean;
}
// Extend a model
model DigitalProduct extends Product {
downloadUrl: string;
fileSize: int64;
}
// Template models
model Page<T> {
items: T[];
total: int64;
page: int32;
pageSize: int32;
}
// Utility types
model CreateProductRequest is OmitProperties<Product, "id">;
model UpdateProductRequest is UpdateableProperties<Product>;
Enums and Unions
enum OrderStatus {
Pending: "pending",
Processing: "processing",
Shipped: "shipped",
Delivered: "delivered",
Cancelled: "cancelled",
}
// Union types
alias StringOrNumber = string | int32;
alias Response<T> = T | NotFoundResponse | UnauthorizedResponse;
Decorators
TypeSpec uses decorators to add HTTP semantics, validation, and documentation:
model User {
@format("email")
email: string;
@minLength(8)
@maxLength(72)
password: string;
@minValue(0)
@maxValue(120)
age?: int32;
@doc("ISO 3166-1 alpha-2 country code")
country?: string;
}
HTTP Operations
import "@typespec/http";
using TypeSpec.Http;
@service({
title: "Product API",
version: "1.0.0",
})
@server("https://api.example.com", "Production")
namespace ProductAPI;
@route("/products")
@tag("Products")
interface ProductOperations {
@doc("List all products with optional filtering")
@get
list(
@query category?: string,
@query page?: int32 = 1,
@query pageSize?: int32 = 20,
): Page<Product>;
@doc("Create a new product")
@post
create(@body product: CreateProductRequest): {
@statusCode statusCode: 201;
@body product: Product;
};
@doc("Get a specific product")
@get
read(@path id: string): Product | NotFoundResponse;
}
Authentication
import "@typespec/http";
using TypeSpec.Http;
// Define security scheme
@useAuth(BearerAuth)
namespace SecureAPI;
// Or per-operation
@useAuth(BearerAuth | ApiKeyAuth<ApiKeyLocation.header, "X-API-Key">)
interface AdminOperations {
@get
dashboard(): DashboardData;
}
Generating Output
# Compile to OpenAPI 3.0
tsp compile main.tsp
# Output goes to tsp-output/ by default
ls tsp-output/@typespec/openapi3/
# openapi.yaml
Configure output in tspconfig.yaml:
emit:
- "@typespec/openapi3"
- "@typespec/json-schema"
options:
"@typespec/openapi3":
output-file: "openapi.yaml"
openapi-versions:
- "3.0.0"
"@typespec/json-schema":
output-dir: "json-schemas"
Generating Clients
With the generated OpenAPI spec, use any OpenAPI client generator:
# TypeScript client with openapi-typescript
npx openapi-typescript tsp-output/@typespec/openapi3/openapi.yaml -o src/api.ts
# Python client
openapi-generator-cli generate \
-i tsp-output/@typespec/openapi3/openapi.yaml \
-g python \
-o clients/python
# Go client
openapi-generator-cli generate \
-i tsp-output/@typespec/openapi3/openapi.yaml \
-g go \
-o clients/go
CI Integration
Check that generated specs don't drift from the source:
# .github/workflows/api-check.yml
name: API Spec Check
on: [push, pull_request]
jobs:
check-spec:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npx tsp compile main.tsp
- name: Check for spec drift
run: |
if ! git diff --quiet tsp-output/; then
echo "Generated spec has changed. Run 'tsp compile' and commit."
git diff tsp-output/
exit 1
fi
TypeSpec vs. Alternatives
| Tool | Approach | Output |
|---|---|---|
| TypeSpec | Design-first language | OpenAPI, Protobuf, JSON Schema |
| OpenAPI YAML | Write spec directly | Docs, clients |
| tRPC | Code-first (TypeScript) | TypeScript clients only |
| Protobuf | IDL for binary protocols | gRPC, binary |
| Zod + openapi-zod-client | Code-first validation | OpenAPI from Zod schemas |
TypeSpec fills the gap between "write OpenAPI YAML" (verbose, no reuse) and "code-first with annotations" (couples spec to implementation). It's most valuable for teams with multiple consumers of an API, or where the contract needs to exist independently of any implementation.
Subscribe to DevTools Guide for more developer tool deep dives.