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:
import "./books.tsp"; // Import a fileimport "./books"; // Import main.tsp in a folderimport "/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:
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:
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 modellevel: 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:
components:schemas:User:type: objectproperties:id:type: stringname:type: stringemail: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:
op listUsers(): User[]; // Defaults to GETop getUser(id: string): User; // Defaults to GETop 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:
@route("/users")interface Users {op listUsers(): User[]; // Defaults to GET /usersop 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:
paths:/users:get:operationId: listUsersresponses:200:description: OKcontent:application/json:schema:type: arrayitems:$ref: "#/components/schemas/User"post:operationId: createUserrequestBody:required: truecontent:application/json:schema:$ref: "#/components/schemas/User"responses:200:description: OKcontent:application/json:schema:$ref: "#/components/schemas/User"/users/{id}:get:operationId: getUserparameters:- name: idin: pathrequired: trueschema:type: stringresponses:200:description: OKcontent: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:
@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:
npm install -g @typespec/compiler
Step 2: Create a TypeSpec Project
Create a new directory for your TypeSpec project and navigate into it:
mkdir typespec-example-speakeasycd typespec-example-speakeasy
Run the following command to initialize a new TypeSpec project:
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
:
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:
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.
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")@keyid: 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")@oneOfunion 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:
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.
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")@keyid: 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")@oneOfunion 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: 3.0.0info:title: Book Store APItermsOfService: https://bookstore.example.com/termscontact:name: API Supporturl: https://bookstore.example.com/supportemail: support@bookstore.example.comlicense:name: Apache 2.0url: https://www.apache.org/licenses/LICENSE-2.0.htmldescription: API for managing a book store inventory and ordersversion: 1.0.0tags:- name: publications- name: orderspaths:/orders:post:tags:- ordersoperationId: placeOrderdescription: Place a new orderparameters: []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: truecontent:application/json:schema:$ref: "#/components/schemas/Order"example:order:id: abccustomerId: "123"items:- {}- {}totalPrice: 29.98status: Pending/orders/{id}:get:tags:- ordersoperationId: getOrderdescription: Get an order by IDparameters:- name: idin: pathrequired: trueschema:type: stringresponses:"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:- ordersoperationId: updateOrderStatusdescription: Update the status of an orderparameters:- name: idin: pathrequired: trueschema:type: stringresponses:"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: truecontent:application/json:schema:$ref: "#/components/schemas/OrderStatus"example:id: "123"status: Shipped/publications:get:tags:- publicationsoperationId: listPublicationsdescription: List all publicationsparameters: []responses:"200":description: The request has succeeded.content:application/json:schema:type: arrayitems:$ref: "#/components/schemas/Publication"example:- {}- {}post:tags:- publicationsoperationId: createPublicationdescription: Create a new publicationparameters: []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: truecontent:application/json:schema:$ref: "#/components/schemas/Publication"example:publication: {}/publications/{id}:get:tags:- publicationsoperationId: getPublicationdescription: Get a specific publication by IDparameters:- name: idin: pathrequired: trueschema:type: stringresponses:"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: objectrequired:- type- author- isbnproperties:type:type: stringenum:- Bookauthor:type: stringdescription: Author of the bookisbn:type: stringdescription: ISBN of the bookallOf:- $ref: "#/components/schemas/PublicationBase"example:type: Bookauthor: Author Nameisbn: "1234567890"description: Represents a book in the storeError:type: objectrequired:- code- messageproperties:code:type: integerformat: int32description: Error codemessage:type: stringdescription: Error messageexample:code: 404message: Publication not founddescription: Error responseMagazine:type: objectrequired:- type- issueNumber- publisherproperties:type:type: stringenum:- MagazineissueNumber:type: integerformat: int32description: Issue number of the magazinepublisher:type: stringdescription: Publisher of the magazineallOf:- $ref: "#/components/schemas/PublicationBase"example:type: MagazineissueNumber: 1publisher: Publisher Namedescription: Represents a magazine in the storeOrder:type: objectrequired:- id- customerId- items- totalPrice- statusproperties:id:type: stringdescription: Unique identifier for the ordercustomerId:type: stringdescription: Customer who placed the orderitems:type: arrayitems:$ref: "#/components/schemas/Publication"description: List of publications in the ordertotalPrice:type: numberformat: floatdescription: Total price of the orderstatus:allOf:- $ref: "#/components/schemas/OrderStatus"description: Status of the orderexample:id: abccustomerId: "123"items:- {}- {}totalPrice: 29.98status: Pendingdescription: Represents an order for publicationsOrderStatus:type: stringenum:- Pending- Shipped- Delivered- Cancelleddescription: Possible statuses for an orderPublication:oneOf:- $ref: "#/components/schemas/Book"- $ref: "#/components/schemas/Magazine"discriminator:propertyName: typemapping:Book: "#/components/schemas/Book"Magazine: "#/components/schemas/Magazine"example: {}PublicationBase:type: objectrequired:- id- title- publishDate- price- typeproperties:id:type: stringdescription: Unique identifiertitle:type: stringdescription: Title of the publicationpublishDate:type: stringformat: date-timedescription: Publication dateprice:type: numberformat: floatdescription: Price in USDtype:allOf:- $ref: "#/components/schemas/PublicationType"description: Type of publicationdescription: Base model for books and magazinesPublicationType:type: stringenum:- Book- MagazineVersions:type: stringenum:- 1.0.0servers:- url: http://127.0.0.1:4010description: Book Store API v1variables: {}
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:
speakeasy --version
Then, generate a TypeScript SDK using the following command:
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:
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:
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:
overlay: 1.0.0info:title: Add Examples to Book and Magazine Modelsversion: 1.0.0actions:- target: $.components.schemas.Bookupdate:example:id: "1"title: "The Great Gatsby"publishDate: "2022-01-01T00:00:00Z"price: 19.99- target: $.components.schemas.Magazineupdate:example:id: "2"title: "National Geographic"publishDate: "2022-01-01T00:00:00Z"price: 5.99
Validate the overlay using Speakeasy:
speakeasy overlay validate -o bookstore-overlay.yaml
Then apply the overlay to the OpenAPI document:
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:
example:type: MagazineissueNumber: 1publisher: Publisher Nameid: "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:
- TypeSpec Documentation (opens in a new tab): The official TypeSpec documentation provides detailed information on the TypeSpec language, standard library, and emitters.
- TypeSpec Releases (opens in a new tab): Keep up with the latest TypeSpec releases and updates on GitHub.
- TypeSpec Playground (opens in a new tab): Worth mentioning again: experiment with TypeSpec in the browser, generate OpenAPI documents, and view the resulting Swagger UI.
- Speakeasy Documentation: Speakeasy has extensive documentation on how to generate SDKs from OpenAPI documents, customize SDKs, and more.
- Speakeasy OpenAPI Reference: For a detailed reference on the OpenAPI specification.