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.