How to Create OpenAPI Schemas and SDKs With TypeSpec
TypeSpec 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 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:
We can install modules using npm, and use the import statement to import them into our TypeSpec project.
Namespaces , 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:
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:
Models in TypeSpec
Models 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:
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:
The equivalent OpenAPI specification for the User model above would look like this:
Operations in TypeSpec
Operations 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:
Interfaces in TypeSpec
Interfaces 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:
The equivalent OpenAPI specification for the Users interface above would look like this:
Decorators in TypeSpec
Decorators 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:
Decorators allow you to add custom behavior to your TypeSpec definitions using JavaScript functions. You can define your own decorators 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 .
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 .
Step 1: Install the TypeSpec Compiler CLI
Install tsp globally using npm:
Step 2: Create a TypeSpec Project
Create a new directory for your TypeSpec project and navigate into it:
Run the following command to initialize a new TypeSpec project:
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:
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:
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:
Example TypeSpec File
Here’s an example of a complete TypeSpec file for a Book Store API:
Let’s break down some of the key features of this TypeSpec file:
Importing and Using Modules
The file starts by importing necessary TypeSpec modules:
These modules extend TypeSpec’s capabilities for HTTP APIs, OpenAPI generation, and API versioning.
Namespace and Service Definition
The BookStore namespace is decorated with several metadata decorators:
@service marks this namespace as a service and provides its title
@info provides additional information for the OpenAPI document
@versioned specifies the API versions
@server defines the base URL for the API
@doc provides a description for the API
Models and Inheritance
TypeSpec supports model inheritance, which is used to create specific publication types:
Union Types
Union types allow representing multiple possible types with a discriminator property:
Interfaces and Operations
Interfaces in TypeSpec group related operations:
Operations can be defined with various parameters and return types:
The @path decorator indicates a path parameter, while @body would indicate a request body parameter.
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:
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.
Generated OpenAPI Document Structure
When the TypeSpec compiler processes our specification, it generates an OpenAPI document. Here’s what the structure of the generated OpenAPI document looks like:
OpenAPI Version
The document starts with the OpenAPI version:
This is determined by the @typespec/openapi3 emitter we used, which generates OpenAPI 3.0 documents.
API Information
The info section contains metadata from our @service and @info decorators:
Server Information
The server URL from our @server decorator:
Paths and Operations
The /publications and /orders paths come from our interface route decorators:
The @body parameters in TypeSpec translate to requestBody in OpenAPI:
Components and Schemas
Our models become schemas in the components section:
Union types use the oneOf keyword:
Adding Retries with OpenAPI Extensions
To add retry logic to the listPublications operation, we can add the Speakeasy x-speakeasy-retries extension to our TypeSpec specification:
This generates the following extension in the OpenAPI document:
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.
Then, generate a TypeScript SDK using the following command:
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.
After generating our OpenAPI document, we can generate an SDK using the Speakeasy CLI. Here’s the process:
First, compile our TypeSpec code to generate the OpenAPI document:
Then use Speakeasy to generate the SDK:
This will create a TypeScript SDK in the ./sdks/bookstore-ts directory.
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:
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:
Validate the overlay using Speakeasy:
Then apply the overlay to the OpenAPI document:
If we look at the combined-openapi.yaml file, we should see the examples added to the Book and Magazine models, for example:
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 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 .
We added our TypeSpec specification 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 : The official TypeSpec documentation provides detailed information on the TypeSpec language, standard library, and emitters.
TypeSpec Releases : Keep up with the latest TypeSpec releases and updates on GitHub.
TypeSpec Playground : 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.