Jun 30, 2024

Notes on Trying TypeSpec

Background

For APIs, I prefer defining them in a DSL and generating code from it. With REST, you can write OpenAPI (Swagger) and generate docs/clients, but writing YAML is often painful.

There are GUI tools to author OpenAPI, but requiring a separate GUI to write and read specs feels wrong—specs should be text, versioned like code.

Code‑first tools that generate OpenAPI from a backend exist, but they tie you to a language/framework. If you’re only on the frontend, it’s not ideal.

Since I already write TypeScript types on the frontend, a TypeScript‑like way to write API specs would be ideal. That’s where TypeSpec fits.

What is TypeSpec?

TypeSpec is a Microsoft DSL for designing and documenting APIs using TypeScript‑like syntax.

Official site: https://typespec.io/

Install and setup

Install globally:

npm install -g @typespec/compiler

Check the version:

tsp --version

Initialize a new project (creates main.tsp, package.json, tspconfig.yaml, etc.):

tsp init

Install dependencies:

tsp install

Basic usage

Create main.tsp. Example from the docs:

using TypeSpec.Http;

model Store {
  name: string;
  address: Address;
}

model Address {
  street: string;
  city: string;
}

@route("/stores")
interface Stores {
  list(@query filter: string): Store[];
  read(@path id: Store): Store;
}

If you’ve written code before, you can read this at a glance. Compared to the equivalent OpenAPI YAML, TypeSpec is far more concise.

Generate OpenAPI

Install the package:

npm install @typespec/openapi3

Compile TypeSpec to generate OpenAPI:

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

You can configure tspconfig.yaml so you don’t pass --emit each time:

emit:
  - "@typespec/openapi3"

Watch for file changes:

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

By default, tsp-output/@typespec/openapi3/openapi.yaml is created. To output openapi.yml at the project root, add:

options:
  "@typespec/openapi3":
    emitter-output-dir: "{output-dir}"
    output-file: "openapi.yml"

output-dir: "{project-root}"

Syntax

Models

Define data models with model. Defaults and optional properties are TypeScript‑like:

model User {
  id: string;
  name: string = "Anonymous";
  description?: string;
}

Decorators add docs and validation:

model User {
  @doc("User ID")
  @maxLength(50)
  username: string;

  @doc("User email address")
  @pattern("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$")
  email: string;

  @doc("User age (18+)" )
  @minValue(18)
  age: int32;
}

Use enums:

enum UserRole {
  Admin,
  Editor,
  Viewer
}

model User {
  role: UserRole;
}

Operations

Use op to define endpoints. Example GET to fetch a user:

op getUser(userId: string): User;

Decorate with HTTP metadata:

@get
@route("/users/{userId}")
op getUser(userId: string): User;

Interfaces

Group operations with interface. CRUD for users:

interface Users {
	@get
	@route("/users")
	listUsers(limit?: int32, role?: string): User[];

	@get
	@route("/users/{userId}")
	getUser(userId: string): User;

	@post
	@route("/users")
	createUser(
		user: {
			name: string;
			email: string;
		},
	): User;

	@patch
	@route("/users/{userId}")
	updateUser(
		userId: string,
		user: {
			name?: string;
			email?: string;
		},
	): User;

	@delete
	@route("/users/{userId}")
	deleteUser(userId: string): void;
}

Takeaways

I used TypeSpec on a project to write an API spec and it felt like writing TypeScript. With the VS Code extension you also get static checks, so syntax errors are easy to catch.

Following the “principle of least astonishment,” TypeSpec feels like a low‑surprise way for TypeScript programmers to write API specs. I plan to use it more going forward.