-
Notifications
You must be signed in to change notification settings - Fork 38
Spot Guide
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.
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.
API operations are defined by the class decorator @endpoint
:
@endpoint({
method: "GET",
path: "/companies/:id"
})
class GetCompany {
//...
}
For details and available configuration see @endpoint.
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.
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.
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.
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 |
Exact values can be used as types:
role: "admin"
These should be used sparingly.
Optional fields can specified by adding a ?
to the end of the field name:
class GetUsers {
@request
request(@queryParams queryParams: { page?: Integer });
}
This is the most used data structure. Object properties can be of any type:
{ firstName: String; lastName: String; age: Integer }
nicknames: String[]
Unions describe types that may be one of a list of types:
role: "superuser" | "admin" | "member"
, param: String | Number
//...
notFoundResponse(@body body: ErrorBody) {}
//...
interface ErrorBody {
message: String;
code: Integer;
}
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;
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 {
// ...
}
See Contract Testing