-
Notifications
You must be signed in to change notification settings - Fork 2
REST API Specs & Docs
This guide details the process of creating OpenAPI spec files using TypeSpec and generating documentation from them. We will also outline the roles and responsibilities of the Prime engineering team and the Developer Experience (DevEx) team. It includes examples, a quick start guide for TypeSpec, and common decorators.
- TypeSpec QuickStart: A tool that allows for the creation of OpenAPI spec files using a custom syntax.
-
Roles and Responsibilities:
- Prime Engineering Team: Responsible for writing TypeSpec specs and compiling them into OpenAPI specs.
- DevEx Team: Ensures spec files meet required fields, verifies correct generation of documentation and functionality, and merges the PR to publish the documentation.
The Full TypeSpec documentation can be found here.
The full installation guide can be found on the TypeSpec getting started documentation. Below is a quick breakdown of what you will need.
Create a new file with the .tsp
extension. Below is an example of a simple TypeSpec file.
import "@typespec/http";
import "@typespec/versioning";
import "./Logs/main.tsp";
using TypeSpec.Http;
using VoiceAPI.Logs;
// Top-level service
@service({
title: "Voice API",
version: "1.0.0"
})
@server("https://{space_name}.signalwire.com/api/voice", "Endpoint", {
space_name: string = "{Your_Space_Name}"
})
@useAuth(BasicAuth)
@doc("An API to programmatically access information about SignalWire Voice activity.")
namespace VoiceAPI;
Models
in TypeSpec are utilized to define
the structure or schema of data.
Models can be categorized into two main types:
A Record model is a structure that consists of named fields, referred to as properties.
- The name can be an
identifier
orstring literal
. - The type can be any type reference.
- Properties are arranged in a specific order. Refer to property ordering for more details.
- The properties can be optional or required. A property is optional if it is followed by a
?
symbol. - A
default
value can be assigned to a property by using the=
symbol followed by the default value.
model ChargeDetail {
company_name: string = "SignalWire";
description?: string;
charge?: float64;
}
Arrays are models created using the []
syntax, which is a shorthand for using the
Array<T>
model type.
We can use decorators
in TypeSpec
to attach metadata to types within a TypeSpec project. They can also be used to compute types
based on their inputs. Decorators form the core of TypeSpec's extensibility, providing the
flexibility to describe a wide variety of APIs and associated metadata such as documentation,
constraints, samples, and more.
Below are some common decorators used in TypeSpec files.
Decorator | Description |
---|---|
@body |
Explicitly specify that this property type will be exactly the HTTP body. |
@delete |
Specify the HTTP verb for the target operation to be DELETE . |
@deprecated |
Mark this type as deprecated. |
@doc |
Attach a documentation string. |
@error |
Specify that this model is an error type. Operations return error types when the operation has failed. |
@example |
Attach an example value to a property. |
@format |
Specify a known data format hint for this string type. For example uuid , uri , etc. This differs from the @pattern decorator which is meant to specify a regular expression while @format accepts a known format name. The format names are open ended and are left to emitter to interpret. |
@get |
Specify the HTTP verb for the target operation to be GET . |
@head |
Specify the HTTP verb for the target operation to be HEAD . |
@header |
Specify that this property is to be sent as a header. |
@patch |
Specify the HTTP verb for the target operation to be PATCH . |
@path |
Explicitly specify that this property is to be interpolated as a path parameter. |
@pattern |
Specify the pattern this string should respect using simple regular expression syntax. |
@post |
Specify the HTTP verb for the target operation to be POST . |
@put |
Specify the HTTP verb for the target operation to be PUT . |
@query |
Specify this property is to be sent as a query parameter. |
@route |
Defines the relative route URI for the target operation. The first argument should be a URI fragment that may contain one or more path parameter fields. If the namespace or interface that contains the operation is also marked with a @route decorator, it will be used as a prefix to the route URI of the operation. @route can only be applied to operations, namespaces, and interfaces. |
@server |
Specify the endpoint for this service. |
@service |
Mark this namespace as describing a service and configure service properties. |
@statusCode |
Specify the status code for this response. Property type must be a status code integer or a union of status code integer. |
@summary |
Typically a short, single-line description. |
@tag |
Attaches a tag to an operation, interface, or namespace. Multiple @tag decorators can be specified to attach multiple tags to a TypeSpec element. |
@useAuth |
Specify authentication for a whole service or specific methods. See the documentation in the Http library for full details. |
Operations are essentially service endpoints, characterized by an operation name, parameters, and a return type.
You can declare operations using the op
keyword:
op getDog(): Dog;
The parameters of an operation represent a model. Therefore, you can perform any action with parameters that you can with a model, including the use of the spread operator:
op getDog(name: string, gender?: string, breed?: string): Dog
You can also pass a model and all its properties as a parameter:
op getDog(...DogParams): Dog
Frequently, an endpoint may return one of several possible models. For instance, there could be a return type for when an item is located, and another for when it isn't. Unions are employed to express this scenario:
model Dog {
name: string;
gender: string;
breed: string;
age: int;
model DogNotFound {
error: "Not Found";
}
op getDog(breed: string, gender?: string, name?: string): Dog | DogNotFound
TypeSpec files are structured in a way that allows for modularity and reusability.
We can use TypeSpecs namespace
and
import
features to structure the files
in a way that makes sense for the project.
Below is an example of how to structure a TypeSpec project with multiple directories and files.
We create a VoiceAPI
namespace in the main.tsp
file in the Voice
directory and also create
sub-namespaces for Logs
and Calls
. These sub-namespaces contain a main
file and parameters
, responses
,
and routes
files. The main
file in the sub-namespaces imports the
other files in the same directory. Now when you import either Logs
or Calls
in the main
file in the Voice
directory, all the files in the sub-namespaces are imported.
root/
├── Voice/
│ ├── main.tsp
│ ├── Logs/
│ │ ├── main.tsp
│ │ ├── parameters.tsp
│ │ ├── responses.tsp
│ │ └── routes.tsp
│ └── Calls/
│ ├── main.tsp
│ ├── parameters.tsp
│ ├── responses.tsp
│ └── routes.tsp
import "@typespec/http";
import "@typespec/versioning";
import "./Logs/main.tsp";
import "./Calls/main.tsp";
using TypeSpec.Http;
using VoiceAPI.Logs;
using VoiceAPI.Calls;
// Top-level service
@service({
title: "Voice API",
version: "1.0.0"
})
@server("https://{space_name}.signalwire.com/api/voice", "Endpoint", {
space_name: string = "{Your_Space_Name}"
})
@useAuth(BasicAuth)
@doc("An API to programmatically access information about SignalWire Voice activity.")
namespace VoiceAPI;
import "@typespec/http";
import "./responses.tsp";
import "./parameters.tsp";
import "./routes.tsp";
using TypeSpec.Http;
using VoiceAPI.Logs.Responses;
using VoiceAPI.Logs.Parameters;
using VoiceAPI.Logs.Routes;
Feel free to structure the TypeSpec files in a way that makes sense for the project, the above example is just a suggestion. Typically, aim to have the files organized in a way that makes it easy to find and understand the different parts of the API. Also try to keep the files organized in a way that resembles the API structure and how they will be generated on the Documentation site.
To compile the TypeSpec file to OpenAPI, navigate to the directory containing the main .tsp
file and run the following command:
tsp compile .
This command will generate a directory called tsp-output
containing the OpenAPI spec file.
If our file structure looked like the below example:
root/
├── voice-api/
│ ├── main.tsp
│ ├── tsp-output/
│ │ └── @typespec/
│ │ └── openapi3/
│ │ └── openapi.yaml
│ └── Logs/
│ ├── main.tsp
│ ├── parameters.tsp
│ ├── responses.tsp
│ └── routes.tsp
├── messaging-api/
│ ├── main.tsp
│ ├── tsp-output/
│ │ └── @typespec/
│ │ └── openapi3/
│ │ └── openapi.yaml
│ └── Logs/
│ ├── main.tsp
│ ├── parameters.tsp
│ ├── responses.tsp
│ └── routes.tsp
├── fax-api/
│ ├── main.tsp
│ ├── tsp-output/
│ │ └── @typespec/
│ │ └── openapi3/
│ │ └── openapi.yaml
│ └── Logs/
│ ├── main.tsp
│ ├── parameters.tsp
│ ├── responses.tsp
│ └── routes.tsp
To compile the main.tsp
file in the voice-api
directory, run the following command:
cd voice-api && tsp compile .
To help automate the process of compiling the TypeSpec files, we have added a tsp compile .
command for each API. Below is an example of the scripts
section in the package.json
file.
"scripts": {
"build": "npm run build:all",
"build:calling-api": "cd ./calling && tsp compile . && cd ..",
"build:chat-api": "cd ./chat && tsp compile . && cd ..",
"build:fabric-api": "cd ./fabric && tsp compile . && cd ..",
"build:fax-api": "cd ./fax && tsp compile . && cd ..",
"build:messaging-api": "cd ./messaging && tsp compile . && cd ..",
"build:project-api": "cd ./project && tsp compile . && cd ..",
"build:pubsub-api": "cd ./pubsub && tsp compile . && cd ..",
"build:space-api": "cd ./relay-rest && tsp compile . && cd ..",
"build:video-api": "cd ./video && tsp compile . && cd ..",
"build:voice-api": "cd ./voice && tsp compile . && cd ..",
"build:twiml-api": "cd ./twiml_compatible && tsp compile . && cd ..",
"build:all": "npm run build:calling-api && npm run build:chat-api && npm run build:fabric-api && npm run build:fax-api && npm run build:messaging-api && npm run build:project-api && npm run build:pubsub-api && npm run build:space-api && npm run build:video-api && npm run build:voice-api && npm run build:twiml-api"
},
Running npm run build
will compile all the APIs. If a new API is added, you can add a new
build:api-name
script to the package.json
file.
The Prime engineering team is responsible for writing the TypeSpec specs and compiling them into OpenAPI specs. The team is also responsible for ensuring that the specs are accurate and up-to-date. The Prime team will work closely with the DevEx team to ensure that the specs meet the required fields and that the documentation is generated correctly.
Responsibility breakdown:'
- Writing TypeSpec specs for the REST API.
- Compiling the TypeSpec specs into OpenAPI specs.
- Ensuring that the specs are accurate and up-to-date.
- Working closely with the DevEx team to ensure that the specs meet the required fields for documentation.
- Pushing the initial PR to the
docs
repository.
To get started with TypeSpec, follow the Quick Start Guide above.
Once you have completed the installation and have familiarized yourself with the syntax, you can
start writing TypeSpec spec files in the api
directory of the docs
repository.
Here you will find two directories, compatibility-api
and signalwire-rest
.
The compatibility-api
directory contains the TypeSpec files for the TwiML compatible API while
the signalwire-rest
directory contains the TypeSpec files for all SignalWire REST API.
Here you can create new TypeSpec files or update existing ones.
When writing TypeSpec specs, ensure that the following fields are included:
-
@service
decorator with thetitle
andversion
properties.- The
version
property will give deprecated warnings when included in the@service
decorator. Ignore this for now.@service({ title: "Voice API", version: "1.0.0" })
- The
-
@server
decorator with theurl
,name
, andparameters
properties.- The
parameters
property should include thespace_name
parameter with a default value.@server("https://{space_name}.signalwire.com/api/voice", "Endpoint", { space_name: string = "{Space_Name}" })
- The
-
@useAuth
decorator with the authentication method.- The authentication method should be
BasicAuth
. These are imported from the@typespec/http
library.@useAuth(BasicAuth)
- The authentication method should be
-
@doc
decorator with a brief description of the API. This is important for the documentation.- We should use this decorator whereever possible to provide a description of the API.
model ListLogsParams { @query @doc("Include logs for deleted activity. \n\n**Example**: false") include_deleted?: boolean, @query @doc("Return logs for activity prior to this date. \n\n**Example**: 2022-04-30") created_before?: string, @query @doc("Return logs for activity on this date. \n\n**Example**: 2022-04-30") created_on?: string, @query @doc("Return logs for activity after this date. \n\n**Example**: 2022-04-30") created_after?: string, @query @doc("Specify the number of results to return on a single page. The default page size is `50` and the maximum is `1000`. \n\n**Example**: 20") page_size?: int32 }
- We should use this decorator whereever possible to provide a description of the API.
-
@example
decorator with an example value for the property.- This is important for the documentation to provide examples of the property for users to compare.
model ListLogsParams { @query @doc("Include logs for deleted activity.") @example false include_deleted?: boolean, @query @doc("Return logs for activity prior to this date.") @example "2022-04-30" created_before?: string, @query @doc("Return logs for activity on this date.") @example "2022-04-30" created_on?: string, @query @doc("Return logs for activity after this date.") @example "2022-04-30" created_after?: string, @query @doc("Specify the number of results to return on a single page. The default page size is `50` and the maximum is `1000`.") @example 20 page_size?: int32 }
- This is important for the documentation to provide examples of the property for users to compare.
-
@summary
decorator with a short, single-line description.@summary("This is a pet") model Pet {}
-
@tag
decorator to attach a tag to an operation, interface, or namespace.- This is extremely important for grouping operations in the documentation.
@tag("Logs")
- This is extremely important for grouping operations in the documentation.
-
@statusCode
decorator to specify the status code for the response.op read(): { @statusCode _: 200; @body pet: Pet; }; op create(): { @statusCode _: 201 | 202; };
If you are adding a new API directory, ensure that the directory structure is similar to the
existing directories. This will make it easier to maintain and update the API specs.
Also make sure to add the new API directory to the package.json
file so that the API can be
compiled with the other APIs.
Once you have written the TypeSpec specs, you can compile them into OpenAPI specs by running the
npm run build
command. This will compile all the API specs and generate the OpenAPI spec files
in the tsp-output
directory.
After compiling the TypeSpec specs into OpenAPI specs, push the initial PR. This push should include the OpenAPI spec files and any changes to the existing TypeSpec file or new TypeSpec files.
Once the PR is created, assign it to the DevEx team for review by assigning the label team/developer-experience
.
The DevEx team will review the PR to ensure that the specs meet the required fields and that
the documentation is generated correctly.