How to Create OpenAPI Schemas and SDKs With TypeSpec

TypeSpec (opens in a new tab) is a brand-new domain-specific language (DSL) used to describe APIs. As the name implies you describe your API using a TypeScript-like type system, with language constructs such as model for the structure or schema of your API’s data, or op for operations in your API. If you’ve used OpenAPI, these concepts likely sound familiar – this is because TypeSpec is also influenced by and generates OpenAPI.

So something that is like OpenAPI, and also generates OpenAPI specifications? You may be asking yourself, why does TypeSpec exist? Like many people, our initial reaction to TypeSpec was to reference the iconic XKCD strip:


However, after spending some time with it, we’ve come to understand the justification for a new DSL - we’ll cover some of that shortly. We also ran into this young language’s rough edges, and we’ll cover those in detail, too.

Our end goal with this article is to create a high-quality TypeScript SDK. However, before we create an SDK, we’ll need to learn how to generate an OpenAPI document based on a TypeSpec specification. For that, we need to learn TypeSpec, and there is no better way to get started learning a new language than by asking why it exists in the first place.

The Problem TypeSpec Solves

Code generation is a force multiplier in API design and development. When an executive unironically asks, “How do we 10x API creation?”, the unironic answer is, ” API-first design + Code generation.”

API-first means specifying exactly what your application’s programming interface will look like before anything gets built, code generation means using that definition to create documentation, server (stubs) and client libraries (SDKs).

As mentioned previously,OpenAPI is widely used for exactly this reason – it provides a human-readable (as YAML) specification format for APIs, and comes with a thriving ecosystem of tools and code generators. So if OpenAPI exists, what can TypeSpec add?

The fundamental problem TypeSpec aims to solve is that writing OpenAPI documents by hand is complex, tedious, and error-prone. The complexity often leads to teams to abandon an API-first approach and instead start by coding their API, and then extracting OpenAPI from the codebase when they get to the point where they need documentation and SDKs – a quasi-API-first approach.

Ultimately, OpenAPI isn’t for everyone. Neither is TypeSpec for that matter. But for those who are immersed in the TypeScript ecosystem, TypeSpec may be a more natural fit than OpenAPI. And the more tools we have to help businesses create great APIs, the better.

TypeSpec Development Status

Before you trade in your OpenAPI YAML for TypeSpec, know that at the time of writing, TypeSpec is nowhere near as feature-rich and stable as OpenAPI. If you’re designing a new API from scratch, taking the time to learn OpenAPI will benefit your team, even if TypeSpec one day becomes the most popular API specification language.

TypeSpec Libraries and Emitters

Developers can extend the capabilities of TypeSpec by creating and using libraries. These libraries can provide additional functionality, such as decorators, types, and operations, that are not part of the core TypeSpec language.

A special type of library in TypeSpec is an emitter. Emitters are used to generate output from a TypeSpec specification. For example, the @typespec/openapi3 library provides an emitter that generates an OpenAPI document from a TypeSpec specification.

When targeting a specific output format, such as OpenAPI, you can use the corresponding emitter library to generate the desired output. This allows you to write your API specification in TypeSpec and then generate the output in the desired format.

A Brief Introduction to TypeSpec Syntax

This guide won’t give a complete introduction or overview of TypeSpec, but we’ll take a brief look at the language’s structure and important concepts in the context of generating SDKs.

Modularity in TypeSpec

The main entry point in TypeSpec is the main.tsp file. This file has the same role as the index.ts file in a TypeScript project.

Just like in TypeScript, we can organize code into files, folders, and modules, then import (opens in a new tab) these using the import statement. This helps split large API specifications into smaller, more manageable parts. The difference between TypeScript and TypeSpec in this regard is that TypeSpec imports files, not code.

Here’s an example of how you can import files, folders, and modules in TypeSpec:

main.tsp
import "./books.tsp"; // Import a file
import "./books"; // Import main.tsp in a folder
import "/books"; // Import a TypeSpec module's main.tsp file

We can install modules using npm, and use the import statement to import them into our TypeSpec project.

Namespaces (opens in a new tab), another TypeScript feature that TypeSpec borrows, allow you to group types and avoid naming conflicts. This is especially useful when importing multiple files that define types with the same name. Just like with TypeScript, namespaces may be nested and span multiple files.

Namespaces are defined using the namespace keyword, followed by the namespace name and a block of type definitions. Here’s an example:

namespace MyNamespace {
model User {
id: string;
name: string;
}
}

They may also be defined at the file level, using the namespace keyword followed by the namespace name and a block of type definitions. Here’s an example:

namespace MyNamespace;
model User {
id: string;
name: string;
}
model Post {
id: string;
title: string;
content: string;
}

Models in TypeSpec

Models (opens in a new tab) in TypeSpec are similar to OpenAPI’s schema objects. They define the structure of the data that will be sent and received by your API. We define models using the model keyword, followed by the model name and a block of properties. Here’s an example:

main.tsp
model User {
id: string;
name: string;
email: string;
}

Models are composable and extensible. You can reference other models within a model definition, extend a model with additional properties, and compose multiple models into a single model. Here’s an example of model composition:

main.tsp
namespace WithComposition {
model User {
id: string;
name: string;
email: string;
}
model HasRole {
role: string;
}
model Admin is User { // Copies the properties and decorators from User
...HasRole; // Extends the User model with the properties from the HasRole model
level: number; // Adds a new property to the Admin model
}
}
// The Admin model above will have the following properties:
namespace WithoutComposition {
model Admin {
id: string;
name: string;
email: string;
role: string;
level: number;
}
}

The equivalent OpenAPI specification for the User model above would look like this:

openapi.yaml
components:
schemas:
User:
type: object
properties:
id:
type: string
name:
type: string
email:
type: string

Operations in TypeSpec

Operations (opens in a new tab) in TypeSpec are similar to OpenAPI operations. They describe the methods that users can call in your API. We define operations using the op keyword, followed by the operation name. Here’s an example:

main.tsp
op listUsers(): User[]; // Defaults to GET
op getUser(id: string): User; // Defaults to GET
op createUser(@body user: User): User; // Defaults to POST with a body parameter

Interfaces in TypeSpec

Interfaces (opens in a new tab) in TypeSpec group related operations together, similar to OpenAPI’s paths object. We define interfaces using the interface keyword, followed by the interface name and a block of operations. Here’s an example:

main.tsp
@route("/users")
interface Users {
op listUsers(): User[]; // Defaults to GET /users
op getUser(id: string): User; // Defaults to GET /users/{id}
op createUser(@body user: User): User; // Defaults to POST /users
}

The equivalent OpenAPI specification for the Users interface above would look like this:

openapi.yaml
paths:
/users:
get:
operationId: listUsers
responses:
200:
description: OK
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/User"
post:
operationId: createUser
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/User"
responses:
200:
description: OK
content:
application/json:
schema:
$ref: "#/components/schemas/User"
/users/{id}:
get:
operationId: getUser
parameters:
- name: id
in: path
required: true
schema:
type: string
responses:
200:
description: OK
content:
application/json:
schema:
$ref: "#/components/schemas/User"

Decorators in TypeSpec

Decorators (opens in a new tab) in TypeSpec add metadata to models, operations, and interfaces. They start with the @ symbol followed by the decorator name. Here’s an example of the @doc decorator:

main.tsp
@doc("A user in the system")
model User {
@doc("The unique identifier of the user")
id: string;
@doc("The name of the user")
name: string;
@doc("The email address of the user")
email: string;
}

Decorators allow you to add custom behavior to your TypeSpec definitions using JavaScript functions. You can define your own decorators (opens in a new tab) or use built-in decorators provided by TypeSpec or third-party libraries.

Learn More About TypeSpec

The language features above should be enough to help you find your way around a TypeSpec specification.

If you’re interested in learning more about the TypeSpec language, see the official documentation (opens in a new tab).

We’ll cover more detailed examples of TypeSpec syntax in our full example below.

Generating an OpenAPI Document from TypeSpec

Now that we have a basic understanding of TypeSpec syntax, let’s generate an OpenAPI document from a TypeSpec specification.

The example below will guide you through the process of creating a TypeSpec project, writing a TypeSpec specification, and generating an OpenAPI document from it.

For a speedrun, we’ve published the full example in a GitHub repository (opens in a new tab).

Step 1: Install the TypeSpec Compiler CLI

Install tsp globally using npm:

Terminal
npm install -g @typespec/compiler

Step 2: Create a TypeSpec Project

Create a new directory for your TypeSpec project and navigate into it:

Terminal
mkdir typespec-example-speakeasy
cd typespec-example-speakeasy

Run the following command to initialize a new TypeSpec project:

Terminal
tsp init

This will prompt you to select a template for your project. Choose the Generic REST API template and press enter. Press enter repeatedly to select the defaults until the project is initialized.

Step 3: Install the TypeSpec Dependencies

Install the TypeSpec dependencies using tsp:

Terminal
tsp install

We’ll need to install the @typespec/versioning and @typespec/openapi modules to generate an OpenAPI document. Run the following commands to install these modules:

Terminal
npm install @typespec/versioning @typespec/openapi

Step 4: Write Your TypeSpec Specification

Open the main.tsp file in your text editor and write your TypeSpec specification. Here’s an example of a simple TypeSpec specification:

We start by importing the necessary TypeSpec modules.

These modules are provided by the TypeSpec project, but are not part of the core TypeSpec language. They extend the capabilities of TypeSpec for specific use cases.


By writing three using statements (opens in a new tab), we expose the contents of the Http, OpenAPI, and Versioning modules to the current file. This allows us to use the functionality provided by these modules in our TypeSpec specification with less code.

Without these statements, we can still access the functionality of the modules by using the fully qualified names of the types and functions they provide.

For example, instead of writing @operationId, we could write TypeSpec.OpenAPI.operationId to access the operationId decorator provided by the OpenAPI module.


Next, we define the BookStore namespace, which will contain all the models, interfaces, and operations related to the bookstore API. Namespaces are used to group related types and operations together, and avoid naming conflicts.

This is a file-level namespace (it has no code block delimiters – no { and }), which means it spans the entire file.


Taking a step back, we see that the BookStore namespace is decorated with the @service decorator (opens in a new tab) from the TypeSpec core library.

The @service decorator marks the namespace as a service, and provides the API’s title.


We decorate the BookStore namespace with the @info decorator (opens in a new tab) from the @TypeSpec.OpenAPI library to provide information for the OpenAPI document’s info object.


TypeSpec’s @versioned decorator (opens in a new tab) from the @TypeSpec.Versioning library specifies the versions of the API.

We define a single version, 1.0.0, for the API, but you can define multiple versions if needed.


We add a @server decorator to the BookStore namespace, which specifies the base URL of the API server: http://127.0.0.1:4010.

This is the default Prism (opens in a new tab) server URL here, but you can replace this with the actual base URL of your API server.


Finally, our BookStore namespace is decorated with the @doc decorator (opens in a new tab) from the TypeSpec core library, which provides a description of the API.

In the OpenAPI 3 emitter, this description will be used as the description field in the OpenAPI document’s info object.


This brings us to our first model, PublicationBase, which represents the base model for books and magazines in the store.

Here we see how models are defined in TypeSpec using the model keyword, followed by the model name and a block of properties.

Property types are similar to those in OpenAPI, but with some nuances. For example, float32 is used instead of number, and utcDateTime is used for date-time values.

See the TypeSpec data types documentation (opens in a new tab) for more information on the available data types. We should also educate ourselves about how these data types are represented in the OpenAPI document (opens in a new tab).

The type property is defined as an enum (opens in a new tab) to represent the type of publication. This is a custom enum defined within the BookStore namespace.


Next, we define constants, BookExample1 and BookExample2, using the #{} syntax. We’ll use these examples to demonstrate the structure of the Book model.

On their own, these constants are not part of the model, nor will they be emitted by the OpenAPI emitter. We have to pass them as values in the @example decorator to include them in the OpenAPI document.


This is the Book model, which extends the PublicationBase model. We use the @example decorator to provide an example value for the model.

The extends keyword causes the Book model to inherit properties from the PublicationBase model, with the ability to add additional properties specific to books, or override existing properties.


Magazines are represented by the Magazine model, which also extends the PublicationBase model. We provide example values for the Magazine model using the @example decorator.

Note how the Magazine model adds properties specific to magazines, such as issueNumber and publisher.


To represent both books and magazines in a single model, we define a Publication union type (opens in a new tab). The Publication model is a union of the Book and Magazine models, with a discriminator property type to differentiate between the two.

The @discriminator decorator specifies the property that will be used to determine the type of the publication.

The @oneOf decorator (opens in a new tab) is specific to the OpenAPI 3 emitter, and indicates that the Publication schema should reference the Book and Magazine schemas using the oneOf keyword instead of allOf.


The Order model represents an order for publications in the store. It contains familiar properties much like those of the Publication models, except for a reference to the Publication model in the items property.

The items property is an array of publications, which can contain both books and magazines.


Moving on to operations, let’s start with the Publications interface, which wraps operations for managing publications in the store.


We decorate the Publications interface with the @tag decorator (opens in a new tab) from the standard library to specify the tag for the operations in the interface. Tags are used to group related operations in the OpenAPI document, and can be applied to interfaces, operations, and namespaces.

Since we are using the OpenAPI 3 emitter, the @tag decorator will be used to generate the tags field in the OpenAPI document.


The @route decorator (opens in a new tab) provided by the @TypeSpec.Http library specifies the path prefix for the operations in the Publications interface.


In the Publications interface, we define three operations: list, get, and create.


Let’s focus for a moment on what we don’t see in the operation definitions.

Note how the op keyword is optional when defining operations within an interface. The operations are defined directly within the interface block, without the need for the op keyword.

The operations are also defined without an HTTP method, such as GET or POST. This is because the default HTTP method for an operation is determined by the operation’s parameters.

If an operation contains a @body parameter, it defaults to POST. Any operation without a @body parameter defaults to GET.


The get operation in the Publications interface takes a string parameter id and returns a Publication or an Error.

Note how the @path decorator is used to specify that the id parameter is part of the path in the URL.

This operation will have the path /publications/{id} in the OpenAPI document. TypeSpec will automatically generate the path parameter for the id parameter.


Examples for operation parameters and return types are provided using the @opExample decorator from the standard library. These examples will be included in the OpenAPI document to demonstrate the structure of the request and response payloads.

Note that this functionality, at the time of writing (with TypeSpec 0.58.1), is not yet fully implemented in the OpenAPI emitter.

The best part of the @opExample decorator is that it allows you to provide example values for the operation parameters and return types directly in the TypeSpec specification, and that these values are typed.

This enables code editors and IDEs to provide autocompletion and type-checking for the example values, making it easier to write and maintain the examples.

This also means TypeSpec forces you to keep examples up to date with the actual data structures, the lack of which is a common source of errors in API documentation.


To generate useful operation IDs in the OpenAPI document, we use the @operationId decorator (opens in a new tab) from the @TypeSpec.OpenAPI library.

Without this decorator, TypeSpec will still derive operation IDs from the operation names, but using the decorator allows us to provide more descriptive and meaningful operation IDs.

Keep in mind that specifying manual operation IDs can lead to duplicate IDs.


That concludes our tour of the BookStore namespace. We’ve defined models for publications, orders, and errors, as well as interfaces for managing publications and orders.

main.tsp
import "@typespec/http";
import "@typespec/openapi";
import "@typespec/openapi3";
import "@typespec/versioning";
using TypeSpec.Http;
using TypeSpec.OpenAPI;
using TypeSpec.Versioning;
@service({
title: "Book Store API",
})
@info({
termsOfService: "https://bookstore.example.com/terms",
contact: {
name: "API Support",
url: "https://bookstore.example.com/support",
email: "support@bookstore.example.com",
},
license: {
name: "Apache 2.0",
url: "https://www.apache.org/licenses/LICENSE-2.0.html",
},
})
@versioned(Versions)
@server("http://127.0.0.1:4010", "Book Store API v1")
@doc("API for managing a book store inventory and orders")
namespace BookStore;
enum Versions {
`1.0.0`,
}
enum PublicationType {
Book,
Magazine,
}
@doc("Base model for books and magazines")
model PublicationBase {
@doc("Unique identifier")
@key
id: string;
@doc("Title of the publication")
title: string;
@doc("Publication date")
publishDate: utcDateTime;
@doc("Price in USD")
price: float32;
@doc("Type of publication")
type: PublicationType;
}
const BookExample1 = #{
id: "123",
title: "Book Title",
publishDate: utcDateTime.fromISO("2020-01-01T00:00:00Z"),
price: 19.99,
type: PublicationType.Book,
author: "Author Name",
isbn: "1234567890",
};
const BookExample2 = #{
id: "456",
title: "Another Book Title",
publishDate: utcDateTime.fromISO("2020-02-01T00:00:00Z"),
price: 24.99,
type: PublicationType.Book,
author: "Another Author",
isbn: "0987654321",
};
@example(BookExample1)
@doc("Represents a book in the store")
model Book extends PublicationBase {
type: PublicationType.Book;
@doc("Author of the book")
author: string;
@doc("ISBN of the book")
isbn: string;
}
const MagazineExample1 = #{
id: "789",
title: "Magazine Title",
publishDate: utcDateTime.fromISO("2020-03-01T00:00:00Z"),
price: 9.99,
type: PublicationType.Magazine,
issueNumber: 1,
publisher: "Publisher Name",
};
const MagazineExample2 = #{
id: "012",
title: "Another Magazine Title",
publishDate: utcDateTime.fromISO("2020-04-01T00:00:00Z"),
price: 7.99,
type: PublicationType.Magazine,
issueNumber: 2,
publisher: "Another Publisher",
};
@example(MagazineExample1)
@doc("Represents a magazine in the store")
model Magazine extends PublicationBase {
type: PublicationType.Magazine;
@doc("Issue number of the magazine")
issueNumber: int32;
@doc("Publisher of the magazine")
publisher: string;
}
const PublicationExample1 = BookExample1;
const PublicationExample2 = MagazineExample1;
@example(PublicationExample1)
@discriminator("type")
@oneOf
union Publication {
book: Book,
magazine: Magazine,
}
@doc("Possible statuses for an order")
enum OrderStatus {
Pending,
Shipped,
Delivered,
Cancelled,
};
const OrderExample1 = #{
id: "abc",
customerId: "123",
items: #[BookExample1, MagazineExample1],
totalPrice: 29.98,
status: OrderStatus.Pending,
};
@example(OrderExample1)
@doc("Represents an order for publications")
model Order {
@doc("Unique identifier for the order")
id: string;
@doc("Customer who placed the order")
customerId: string;
@doc("List of publications in the order")
items: Publication[];
@doc("Total price of the order")
totalPrice: float32;
@doc("Status of the order")
status: OrderStatus;
}
@doc("Operations for managing publications")
@tag("publications")
@route("/publications")
interface Publications {
@opExample(#{ returnType: #[BookExample1, MagazineExample1] })
@doc("List all publications")
@operationId("listPublications")
list(): Publication[];
@opExample(#{ parameters: #{ id: "123" }, returnType: BookExample1 })
@doc("Get a specific publication by ID")
@operationId("getPublication")
get(@path id: string): Publication | Error;
@opExample(#{
parameters: #{ publication: BookExample1 },
returnType: BookExample1,
})
@doc("Create a new publication")
@operationId("createPublication")
create(@body publication: Publication): Publication | Error;
}
@doc("Operations for managing orders")
@tag("orders")
@route("/orders")
interface Orders {
@opExample(#{
parameters: #{ order: OrderExample1 },
returnType: OrderExample1,
})
@doc("Place a new order")
@operationId("placeOrder")
placeOrder(@body order: Order): Order | Error;
@opExample(#{ parameters: #{ id: "123" }, returnType: OrderExample1 })
@doc("Get an order by ID")
@operationId("getOrder")
getOrder(@path id: string): Order | Error;
@opExample(#{
parameters: #{ id: "123", status: OrderStatus.Shipped },
returnType: OrderExample1,
})
@doc("Update the status of an order")
@operationId("updateOrderStatus")
updateStatus(@path id: string, @body status: OrderStatus): Order | Error;
}
@example(#{ code: 404, message: "Publication not found" })
@error
@doc("Error response")
model Error {
@doc("Error code")
code: int32;
@doc("Error message")
message: string;
}

Step 5: Generate the OpenAPI Document

Now that we’ve written our TypeSpec specification, we can generate an OpenAPI document from it using the tsp compiler.

Run the following command to generate an OpenAPI document:

Terminal
tsp compile main.tsp --emit @typespec/openapi3

The tsp compile command creates a new directory called tsp-output, then the @typespec/openapi3 emitter creates the directories @typespec/openapi3 within. If we were to use other emitters, such as protobuf, we would see @typespec/protobuf directories instead.

Because we’re using the versioning library, the OpenAPI document will be generated for the specified version of the API. In our case, the file generated by the OpenAPI 3 emitter will be named openapi.yaml.

Step 6: View the Generated OpenAPI Document

Open the generated OpenAPI document in your text editor or a YAML viewer to see the API specification.

Let’s scroll through the generated OpenAPI document to see how our TypeSpec specification was translated into an OpenAPI specification.



The OpenAPI document starts with the openapi field, which specifies the version of the OpenAPI specification used in the document. In this case, it’s version 3.0.0.

This version is determined by the emitter we used to generate the OpenAPI document. The @typespec/openapi3 emitter generates OpenAPI 3.0 documents.



The info field contains metadata about the API, such as the title, terms of service, contact information, license, and description. This information is provided by the @service and @info decorators in the TypeSpec specification.



Let’s take a closer look at the placeOrder operation in the OpenAPI document. The operation is defined under the /orders path and uses the POST method.



Firstly, we see that the operation’s operationId is set to placeOrder, which is the same as the @operationId decorator in the TypeSpec specification.



The operation is tagged with the orders tag, which is specified by the @tag decorator in the TypeSpec specification. In this case, we tagged the Orders interface with the orders tag, instead of the individual operations.

The tags still apply to individual operations in the OpenAPI document, as seen here.



Instead of parameters, this operation uses a requestBody field to specify the request payload. The @body parameter in the TypeSpec specification corresponds to the requestBody field in the OpenAPI document.



Of particular interest is the example field in the requestBody object. This field provides an example value for the request payload, demonstrating the structure of the data expected by the API.

The current implementation of the OpenAPI emitter supports the @opExample decorator for operation examples, but does not yet support extended models or unions. This shows up in the generated OpenAPI document as empty objects in the items array for the order example.



Next, let’s look at how the OpenAPI document represents our polymorphic Publication model.



Because the Publication model is a union of the Book and Magazine models, and we decorated this union with @oneOf in TypeSpec, the OpenAPI document uses the oneOf keyword to represent the union.



Unfortunately, as of version 0.58.1 of TypeSpec, the OpenAPI emitter also seems to fail to include the example values for the Publication model in the OpenAPI document.



Likewise, the Book schema’s example is incomplete in the OpenAPI document. The emitter does not show the example values for the PublicationBase properties, such as id, title, publishDate, and price.


In the Book schema, we also see how the allOf keyword is used to combine the properties of the PublicationBase model with the additional properties of the Book model.


Problems with examples aside, the OpenAPI document provides a clear representation of the API we defined in our TypeSpec specification.

main.tsp
import "@typespec/http";
import "@typespec/openapi";
import "@typespec/openapi3";
import "@typespec/versioning";
using TypeSpec.Http;
using TypeSpec.OpenAPI;
using TypeSpec.Versioning;
@service({
title: "Book Store API",
})
@info({
termsOfService: "https://bookstore.example.com/terms",
contact: {
name: "API Support",
url: "https://bookstore.example.com/support",
email: "support@bookstore.example.com",
},
license: {
name: "Apache 2.0",
url: "https://www.apache.org/licenses/LICENSE-2.0.html",
},
})
@versioned(Versions)
@server("http://127.0.0.1:4010", "Book Store API v1")
@doc("API for managing a book store inventory and orders")
namespace BookStore;
enum Versions {
`1.0.0`,
}
enum PublicationType {
Book,
Magazine,
}
@doc("Base model for books and magazines")
model PublicationBase {
@doc("Unique identifier")
@key
id: string;
@doc("Title of the publication")
title: string;
@doc("Publication date")
publishDate: utcDateTime;
@doc("Price in USD")
price: float32;
@doc("Type of publication")
type: PublicationType;
}
const BookExample1 = #{
id: "123",
title: "Book Title",
publishDate: utcDateTime.fromISO("2020-01-01T00:00:00Z"),
price: 19.99,
type: PublicationType.Book,
author: "Author Name",
isbn: "1234567890",
};
const BookExample2 = #{
id: "456",
title: "Another Book Title",
publishDate: utcDateTime.fromISO("2020-02-01T00:00:00Z"),
price: 24.99,
type: PublicationType.Book,
author: "Another Author",
isbn: "0987654321",
};
@example(BookExample1)
@doc("Represents a book in the store")
model Book extends PublicationBase {
type: PublicationType.Book;
@doc("Author of the book")
author: string;
@doc("ISBN of the book")
isbn: string;
}
const MagazineExample1 = #{
id: "789",
title: "Magazine Title",
publishDate: utcDateTime.fromISO("2020-03-01T00:00:00Z"),
price: 9.99,
type: PublicationType.Magazine,
issueNumber: 1,
publisher: "Publisher Name",
};
const MagazineExample2 = #{
id: "012",
title: "Another Magazine Title",
publishDate: utcDateTime.fromISO("2020-04-01T00:00:00Z"),
price: 7.99,
type: PublicationType.Magazine,
issueNumber: 2,
publisher: "Another Publisher",
};
@example(MagazineExample1)
@doc("Represents a magazine in the store")
model Magazine extends PublicationBase {
type: PublicationType.Magazine;
@doc("Issue number of the magazine")
issueNumber: int32;
@doc("Publisher of the magazine")
publisher: string;
}
const PublicationExample1 = BookExample1;
const PublicationExample2 = MagazineExample1;
@example(PublicationExample1)
@discriminator("type")
@oneOf
union Publication {
book: Book,
magazine: Magazine,
}
@doc("Possible statuses for an order")
enum OrderStatus {
Pending,
Shipped,
Delivered,
Cancelled,
};
const OrderExample1 = #{
id: "abc",
customerId: "123",
items: #[BookExample1, MagazineExample1],
totalPrice: 29.98,
status: OrderStatus.Pending,
};
@example(OrderExample1)
@doc("Represents an order for publications")
model Order {
@doc("Unique identifier for the order")
id: string;
@doc("Customer who placed the order")
customerId: string;
@doc("List of publications in the order")
items: Publication[];
@doc("Total price of the order")
totalPrice: float32;
@doc("Status of the order")
status: OrderStatus;
}
@doc("Operations for managing publications")
@tag("publications")
@route("/publications")
interface Publications {
@opExample(#{ returnType: #[BookExample1, MagazineExample1] })
@doc("List all publications")
@operationId("listPublications")
list(): Publication[];
@opExample(#{ parameters: #{ id: "123" }, returnType: BookExample1 })
@doc("Get a specific publication by ID")
@operationId("getPublication")
get(@path id: string): Publication | Error;
@opExample(#{
parameters: #{ publication: BookExample1 },
returnType: BookExample1,
})
@doc("Create a new publication")
@operationId("createPublication")
create(@body publication: Publication): Publication | Error;
}
@doc("Operations for managing orders")
@tag("orders")
@route("/orders")
interface Orders {
@opExample(#{
parameters: #{ order: OrderExample1 },
returnType: OrderExample1,
})
@doc("Place a new order")
@operationId("placeOrder")
placeOrder(@body order: Order): Order | Error;
@opExample(#{ parameters: #{ id: "123" }, returnType: OrderExample1 })
@doc("Get an order by ID")
@operationId("getOrder")
getOrder(@path id: string): Order | Error;
@opExample(#{
parameters: #{ id: "123", status: OrderStatus.Shipped },
returnType: OrderExample1,
})
@doc("Update the status of an order")
@operationId("updateOrderStatus")
updateStatus(@path id: string, @body status: OrderStatus): Order | Error;
}
@example(#{ code: 404, message: "Publication not found" })
@error
@doc("Error response")
model Error {
@doc("Error code")
code: int32;
@doc("Error message")
message: string;
}
openapi.yaml
openapi: 3.0.0
info:
title: Book Store API
termsOfService: https://bookstore.example.com/terms
contact:
name: API Support
url: https://bookstore.example.com/support
email: support@bookstore.example.com
license:
name: Apache 2.0
url: https://www.apache.org/licenses/LICENSE-2.0.html
description: API for managing a book store inventory and orders
version: 1.0.0
tags:
- name: publications
- name: orders
paths:
/orders:
post:
tags:
- orders
operationId: placeOrder
description: Place a new order
parameters: []
responses:
"200":
description: The request has succeeded.
content:
application/json:
schema:
$ref: "#/components/schemas/Order"
example: {}
default:
description: An unexpected error response.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
example: {}
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/Order"
example:
order:
id: abc
customerId: "123"
items:
- {}
- {}
totalPrice: 29.98
status: Pending
/orders/{id}:
get:
tags:
- orders
operationId: getOrder
description: Get an order by ID
parameters:
- name: id
in: path
required: true
schema:
type: string
responses:
"200":
description: The request has succeeded.
content:
application/json:
schema:
$ref: "#/components/schemas/Order"
example: {}
default:
description: An unexpected error response.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
example: {}
post:
tags:
- orders
operationId: updateOrderStatus
description: Update the status of an order
parameters:
- name: id
in: path
required: true
schema:
type: string
responses:
"200":
description: The request has succeeded.
content:
application/json:
schema:
$ref: "#/components/schemas/Order"
example: {}
default:
description: An unexpected error response.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
example: {}
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/OrderStatus"
example:
id: "123"
status: Shipped
/publications:
get:
tags:
- publications
operationId: listPublications
description: List all publications
parameters: []
responses:
"200":
description: The request has succeeded.
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Publication"
example:
- {}
- {}
post:
tags:
- publications
operationId: createPublication
description: Create a new publication
parameters: []
responses:
"200":
description: The request has succeeded.
content:
application/json:
schema:
$ref: "#/components/schemas/Publication"
example: {}
default:
description: An unexpected error response.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
example: {}
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/Publication"
example:
publication: {}
/publications/{id}:
get:
tags:
- publications
operationId: getPublication
description: Get a specific publication by ID
parameters:
- name: id
in: path
required: true
schema:
type: string
responses:
"200":
description: The request has succeeded.
content:
application/json:
schema:
$ref: "#/components/schemas/Publication"
example: {}
default:
description: An unexpected error response.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
example: {}
components:
schemas:
Book:
type: object
required:
- type
- author
- isbn
properties:
type:
type: string
enum:
- Book
author:
type: string
description: Author of the book
isbn:
type: string
description: ISBN of the book
allOf:
- $ref: "#/components/schemas/PublicationBase"
example:
type: Book
author: Author Name
isbn: "1234567890"
description: Represents a book in the store
Error:
type: object
required:
- code
- message
properties:
code:
type: integer
format: int32
description: Error code
message:
type: string
description: Error message
example:
code: 404
message: Publication not found
description: Error response
Magazine:
type: object
required:
- type
- issueNumber
- publisher
properties:
type:
type: string
enum:
- Magazine
issueNumber:
type: integer
format: int32
description: Issue number of the magazine
publisher:
type: string
description: Publisher of the magazine
allOf:
- $ref: "#/components/schemas/PublicationBase"
example:
type: Magazine
issueNumber: 1
publisher: Publisher Name
description: Represents a magazine in the store
Order:
type: object
required:
- id
- customerId
- items
- totalPrice
- status
properties:
id:
type: string
description: Unique identifier for the order
customerId:
type: string
description: Customer who placed the order
items:
type: array
items:
$ref: "#/components/schemas/Publication"
description: List of publications in the order
totalPrice:
type: number
format: float
description: Total price of the order
status:
allOf:
- $ref: "#/components/schemas/OrderStatus"
description: Status of the order
example:
id: abc
customerId: "123"
items:
- {}
- {}
totalPrice: 29.98
status: Pending
description: Represents an order for publications
OrderStatus:
type: string
enum:
- Pending
- Shipped
- Delivered
- Cancelled
description: Possible statuses for an order
Publication:
oneOf:
- $ref: "#/components/schemas/Book"
- $ref: "#/components/schemas/Magazine"
discriminator:
propertyName: type
mapping:
Book: "#/components/schemas/Book"
Magazine: "#/components/schemas/Magazine"
example: {}
PublicationBase:
type: object
required:
- id
- title
- publishDate
- price
- type
properties:
id:
type: string
description: Unique identifier
title:
type: string
description: Title of the publication
publishDate:
type: string
format: date-time
description: Publication date
price:
type: number
format: float
description: Price in USD
type:
allOf:
- $ref: "#/components/schemas/PublicationType"
description: Type of publication
description: Base model for books and magazines
PublicationType:
type: string
enum:
- Book
- Magazine
Versions:
type: string
enum:
- 1.0.0
servers:
- url: http://127.0.0.1:4010
description: Book Store API v1
variables: {}

Step 7: Generate an SDK from the OpenAPI Document

Now that we have an OpenAPI document for our API, we can generate an SDK using Speakeasy.

Make sure you have Speakeasy installed:

Terminal
speakeasy --version

Then, generate a TypeScript SDK using the following command:

Terminal
speakeasy generate sdk \
--schema tsp-output/@typespec/openapi3/openapi.yaml \
--lang typescript \
--out ./sdks/bookstore-ts

This command generates a TypeScript SDK for the API defined in the OpenAPI document. The SDK will be placed in the sdks/bookstore-ts directory.

Step 8: Customize the SDK

We’d like to add retry logic to the SDK’s listPublications to handle network errors gracefully. We’ll do this by using an OpenAPI extension that Speakeasy provides, x-speakeasy-retries.

Instead of modifying the OpenAPI document directly, we’ll add this extension to the TypeSpec specification and regenerate the OpenAPI document and SDK.

Let’s add the x-speakeasy-retries extension to the listPublications operation in the TypeSpec specification.

Do this by adding the @extension decorator to the listPublications operation, then specifying the x-speakeasy-retries extension.


Now that we’ve added the x-speakeasy-retries extension to the BookStore namespace, we can regenerate the OpenAPI document:

main.tsp
interface Publications {
@extension("x-speakeasy-retries", {
strategy: "backoff",
backoff: {
initialInterval: 500,
maxInterval: 60000,
maxElapsedTime: 3600000,
exponent: 1.5,
},
statusCodes: ["5XX"],
retryConnectionErrors: true
})
@opExample(#{ returnType: #[BookExample1, MagazineExample1] })
@doc("List all publications")
@operationId("listPublications")
list(): Publication[];

Now that we’ve added the x-speakeasy-retries extension to the listPublications operation in the TypeSpec specification, we can use Speakeasy to recreate the SDK:

Terminal
speakeasy generate sdk \
--schema tsp-output/@typespec/openapi3/openapi.yaml \
--lang typescript \
--out ./sdks/bookstore-ts

Common TypeSpec Pitfalls and Possible Solutions

While working with TypeSpec version 0.58.1, we encountered a few limitations and pitfalls that you should be aware of.

1. Limited Support for Model and Operation Examples

Examples only shipped as part of TypeSpec version 0.58.0, and the OpenAPI emitter is still in development. This means that the examples provided in the TypeSpec specification may not be included in the generated OpenAPI document.

To work around this limitation, you can provide examples directly in the OpenAPI document, preferably by using an OpenAPI Overlay.

Here’s an overlay, saved as bookstore-overlay.yaml, that adds examples to the Book and Magazine models in the OpenAPI document:

bookstore-overlay.yaml
overlay: 1.0.0
info:
title: Add Examples to Book and Magazine Models
version: 1.0.0
actions:
- target: $.components.schemas.Book
update:
example:
id: "1"
title: "The Great Gatsby"
publishDate: "2022-01-01T00:00:00Z"
price: 19.99
- target: $.components.schemas.Magazine
update:
example:
id: "2"
title: "National Geographic"
publishDate: "2022-01-01T00:00:00Z"
price: 5.99

Validate the overlay using Speakeasy:

Terminal
speakeasy overlay validate -o bookstore-overlay.yaml

Then apply the overlay to the OpenAPI document:

Terminal
speakeasy overlay apply -s tsp-output/@typespec/openapi3/openapi.yaml -o bookstore-overlay.yaml > combined-openapi.yaml

If we look at the combined-openapi.yaml file, we should see the examples added to the Book and Magazine models, for example:

combined-openapi.yaml
example:
type: Magazine
issueNumber: 1
publisher: Publisher Name
id: "2"
title: "National Geographic"
publishDate: "2022-01-01T00:00:00Z"
price: 5.99

2. Only Single Examples Supported

At the time of writing, the OpenAPI emitter only supports a single example for each operation or model. If you provide multiple examples using the @opExample decorator in the TypeSpec specification, only the last example will be included in the OpenAPI document.

OpenAPI version 3.0.0 introduced support for multiple examples using the examples field, and since OpenAPI 3.1.0, the singular example field is marked as deprecated in favor of multiple examples.

3. No Extensions at the Namespace Level

We found that the x-speakeasy-retries extension could not be added at the namespace level in the TypeSpec specification, even though Speakeasy supports this extension at the operation level.

The TypeSpec documentation on the @extension (opens in a new tab) decorator does not mention any restrictions on where extensions can be applied, so this may be a bug or an undocumented limitation.

To work around this limitation, you can add the x-speakeasy-retries extension directly to the OpenAPI document using an overlay, as shown in the previous example, or by adding it to each operation individually in the TypeSpec specification.

4. No Support for Webhooks or Callbacks

TypeSpec does not yet support webhooks or callbacks, which are common in modern APIs. This means you cannot define webhook operations or callback URLs in your TypeSpec specification and generate OpenAPI documents for them.

To work around this limitation, you can define webhooks and callbacks directly in the OpenAPI document using an overlay, or by adding them to the OpenAPI document manually.

5. OpenAPI 3.0.0 Only

TypeSpec’s OpenAPI emitter currently only supports OpenAPI version 3.0.0. We much prefer OpenAPI 3.1.0, which introduced several improvements over 3.0.0.

The TypeSpec Playground

To help you experiment with TypeSpec and see how it translates to OpenAPI, the Microsoft team created a TypeSpec Playground (opens in a new tab).

We added our TypeSpec specification (opens in a new tab) to the playground. You can view the generated OpenAPI document and SDK, or browse a generated Swagger UI for the API.

Further Reading

This guide barely scratches the surface of what you can do with TypeSpec. This small language is evolving rapidly, and new features are being added all the time.

Here are some resources to help you learn more about TypeSpec and how to use it effectively: