Logo

Better OpenAPI With TypeSpecWe adopted TypeSpec to simplify API management, reduce maintenance, and improve developer productivity.

Peter Marton
Peter Marton@slashdotpeter
cover

OpenAPI, formerly known as Swagger Specification, has long been the standard for specifying APIs. At OpenMeter, we've used it from the project's inception to define our APIs and generate API docs, clients, types, validators, and server-side route handlers. However, as OpenMeter expanded, managing API specifications across our growing number of products became challenging.

To improve API management, we recently adopted TypeSpec, a new Microsoft project designed to describe APIs and generate schemas and code. TypeSpec feels similar to TypeScript and can output OpenAPI schemas, offering significant power for managing APIs. This article shares our experience and learnings from adopting
TypeSpec for both OpenMeter OSS and OpenMeter Cloud.

What We Love About TypeSpec

Below are some highlights from our journey. While OpenAPI supports many of these features, TypeSpec makes them easier or more natural to implement.

Looks Like TypeScript

The JavaScript ecosystem has benefited from TypeScript's rich type system, with features like templates, interfaces, and spreads simplifying type descriptions. Bringing these concepts to API specifications makes a lot of sense, and that's precisely how TypeSpec feels. Given that both TypeSpec and TypeScript originated from Microsoft, this similarity isn't surprising.

Here's an example showcasing the flexibility TypeSpec provides:

model UnitPrice extends Price {
   // Templates to increase type reusability
   amount: Money<Currency.USD>
 
   // Visibility control over create, update, delete, etc.
   @visibility("read")
   publishedAt: Date
}
 
model ListUnitPrice {
   // Remove admin only properties
   ...OmitProperties<UnitPrice, 'publishedAt'>
}

Feels Like Code

TypeSpec feels less like writing configuration and more like writing code. IDE support, linters, and compilation make maintaining the API spec significantly easier. With imports, packages, and namespaces, you can organize the spec and extend it with external components, much like in application code.

OpenAPI Output and Emitters

We wanted to retain OpenAPI as the source for our various generators (API docs, clients, types, validators, and server-side route handlers). Fortunately, TypeSpec offers an OpenAPI emitter, which makes generating OpenAPI YAML specifications straightforward, preserving our existing workflows.

The TypeSpec team is developing additional emitters to generate SDKs and server routes directly, though these are still in progress.

Our current OpenAPI-based generators include:

Organizing Spec Into Packages

Organizing OpenAPI files is notoriously challenging. TypeSpec's namespaces, packages, and imports simplify managing complex API specifications. The best part? You can install packages from NPM and extend your specifications, enabling reusable code and expanding TypeSpec's functionality. We expect API standards for pagination and error handling to become more standardized across the industry as developers can easily share and install them as npm packages.

import "@typespec/http";
import "@typespec/openapi";
import "@typespec/openapi3";
 
import "./discounts.tsp";
import "./plan.tsp";
import "./prices.tsp";
import "./ratecards.tsp";
import "./routes.tsp";
import "./tax.tsp";
import "./subscription.tsp";
 
namespace OpenMeter.ProductCatalog;

Co-locating Operations and Types

If you've worked with OpenAPI, you likely know the frustration of scrolling between path, parameter, and schema definitions. TypeSpec makes it much easier to co-locate types and operations.

// Operations
@route("/api/v1/customers")
@tag("Customers")
interface Customers {
  @get
  @operationId("listCustomers")
  list(...ListCustomersParams): Paginated<Customer> | CommonErrors;
}
 
// Types
@friendlyName("queryCustomerList")
model ListCustomersParams extends PaginatedQuery {
  // Filter customers by name.
  @query
  @example("ACME")
  name?: string;
}

Self-Contained Properties

OpenAPI's object-level required property can be cumbersome, especially for objects with numerous properties. In TypeSpec, properties are required by default unless suffixed with a question mark, similar to TypeScript. You can also set visibility directly within the property definition.

model User {
  // ...
  @visibility("read", "query")
  deletedAt?: Date
}

I know that the conversations about whether properties should be required or optional for APIs go deep, especially in GraphQL circles, to allow graceful failover for servers, but let's not get carried away.

Increased Consistency With Templates

TypeSpec supports templates, which function like generics and allow for consistent type reuse across APIs. This has been incredibly helpful, reducing the need for multiple model variants for pagination and error handling, which can be error-prone. For instance, we can use Paginated<Customer> to keep the API definitions concise.

// List customers.
@get
@operationId("listCustomers")
list(...ListCustomersParams): Paginated<Customer> | CommonErrors;

And here's the pagination template used above:

// A page of results.
@friendlyName("{name}List", T)
model Paginated<T> {
  page: integer;        // The page number
  @minValue(1)
  @maxValue(1000)
  pageSize: integer;    // Number of items per page
  totalCount: integer;  // Total items count
  @maxItems(1000)
  items: T[];           // Items in the page
}

70% Less Code to Maintain

As a combination of packaging, templates, and reusability, the TypeSpec definition of our API is much smaller than the OpenAPI representation. Our OpenAPI specification is almost 600k characters long, while our TypeSpec files are around a combined 200k characters. That's a significant difference and much less code to write and maintain. For a startup where speed is crucial, everything counts.

What's Next?

Our primary goal with TypeSpec is to improve the consistency and maintainability of OpenMeter APIs and SDKs. The first step was achieving feature parity with our existing OpenAPI specification while retaining compatibility. Moving forward, we plan to unify patterns across APIs and models, increasingly relying on TypeSpec emitters to generate types and SDKs. Expect significant improvements in the OpenMeter API and SDK experience in the coming months. Stay tuned.

You can find our TypeSpec files in the OpenMeter GitHub repository.