Skip to content

Spot Syntax

Leslie Fung edited this page Jun 23, 2021 · 9 revisions

@api

Define an API. This is required and must only be defined once:

import { api } from "@airtasker/spot";

@api({ name: "My API" })
class MyAPI {}
Field Description
name (required) Name of the API. Name of the providing service. The name must be unique to avoid clashes when working with multiple Spot contracts across different services.

@securityHeader

Define a global security header. This should be used within an @api class:

import { api, securityHeader, String } from "@airtasker/spot";

@api({ name: "My API" })
class MyAPI {
  @securityHeader
  "security-header": String;
}

Security headers can only be string types.

@config

Global configuration used for the API. This should be only be defined once on the @api class:

import { api, config } from "@airtasker/spot";

/** My really cool API */
@api({ name: "My API" })
@config({
  paramSerializationStrategy: {
    query: {
      array: "comma"
    }
  }
})
class MyAPI {}
Field Description
paramSerializationStrategy.query.array Serialization strategy for query parameter array values. This can be ampersand (default) or comma

Query parameter serialization strategies

@endpoint({ ... })
class AnEndpoint {
  @request
  request(
    @queryParams
    queryParams: {
      id: Int32[]
    }
  ) {}
}

For id = [3,4,5]:

  • ampersand (default): ?id=3&id=4&id=5
  • comma: ?id=3,4,5

@endpoint

Define a HTTP endpoint for the API. An endpoint describes a particular HTTP action on a URL path:

import {
  body,
  defaultResponse,
  endpoint,
  headers,
  pathParams,
  queryParams,
  request,
  response,
  String
} from "@airtasker/spot";

/** Retrieve all users */
@endpoint({
  method: "GET",
  path: "/users",
  tags: ["Users"]
})
class GetUsers {
  @request
  request(
    @queryParams
    queryParams: {
      search?: String;
    }
  ) {}

  @response({ status: 200 })
  response(@body body: UserListResponse) {}
}

/** Retrieve a user by their unique identifier */
@endpoint({
  method: "GET",
  path: "/users/:id",
  tags: ["Users"]
})
class GetUser {
  @request
  request(
    @pathParams
    pathParams: {
      /** Unique user identifier */
      id: String;
    }
  ) {}

  @response({ status: 200 })
  successResponse(@body body: UserResponse) {}

  @response({ status: 404 })
  notfoundResponse(@body body: ApiErrorResponse) {}
}

/** Create a user */
@endpoint({
  method: "POST",
  path: "/users",
  tags: ["Users"]
})
class CreateUser {
  @request
  request(
    @headers
    headers: {
      /** The authorization token */
      Authorization: String;
    },
    @body body: CreateUserRequest
  ) {}

  @response({ status: 201 })
  successResponse(
    @headers
    headers: {
      Location: String;
    },
    @body body: UserResponse
  ) {}

  @response({ status: 401 })
  unauthorizedResponse(@body body: ApiErrorResponse) {}

  @defaultResponse
  defaultResponse(@body body: ApiErrorResponse) {}
}

interface User {
  firstName: String;
  lastName: String;
}

type UserResponse = User;
type UserListResponse = User[];

interface CreateUserRequest {
  firstName: String;
  lastName: String;
}

interface ApiErrorResponse {
  message: String;
}
Field Description
method (required) HTTP method
path (required) URL path
tags List of tags used for endpoint grouping in documentation

@request

Define a request for endpoints that require headers, path params, query params or request bodies:

import {
  body,
  endpoint,
  headers,
  pathParams,
  queryParams,
  request,
  String
} from "@airtasker/spot";

@endpoint({
  method: "POST",
  path: "/company/:companyId/users"
})
class CreateUser {
  @request
  request(
    @headers
    headers: {
      Authorization: String;
    },
    @pathParams
    pathParams: {
      companyId: String;
    },
    @body body: CreateUserRequest
  ) {}
  //...
}

@endpoint({
  method: "GET",
  path: "/users"
})
class GetUsers {
  @request
  request(
    @queryParams
    queryParams: {
      search?: String;
    }
  ) {}
  //...
}

interface CreateUserRequest {
  firstName: String;
  lastName: String;
}

@headers

Define request or response headers:

import {
  endpoint,
  headers,
  Int32,
  request,
  response,
  String
} from "@airtasker/spot";

@endpoint({
  method: "GET",
  path: "/users"
})
class GetUsers {
  @request
  request(
    @headers
    headers: {
      Authorization: String;
      "Accept-Encoding": String;
    }
  ) {}

  @response({ status: 200 })
  response(
    @headers
    headers: {
      Age: Int32;
    }
    //...
  ) {}
  //...
}

@pathParams

Define path parameters that appear in the path provided in @endpoint(). For example if the path is /users/:id, the endpoint method must define a matching property within the @pathParams decorated argument:

import { endpoint, pathParams, request, String } from "@airtasker/spot";

@endpoint({
  method: "GET",
  path: "/users/:id"
})
class GetUser {
  @request
  request(
    @pathParams
    pathParams: {
      id: String;
    }
    //...
  ) {}
  //...
}

Note: the name of the property must match the name of the path parameter.

@queryParams

Define query parameters:

import { endpoint, queryParams, request, String } from "@airtasker/spot";

@endpoint({
  method: "GET",
  path: "/users"
})
class GetUsers {
  @request
  request(
    @queryParams
    queryParams: {
      search?: String;
    }
    //...
  ) {}
  //...
}

@body

Define request or response body:

import { body, endpoint, request, response } from "@airtasker/spot";

@endpoint({
  method: "POST",
  path: "/users"
})
class CreateUser {
  @request
  request(@body body: CreateUserBody) {}

  @response({ status: 201 })
  response(@body body: UserBody) {}
  //...
}

interface CreateUserBody {
  firstName: String;
  lastName: String;
}

interface UserBody {
  name: String;
}

@response

Define a known response for the endpoint. Responses are identified by their HTTP status code. @response can be used multiple times to define multiple responses:

import { body, endpoint, request, response, String } from "@airtasker/spot";

@endpoint({
  method: "POST",
  path: "/users"
})
class CreateUser {
  @request
  resquest(@body body: CreateUserRequest) {}

  @response({ status: 201 })
  successResponse(@body body: UserResponse) {}

  @response({ status: 400 })
  badRequestResponse(@body body: ApiError) {}

  //...
}

interface CreateUserRequest {
  firstName: String;
  lastName: String;
}

interface UserResponse {
  firstName: String;
  lastName: String;
  role: String;
}

interface ApiError {
  message: String;
}
Field Description
status (required) HTTP status code for the error

@defaultResponse

Define a default response for the endpoint. The default response represents the default format of a response when no other specific @response is matched (by status code). This can only be used once for an @endpoint:

import {
  body,
  defaultResponse,
  endpoint,
  request,
  response,
  String
} from "@airtasker/spot";

@endpoint({
  method: "POST",
  path: "/users"
})
class CreateUser {
  @request
  request(@body body: CreateUserRequest) {}

  @response({ status: 201 })
  successResponse(@body body: UserResponse) {}

  @defaultResponse
  defaultResponse(@body body: ApiError) {}

  //...
}

interface CreateUserRequest {
  firstName: String;
  lastName: String;
}

interface UserResponse {
  firstName: String;
  lastName: String;
  role: String;
}

interface ApiError {
  message: String;
}

Types

Type Description Example
null The null value details: null
String A string value name: String
Number A floating point number (alias for Float) percentage: Number
Float A floating point number percentage: Float
Double A double precision floating point number percentage: Double
Integer An integer (alias for Int32) age: Integer
Int32 A signed 32 bit integer age: Int32
Int64 A signed 64 bit integer numAtoms: Int64
boolean A boolean value isAdmin: boolean
Date A full-date as defined by https://tools.ietf.org/html/rfc3339#section-5.6 dateOfBirth: Date
DateTime A date-time as defined by https://tools.ietf.org/html/rfc3339#section-5.6 createdAt: DateTime
Constant An exact value role: "admin"
Union One-of role: "admin" | "member", param: string | number
Array Collection nicknames: String[]
Object An object person: { firstName: String; lastName: String; }

Optionality

Attributes can be marked as optional using TypeScript's standard optional ? token:

@body
body: {
  firstName: String;
  lastName: String;
  middleName?: String; // optional
}

Supported HTTP Methods

HEAD, GET, POST, PUT, PATCH, DELETE

Documentation

Documentation is provided via JSDoc. These are used where appropriate when generating other document formats such as OpenAPI:

 /** Description */

 /**
    * Description
    */

🚫 // Description

🚫 /* Description */
import {
  api,
  body,
  endpoint,
  headers,
  Int32,
  pathParams,
  queryParams,
  request,
  response,
  securityHeader,
  String
} from "@airtasker/spot";

/** My API description */
@api({ name: "My API" })
class MyAPI {
  /** Security header description */
  @securityHeader
  "security-header": String;
}

/** My endpoint description */
@endpoint({
  method: "GET",
  path: "/users/:id/posts"
})
class MyEndpoint {
  @request
  request(
    @pathParams
    pathParams: {
      /** Id path param description */
      id: Int32;
    },
    @queryParams
    queryParams: {
      /** Visible query param description */
      visible?: boolean;
    },
    @headers
    headers: {
      /** My header description */
      "X-My-Header": String;
    }
  ) {}

  /** Ok response description */
  @response({ status: 200 })
  okResponse(
    @body
    body: ResponseBodyType
  ) {}
}

/** Response body type description */
interface ResponseBodyType {
  /** Property description */
  property: String;
}

OpenAPI 3 Servers

Define an OpenAPI 3 Server. This may be defined multiple times:

import { api, oa3server, oa3serverVariables } from "@airtasker/spot";

@api({
  name: "My API",
  version: "1"
})
class Api {
  /**
   * The production API server
   */
  @oa3server({ url: "https://{username}.gigantic-server.com:{port}/{basePath}" })
  productionServer(
    @oa3serverVariables
    variables: {
      /**
       * this value is assigned by the service provider, in this example `gigantic-server.com`
       * 
       * @default "demo"
       */
      username: String,
      /**
       * @default "8443"
       */
      port: "8443" | "443",
      /**
       * @default "v2"
       */
      basePath: String
    }
  ) {}
}