Skip to content

Spot Guide

Leslie Fung edited this page Jan 28, 2020 · 1 revision
  1. New Spot project
  2. Meta Information
  3. Endpoints
  4. Types
  5. Contract File Structure
  6. Testing

1. New Spot project

npx @airtasker/spot init will generate a new project with the latest version of Spot, tsconfig.json and a small sample contract to get started with.

2. Meta Information

Some meta information is required for a Spot contract to assist with code generation and testing frameworks. This information is defined on the @api class decorator:

@api({ name: "Company API" })
class Api {}

Meta information must be defined only once and in the root contract file (see Contract File Structure).

For details and available configuration see @api.

3. Endpoints

API operations are defined by the class decorator @endpoint:

@endpoint({
  method: "GET",
  path: "/companies/:id"
})
class GetCompany {
  //...
}

For details and available configuration see @endpoint.

Endpoint Request

HTTP request properties are defined on a method annotated with @request. Request headers, path parameters, query parameters and bodies are defined via annotated parameters on the method:

@endpoint({
  method: "POST",
  path: "/companies"
})
class CreateCompany {
  @request
  request(
    @headers
    headers: {
      "My-Request-Header": String;
    },
    @body body: CompanyBody
  ) {}
  //...
}

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

interface CompanyBody {
  name: String;
}

Only one @request can be defined for each endpoint.

For details and available configuration see @request, @headers, @pathParams, @body.

Endpoint Response

HTTP response properties are defined on methods annotated with @response. Response headers and bodies are defined via annotated parameters on the method:

@endpoint({
  method: "GET",
  path: "/companies/:id"
})
class GetCompany {
  @request
  request(@pathParams pathParams: { id: String }) {}

  @response({ status: 200 })
  successResponse(
    @headers
    headers: {
      "My-Response-Header": String;
    },
    @body body: CompanyBody
  ) {}

  @response({ status: 400 })
  notFoundResponse(@body body: ErrorBody) {}
}

interface CompanyBody {
  name: String;
}

interface ErrorBody {
  message: String;
}

For details and available configuration see @response, @headers, @pathParams and @body.

Default Response

Endpoint may return errors with multiple different HTTP status codes where many or all of them have the same response structure. A default response can be defined to describe these errors collectively rather than individually defining them. Only one @defaultResponse may be defined for an endpoint. It has the same structure as a specific @response without a defined status code:

@endpoint({
  method: "POST",
  path: "/companies/:id/users"
})
class CreateUser {
  @request
  request(
    @pathParams
    pathParams: {
      id: String;
    },
    @body body: CreateUserBody
  ) {}

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

  @defaultResponse
  default(@body body: GenericErrorBody) {}
}

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

interface UserBody {
  name: String;
}

interface GenericErrorBody {
  message: String;
}

For details see @defaultResponse, @headers, @pathParams and @body.

4. Types

Native Types

Type Description Example
null The null value avatar: null
String A string value name: String
Integer An integer (alias for Int32) age: Integer
Int32 A 32-bit integer age: Int32
Int64 A 64-bit integer timestamp: Int64
Number A floating point number (alias for Float) average: Number
Float An floating point number average: Float
Double A double precision floating point number average: Double
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

Constants

Exact values can be used as types:

role: "admin"

These should be used sparingly.

Optional Fields

Optional fields can specified by adding a ? to the end of the field name:

class GetUsers {
  @request
  request(@queryParams queryParams: { page?: Integer });
}

Objects

This is the most used data structure. Object properties can be of any type:

{ firstName: String; lastName: String; age: Integer }

Arrays

nicknames: String[]

Unions

Unions describe types that may be one of a list of types:

role: "superuser" | "admin" | "member", param: String | Number

Custom Object Types

//...
  notFoundResponse(@body body: ErrorBody) {}
//...

interface ErrorBody {
  message: String;
  code: Integer;
}

Type Alias

Types can be aliased to more easily read and understand a contract:

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

type UserIdentifier = String;

5. Contract File Structure

The contract file can easily become large very quickly even with just a couple of endpoints. This is where splitting up the contract can be useful. Spot contracts can be split using import statements:

// api.ts

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

import "./endpoints/createcompany";

@api({ name: "Company API" })
class Api {}
// endpoints/createcompany.ts

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

@endpoint({
  method: "POST",
  path: "/companies"
})
class CreateCompany {
  // ...
}

And recursively:

// api.ts

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

import "./endpoints/userendpoints/index";

@api({ name: "Company API" })
class Api {}
// endpoints/userendpoints/index.ts

import "./getuser";
import "./createuser";
// endpoints/userendpoints/getuser.ts

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

@endpoint({
  method: "GET",
  path: "/companies/:companyId/users/:id"
})
class GetUser {
  // ...
}
// endpoints/userendpoints/createuser.ts

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

@endpoint({
  method: "POST",
  path: "/companies/:id/users"
})
class CreateUser {
  // ...
}

6. Testing

See Contract Testing