@saif/platform-typespec¶
Reference for the SAIF.Platform TypeSpec conventions library β the operation templates, status-code models, auth helpers, and eventing contracts that Forge templates generate against.
This page documents Forge's conventions layer, not TypeSpec itself
For the TypeSpec language β models, decorators, templates, is vs extends, versioning β read the TypeSpec documentation. This page only covers what @saif/platform-typespec adds on top.
π Summary¶
@saif/platform-typespec is the library every Forge API contract imports. It supplies:
| Area | Namespace members | Source |
|---|---|---|
| Operation templates | ResourceRead, ResourceList, ResourceCreate, ResourceCreateOrReplace, ResourcePartialUpdate, ResourceDelete, and the action variants |
lib/operations/ |
| Status-code models | Ok, Created, Accepted, NoContent, BadRequest, NotFound, AuthErrors, ServerError, and the rest of 2xx/3xx/4xx/5xx |
lib/status-codes/ |
| Auth helpers | Scopes<>, Roles<> |
lib/openapi/oauth.tsp |
| Server helpers | MockingServer<> |
lib/openapi/server.tsp |
| Eventing | Topic<>, Event<>, BaseEvent |
lib/eventing/ |
Everything lives in the SAIF.Platform namespace. One import and one using make the whole surface available:
The value of the library is that error contracts, status codes, and route shapes are decided once. An operation written as ResourceRead<Order> already returns RFC 7807 Problem Details for 400, 401, 403, 404, and 5xx without you writing a single error model.
π€οΈ Operation Templates¶
Operation templates are used with TypeSpec's is keyword inside an interface. Each one applies @autoRoute, the correct HTTP verb, the matching @β¦Resource REST decorator, and a default error union.
@autoRoute
@added(Versions.v1)
interface Orders {
get is ResourceRead<Order>;
all is ResourceList<Order>;
post is ResourceCreate<Order>;
put is ResourceCreateOrReplace<Order>;
patch is ResourcePartialUpdate<Order>;
delete is ResourceDelete<Order>;
}
Routes come from the model's @resource name and @key property, so @route is never written by hand.
CRUD templates¶
| Template | Verb | Route | Default success | Default errors |
|---|---|---|---|---|
ResourceRead<Resource> |
GET |
/{collection}/{key} |
OkResponse<Resource> (200) |
BadRequest \| NotFound \| AuthErrors \| ServerError |
ResourceList<Resource> |
GET |
/{collection} |
OkResponse<Resource[]> (200) |
BadRequest \| AuthErrors \| ServerError |
ResourceCreate<Resource> |
POST |
/{collection} |
CreatedResponse<Resource> (201) |
BadRequest \| AuthErrors \| ServerError |
ResourceCreateOrReplace<Resource> |
PUT |
/{collection}/{key} |
OkResponse<Resource> \| CreatedResponse<Resource> (200/201) |
BadRequest \| NotFound \| AuthErrors \| ServerError |
ResourcePartialUpdate<Resource> |
PATCH |
/{collection}/{key} |
OkResponse<Resource> (200) |
BadRequest \| NotFound \| AuthErrors \| ServerError |
ResourceDelete<Resource> |
DELETE |
/{collection}/{key} |
NoContent (204) |
BadRequest \| AuthErrors \| ServerError |
AuthErrors is an alias for Unauthorized | Forbidden, so every template above already covers 401 and 403.
Action templates¶
Actions hang off a resource (/{collection}/{key}/{action}) or its collection (/{collection}/{action}). Use them when an operation is a verb rather than a CRUD effect on the resource itself.
| Template | Verb | Target | Default success | Default errors |
|---|---|---|---|---|
ResourceAction<Resource> |
POST |
instance | OkResponse<Resource> (200) |
BadRequest \| NotFound \| AuthErrors \| ServerError |
ResourceActionAsync<Resource> |
POST |
instance | Accepted (202) |
BadRequest \| NotFound \| AuthErrors \| ServerError |
ResourceCollectionAction<Resource> |
POST |
collection | OkResponse<Resource> (200) |
BadRequest \| AuthErrors \| ServerError |
ResourceCollectionActionAsync<Resource> |
POST |
collection | Accepted (202) |
BadRequest \| AuthErrors \| ServerError |
ResourceReadAction<Resource> |
GET |
instance | OkResponse<Resource> (200) |
BadRequest \| NotFound \| AuthErrors \| ServerError |
ResourceCollectionReadAction<Resource> |
GET |
collection | OkResponse<Resource> (200) |
BadRequest \| AuthErrors \| ServerError |
The POST action templates take a Body parameter that defaults to void, so an action with no request body needs no extra arguments:
@autoRoute
interface Orders {
// POST /orders/{id}/cancel
cancel is ResourceAction<Order>;
// POST /orders/{id}/refund with a request body
refund is ResourceAction<Order, {}, RefundRequest>;
// POST /orders/export β 202 Accepted
export is ResourceCollectionActionAsync<Order>;
}
Template parameters¶
Templates fall into two positional signatures. Supply {} for any parameter you want to leave at its default.
No Body parameter β ResourceRead, ResourceList, ResourceCreate, ResourceCreateOrReplace, ResourceDelete, ResourceReadAction, ResourceCollectionReadAction. The body is derived from Resource itself (via ResourceCreateableProperties<T> for creates), not supplied as an argument.
| Position | Parameter | Purpose |
|---|---|---|
| 1 | Resource |
The resource model. Drives the route, the key parameters, and the derived body. |
| 2 | Parameters |
Extra request parameters merged in β query strings, headers, additional path segments. |
| 3 | Response |
Overrides the success response. |
| 4 | Error |
Overrides the error union. |
Has a Body parameter β ResourcePartialUpdate, ResourceAction, ResourceActionAsync, ResourceCollectionAction, ResourceCollectionActionAsync.
| Position | Parameter | Purpose |
|---|---|---|
| 1 | Resource |
The resource model. Drives the route and the key parameters. |
| 2 | Parameters |
Extra request parameters merged in β query strings, headers, additional path segments. |
| 3 | Body |
Overrides the request body model. Defaults to ResourceUpdateableProperties<Resource> for ResourcePartialUpdate, void for action templates. |
| 4 | Response |
Overrides the success response. |
| 5 | Error |
Overrides the error union. |
{} at the Body position is a real empty model, not \"skip this argument\"
{} satisfies Body extends {} as an actual empty-object body, distinct from omitting the argument to fall back to the default. Only omit trailing arguments to keep defaults; do not pass {} positionally unless you mean an empty body.
Adding query parameters:
model OrderFilter {
@query status?: OrderStatus;
@query skip?: int32 = 0;
@query take?: int32 = 20;
}
@autoRoute
interface Orders {
all is ResourceList<Order, OrderFilter>;
}
Extending the error union β keep the defaults and add to them rather than replacing them:
@autoRoute
interface Orders {
post is ResourceCreate<
Order,
{},
CreatedResponse<Order>,
BadRequest | Conflict | AuthErrors | ServerError
>;
}
Overriding Error replaces the whole union
The Error parameter is not additive. If you pass a value, restate every error the operation can return, including AuthErrors and ServerError. Dropping them silently removes 401, 403, and 5xx from the generated OpenAPI and from every generated client.
Overriding the response:
model OrderPage {
items: Order[];
totalCount: int32;
continuationToken?: string;
}
@autoRoute
interface Orders {
all is ResourceList<Order, OrderFilter, OkResponse<OrderPage>>;
}
Body-shaping models¶
The create and update templates derive their request bodies from the resource, so you do not define separate request models:
| Model | Used by | Effect |
|---|---|---|
ResourceCreateableProperties<T> |
ResourceCreate |
Applies Lifecycle.Create visibility, dropping read-only properties. Emits as {Name}Create. |
ResourceUpdateableProperties<T> |
ResourcePartialUpdate |
Makes properties optional for a partial update. Emits as {Name}Update. |
ResourceInstanceParameters<T, P> |
instance operations | Merges P with the resource's @key properties. |
ResourceCollectionParameters<T, P> |
collection operations | Merges P with the parent resource's keys. |
Mark server-owned fields with @visibility(Lifecycle.Read) so they are excluded from create bodies automatically.
Base operation templates¶
ReadOperation, ActionOperation, CreateOperation, UpdateOperation, and DeleteOperation are the unrouted primitives the resource templates are built from. Use them only when you need an operation that is not resource-shaped β for everything else, the Resource* templates carry the routing and REST metadata you would otherwise write by hand.
π’ Status-Code Models¶
Success and redirection¶
Bare status-code markers with no body. Combine them with a body in a response model, or use them directly for empty responses.
| Range | Models |
|---|---|
| 2xx | Ok (200), Created (201), Accepted (202), NonAuthoritativeInformation (203), NoContent (204), ResetContent (205), PartialContent (206), MultiStatus (207), AlreadyReported (208), IMUsed (226) |
| 3xx | MultipleChoices (300), MovedPermanently (301), Found (302), SeeOther (303), NotModified (304), TemporaryRedirect (307), PermanentRedirect (308) |
Success and Redirection match any status in their range, for cases where the exact code is not fixed.
Two wrappers pair a status code with a body:
OkResponse<T>β 200 withTas the bodyCreatedResponse<T>β 201 withTas the body
Errors¶
Every error model extends ProblemDetails, so all error responses are RFC 7807 with Content-Type: application/problem+json.
{
"type": "https://example.com/probs/order-not-found",
"title": "Not Found",
"status": 404,
"detail": "Order 8f3a1c02 does not exist.",
"instance": "/orders/8f3a1c02",
"requestId": "0HN7GQ1V2K3M4",
"traceId": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
}
requestId, traceId, and errors are SAIF extensions to the RFC. errors is an array of { key, values } pairs and carries field-level validation failures.
| Model / alias | Status | Notes |
|---|---|---|
BadRequest |
400 | Validation failures. Populate errors with the offending fields. |
Unauthorized |
401 | Missing or invalid token. |
Forbidden |
403 | Valid token, insufficient scope or role. |
AuthErrors |
401, 403 | Alias for Unauthorized \| Forbidden. Present in every template default. |
NotFound |
404 | |
Conflict |
409 | Optimistic-concurrency and duplicate-key failures. |
UnprocessableEntity |
422 | Semantically invalid but well-formed request. |
TooManyRequests |
429 | Rate limiting. |
ClientError |
400β499 | Matches any 4xx. |
InternalServerError |
500 | |
ServiceUnavailable |
503 | |
GatewayTimeout |
504 | |
ServerError |
500β599 | Matches any 5xx. Present in every template default. |
The full set of RFC-defined 4xx and 5xx codes is available β MethodNotAllowed, NotAcceptable, RequestTimeout, Gone, PreconditionFailed, PayloadTooLarge, UnsupportedMediaType, Locked, UpgradeRequired, NotImplemented, BadGateway, and the rest β each named after its RFC reason phrase. See lib/status-codes/client-errors.tsp and lib/status-codes/server-errors.tsp for the complete list.
Do not hand-roll error models
A custom @error model NotFoundError { code: string; message: string } produces a response shape no other SAIF API returns, and no shared client handler understands. Use NotFound and put the specifics in detail and type.
Building custom status models¶
StatusCode<N> and StatusCodeRange<Min, Max> are the primitives behind every model above. Reach for them only when you need a code the library does not name.
model PaymentRequired extends ProblemDetails {
status: int32 = 402;
...StatusCode<402>;
...OmitProperties<ProblemDetailsProperties, "title" | "status" | "statusCode">;
}
π Auth Helpers¶
Scopes<> and Roles<> are OAuth2 authorization-code flows preconfigured for the platform. One @useAuth declaration covers both identity providers; the generated APIM policies check the correct claim per provider.
| Helper | Checks | Claim | Granted to |
|---|---|---|---|
Scopes<[...]> |
Client.* scopes |
scp |
Calling applications |
Roles<[...]> |
App.* roles |
roles (Entra) / user-groups (Okta) |
Users via Business Roles, or applications |
The | union means either satisfies the requirement β an Okta caller presenting Client.Read and an Entra caller presenting the App.Read role both pass.
Declare the default at the namespace and override per operation where a stronger permission is needed:
@autoRoute
interface Orders {
get is ResourceRead<Order>;
@useAuth(Scopes<["Client.Write"]> | Roles<["App.Write"]>)
post is ResourceCreate<Order>;
@useAuth(Scopes<["Client.Delete"]> | Roles<["App.Admin"]>)
delete is ResourceDelete<Order>;
}
Never reference platform-managed delegation scopes
user_impersonation (Entra), user-groups (Okta), and the Experience-API access scope are handled by the platform. Do not name them in @useAuth.
Permission names must follow the permission naming conventions and be defined in both providers β see App Permissions.
The URLs in the emitted OpenAPI are placeholders
Scopes<> and Roles<> declare https://example.com/oauth2/* endpoints. Real authorization, token, and refresh URLs are injected by APIM at deployment time β do not replace them in your contract.
π Server Helpers¶
MockingServer<> marks a @server entry as a mock endpoint by adding mocking_server: "true" to its variables. Requests routed to that server are served by WireMock Cloud instead of your backend.
@server("http://localhost:21000", "localhost development endpoint")
@server(
"https://myproject.wiremockapi.cloud/",
"WireMock Cloud endpoint",
MockingServer
)
namespace MyApp.Api;
Pair it with @extension("x-mocking", true) to make mocking the default for the service, then override per operation with @extension("x-mocking", false). See the mocking guides for running mocks locally.
π‘ Eventing¶
The eventing contracts model a publish endpoint for a Service Bus topic.
| Member | Purpose |
|---|---|
BaseEvent |
Empty marker model that all events extend. |
Event<"Name"> |
Adds the eventId key plus the X-Event-Type and X-Correlation-Id headers. @resource("Name") supplies the route segment. |
Topic<TEvent> |
Interface with a single post is ResourceCreate<TEvent> operation. |
@autoRoute
interface OrderPlacedTopic extends Topic<OrderPlacedEvent> {}
model OrderPlacedEvent is Event<"OrderPlaced"> {
orderId: string;
customerId: string;
total: decimal;
}
That produces POST /OrderPlaced, with eventId supplied by the caller, X-Event-Type fixed to OrderPlaced, and an optional X-Correlation-Id for tracing. Event Service APIs authenticate with a subscription key rather than OAuth:
See the Event Service guide for the surrounding infrastructure.
β Conventions Checklist¶
-
@saif/platform-typespecimported andusing SAIF.Platform;declared - Operations use
Resource*templates rather than hand-written@route+ verb decorators - No hand-rolled
@errormodels β errors come from the status-code namespace - Any
Erroroverride still includesAuthErrorsandServerError -
@useAuthpairsScopes<>withRoles<>using platform-convention permission names - Read-only resource properties marked
@visibility(Lifecycle.Read) -
MockingServerapplied to the WireMock@serverentry
π§© Linting¶
The library registers a linter with no rules today (src/linter.ts), so none of the conventions above are machine-enforced. Treat this page and the checklist as the contract, and review contract changes by hand.
π Resources¶
- TypeSpec API Design β how to apply this library
- TypeSpec Contracts β project setup and build workflow
- Authorization β permission naming and provider model
- TypeSpec documentation β the language itself
- RFC 7807 Problem Details