# @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.

!!! info "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](https://typespec.io/docs/). 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:

```typespec title="main.tsp"
import "@saif/platform-typespec";

using SAIF.Platform;
```

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](https://datatracker.ietf.org/doc/html/rfc7807) 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.

```typespec
@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:

```typespec
@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. |

!!! warning "`{}` 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:**

```typespec
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:

```typespec
@autoRoute
interface Orders {
  post is ResourceCreate<
    Order,
    {},
    CreatedResponse<Order>,
    BadRequest | Conflict | AuthErrors | ServerError
  >;
}
```

!!! warning "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:**

```typespec
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 with `T` as the body
- `CreatedResponse<T>` — 201 with `T` as the body

### Errors

Every error model extends `ProblemDetails`, so all error responses are RFC 7807 with `Content-Type: application/problem+json`.

```json title="Example 404 response body"
{
  "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.

!!! tip "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.

```typespec
model PaymentRequired extends ProblemDetails {
  status: int32 = 402;
  ...StatusCode<402>;
  ...OmitProperties<ProblemDetailsProperties, "title" | "status" | "statusCode" | "type">;
}
```

!!! warning "Omit `type` when extending `ProblemDetails`"
    `ProblemDetails` declares `type`, so a custom model must omit it from the `ProblemDetailsProperties` spread. With Kiota 1.35.0, declaring `type` on both the base and the concrete error model causes the generated C# error to lose its `Type` property when Kiota replaces the base with `ApiException`. Keeping `type` only on the base preserves the OpenAPI schema's inherited property and restores the generated client's `Type`.

!!! warning "C# server-emitter compatibility"
    With `@typespec/http-server-csharp` 0.58.0-dev.3, concrete error constructors include only locally declared properties in their parameters and response `Value`. After this change, those constructors no longer accept `type`, and their response `Value` omits it. The generated models still inherit `TypeName`, but setting it does not change the body sent by `HttpServiceExceptionFilter`, which serializes `Value`. This trade-off preserves the `ProblemDetails` hierarchy while fixing Kiota clients. Consumers regenerating server code must remove named `type:` arguments and review positional arguments after `status`, since removing `type` shifts the remaining parameters. No generated constructor can put `type` back into `Value`: leaf error models never get a base pass-through constructor, and inherited properties are excluded from the ones the emitter does build. Consumers who need `type` in the wire response must add a hand-written partial class for the affected model(s) with their own constructor that builds `value: new { type = ..., ... }` directly, following the [partial-class pattern used for other generated models](../build/data/database-oracle.md#extending-generated-models-with-partial-classes).

---

## 🔐 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.

```typespec
@useAuth(Scopes<["Client.Read"]> | Roles<["App.Read"]>)
namespace MyApp.Api;
```

| 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:

```typespec
@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>;
}
```

!!! danger "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](authorization.md#permission-naming-conventions) and be defined in both providers; see [App Permissions](../build/identity/configuration/app-permissions.md).

!!! note "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.

!!! danger "Do not hand-write `OAuth2Auth<>`"
    A raw `OAuth2Auth<>` block hard-codes tenant URLs and bypasses dual-provider policy generation. `Scopes<>` and `Roles<>` already declare the flow, and APIM injects the real endpoints at deployment.

---

## 🎭 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.

```typespec
@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](../build/local-dev/mocking/host-wiremock-cli.md) 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. |

```typespec
@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:

```typespec
@useAuth(ApiKeyAuth<ApiKeyLocation.header, "Ocp-Apim-Subscription-Key">)
```

See the [Event Service guide](../build/eventing/event-service.md) for the surrounding infrastructure.

---

## ✅ Conventions Checklist

**Contract setup**

- [ ] `@saif/platform-typespec` imported and `using SAIF.Platform;` declared
- [ ] `@versioned` enum defined, and every new model, operation, and property carries `@added`
- [ ] `MockingServer` applied to the WireMock `@server` entry

**Models**

- [ ] `@resource` and `@key` set on every resource model
- [ ] Read-only resource properties marked `@visibility(Lifecycle.Read)`
- [ ] All properties documented with `@doc`
- [ ] No hand-written create or update request models; the templates derive the body from the resource

**Operations**

- [ ] Operations use `Resource*` templates rather than hand-written `@route` + verb decorators
- [ ] Query parameters passed as the `Parameters` template argument
- [ ] No hand-rolled `@error` models — errors come from the status-code namespace
- [ ] Any `Error` override still includes `AuthErrors` and `ServerError`

**Security**

- [ ] Namespace default `@useAuth` pairs `Scopes<>` with `Roles<>` using platform-convention permission names
- [ ] Write and delete operations override the default with stronger permissions
- [ ] Permission names exist in both identity providers

---

## 🧩 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](../build/apis/typespec.md) - Project setup, build workflow, and applying this library
- [Authorization](authorization.md) — permission naming and provider model
- [TypeSpec documentation](https://typespec.io/docs/) — the language itself
- [RFC 7807 Problem Details](https://datatracker.ietf.org/doc/html/rfc7807)
