Typespec


Should I use DSL in order to build my OpenAPI spec?

I have tried Typespec recently and I wanted to share my thoughts about it. If you are not familiar with Typespec, it is a tool that allows you to generate OpenAPI specs using a DSL: Typespec


How to use Typespec?

The best way to get started with Typespec is just to read the Getting Started section in the Typespec documentation.

If you want to see an extented example you can check this great article from Speakeasy: Typespec: OpenAPI in TypeScript.


Ok, we are on the same pag now so let’s consider the following example:

import "@typespec/http";
import "@typespec/rest";
import "@typespec/openapi3";
import "@typespec/versioning";

import "./api";

using TypeSpec.Http;
using TypeSpec.Rest;
using TypeSpec.Versioning;

@service({
    title: "Users service",
})
@server("https://users.com", "Server url")
@versioned(Versions)
namespace Users;
enum Versions {
    v1: "2024-12-31",
}

@route("/users")
@tag("Users")
@doc("Users API")
interface Users {
    @get
    @doc("Get a list of users")
    users(...UserListRequest): UserListResponse;

    @get
    @doc("Get a user by ID")
    user(
        @path userID: UserID,
    ): UserResponse | UserNotFoundResponse | UserInternalServerErrorResponse;

    @post
    @doc("Create a user")
    createUser(@body user: UserCreateRequestBody, ...UserCreateRequest): User;
}

What we are doing here?

We are defining a service called Users, with a versioned endpoint, and we are defining the routes for the service. The cool thing here is the fact the we have a single file which can contain all the information about our API or we can split the information into multiple files and import them into the main file. So basically the line here

import "./api";

imports this directories which contains all the needed models in order to build the API spec.

 tree api
api
|-- common
|   `-- headers.tsp
|-- main.tsp
|-- models
|   |-- error.tsp
|   `-- user.tsp
|-- requests
|   |-- headers
|   |   |-- idempotency_key.tsp
|   |   `-- request_id.tsp
|   |-- params
|   |   `-- id.tsp
|   |-- user_create.tsp
|   `-- user_list.tsp
|-- responses
|   |-- errors.tsp
|   |-- headers
|   |   |-- idempotency_key.tsp
|   |   `-- request_id.tsp
|   |-- main.tsp
|   |-- user.tsp
|   |-- user_created.tsp
|   `-- users_list.tsp
`-- types.tsp

This is a great way to organize your schemas and make them reusable, it is also easy to add more routes and endpoints to your service.

You can do the same thing without using Typespec but let’s leave this for now and we will come back to it later.

Using the DSL allows us to write some code with less code and boilerplate, let’s see this example for errors:

import "../types.tsp";

namespace Users {
    enum ErrorCode {
        BadRequest: 400,
        Unauthorized: 401,
        Forbidden: 403,
        NotFound: 404,
        InternalServerError: 500,
    }

    model Error<ErrorCode> {
        @doc("The error message")
        message: string;

        @doc("The error code")
        code: ErrorCode;
    }

    model NotFoundError extends Error<ErrorCode.NotFound> {
        code: ErrorCode.NotFound;

        @doc("Resource type")
        resource: "user";

        resource_id: ULID;
    }

    model InternalServerError extends Error<ErrorCode.InternalServerError> {
        code: ErrorCode.InternalServerError;
    }
}

We can define errors using the model keyword and we can built other errors on top of the base error.


If you ever tried to version your API using dates instead of numbers you can do it with Typespec, see the example:

enum Versions {
    `2024-12-24`: "2024-12-24",
    `2024-12-25`: "2024-12-25",
}
 tree tsp-output
tsp-output
`-- @typespec
    `-- openapi3
        |-- openapi.2024-12-24.yaml
        `-- openapi.2024-12-25.yaml

This is a great feature because you can have multiple versions of your API and you can generate the OpenAPI spec for each version.


We can generate JSONSchemas from our model definitions, you just need to add the following configuration:

import "@typespec/json-schema";
...
...
using TypeSpec.JsonSchema;

@jsonSchema

tspconfig.yaml
emit:
  - "@typespec/openapi3"
  - "@typespec/json-schema"

and when we run tsp compile .

we will get all the schemas:

tsp-output/@typespec/json-schema
|-- Email.yaml
|-- ErrorCode.yaml
|-- IdsQueryParam.yaml
|-- InternalServerError.yaml
|-- NotFoundError.yaml
|-- ResponseRequestIdHeader.yaml
|-- User.yaml
|-- UserCreateRequest.yaml
|-- UserCreateRequestBody.yaml
|-- UserID.yaml
|-- UserInternalServerErrorResponse.yaml
|-- UserListRequest.yaml
|-- UserListResponse.yaml
|-- UserNotFoundResponse.yaml
|-- UserResponse.yaml
`-- Versions.yaml

So for example the UserID schema:

$schema: https://json-schema.org/draft/2020-12/schema
$id: UserID.yaml
type: string
examples:
  - 01JFGC9X1AZ49CYB89EHAET96K
minLength: 26
maxLength: 26
pattern: ^[0-9a-zA-Z-]{26}$

As you can see it has been generated based on the model definition: reference


Should I use Typespec? Good and Bad

Typespec is ok because:

Typespec is not ok because:


Conclusion

I think it is great to see tools like that but let’s wait until it matures a bit more, I would prefer to create OpenAPI specs directly and then generate the code from it, these days we can use Overlays Use OpenAPI Overlays Today if we need to add any additional information to the OpenAPI spec.