Skip to content

REST API Specs & Docs

Devon edited this page Dec 30, 2024 · 1 revision

Process for creating REST API documentation

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.

Overview

  • 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.

Quick Start with TypeSpec

The Full TypeSpec documentation can be found here.

Installation

The full installation guide can be found on the TypeSpec getting started documentation. Below is a quick breakdown of what you will need.

  • Install the TypeSpec compiler/CLI
    npm install -g @typespec/compiler
  • Install VSCode or VS extension.

Creating a TypeSpec File

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;

TypeSpec Models

Models in TypeSpec are utilized to define the structure or schema of data.

Types of Models

Models can be categorized into two main types:

Record

A Record model is a structure that consists of named fields, referred to as properties.

  • The name can be an identifier or string 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;
}
Array

Arrays are models created using the [] syntax, which is a shorthand for using the Array<T> model type.

Common TypeSpec Decorators

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.

TypeSpec Operations

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;

Parameters

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

Return Type

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

Structuring TypeSpec Files

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

Example of the main.tsp file in the Voice directory

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;

Example of the main.tsp file in the Logs directory

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.

Compiling TypeSpec to OpenAPI

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.

Example of Generating OpenAPI Spec

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 .

Compiling from package.json

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.


Prime Engineering Team Role & Responsibilities

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.

Getting Started with writing REST API specs

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.

Documentation Required Decorators

When writing TypeSpec specs, ensure that the following fields are included:

  • @service decorator with the title and version 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"
        })
  • @server decorator with the url, name, and parameters properties.

    • The parameters property should include the space_name parameter with a default value.
        @server("https://{space_name}.signalwire.com/api/voice", "Endpoint", {
          space_name: string = "{Space_Name}"
        })
  • @useAuth decorator with the authentication method.

    • The authentication method should be BasicAuth. These are imported from the @typespec/http library.
        @useAuth(BasicAuth)
  • @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
      }
  • @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
      }
  • @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")
  • @statusCode decorator to specify the status code for the response.

    op read(): {
      @statusCode _: 200;
      @body pet: Pet;
    };
    op create(): {
      @statusCode _: 201 | 202;
    };

Adding New API Directories

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.

Compiling the Spec Files

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.

Pushing the Initial PR and passing the PR to the DevEx Team

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.


DevEx Team Role & Responsibilities

WIP