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/compilerCheck the version:
tsp --versionInitialize a new project (creates main.tsp, package.json, tspconfig.yaml, etc.):
tsp initInstall dependencies:
tsp installBasic 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/openapi3Compile TypeSpec to generate OpenAPI:
tsp compile main.tsp --emit @typespec/openapi3You 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 --watchBy 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.