---
title: TypeSpec API Design
description: Set up, design, build, authorize, and mock a Forge TypeSpec API contract.
moved_from:
  - guides/development/typespec.md
  - guides/development/typespec-api-design.md
  - reference/tools/typespec.md
---

<span id="typespec-contracts"></span>

# TypeSpec API Design

How to design a Forge API contract using the `@saif/platform-typespec` conventions library.

!!! info "Scope"
    This guide teaches Forge's conventions, not TypeSpec. For the language itself — syntax, models, templates, decorators — read the [TypeSpec documentation](https://typespec.io/docs/). For the full surface of the conventions library, see the [`@saif/platform-typespec` reference](../../reference/platform-typespec.md).

---

## 📋 Overview

TypeSpec is the source of truth for Forge API contracts. The compiler emits an OpenAPI 3 document, which in turn drives APIM policies, mock servers, and generated clients.

You do not write raw TypeSpec REST operations. Forge ships `@saif/platform-typespec`, which supplies the operation templates, error models, and auth helpers that every SAIF API shares. Projects created by `saif new` are already written in that vocabulary — this guide extends it.

**Rule of thumb:** if the library has a template or model for what you need, use it. Drop to raw TypeSpec only for the gaps.

---

<span id="folder-structure"></span>

## 🏗️ Project Structure

```text
src/{AppName}.TypeSpec/
├── package.json
├── tspconfig.yaml
├── main.tsp
├── models/
│   ├── orders.tsp
│   └── customers.tsp
├── routes/
│   ├── orders.tsp
│   └── customers.tsp
└── ...                     # OpenAPI output location comes from tspconfig.yaml
```

Keep dependency declarations in the project's `package.json`, not in a second version list copied from this guide. The [API template's package file](https://github.com/saif-corp/forge/blob/main/src/templates/saif-feature-api/src/SAIF.App1.TypeSpec/package.json) supplies the scaffold's dependencies and `build` script (`tsp compile .`); the [package reference](../../reference/typescript/saif-platform-typespec/index.md) covers installation. Preserve the existing project's compatible dependency set when applying these examples.

**tspconfig.yaml:**

```yaml
emit:
  - '@typespec/openapi3'
options:
  '@typespec/openapi3':
    emitter-output-dir: '{project-root}/../../infra/api/openapi'
    output-file: 'openapi.v1.yaml'
```

## Usage Instructions

Open the TypeSpec project folder (for example, `src/ApplicationName1.TypeSpec`) in Visual Studio Code for the TypeSpec editing tools. Authenticate to the package feed and install the project's dependencies before building:

```powershell
vsts-npm-auth -config .npmrc
npm install
npm run build
```

Edit the models, operations, authorization, and mocking declarations described below, then run `npm run build` again. The emitter configuration above writes the OpenAPI document to `infra/api/openapi`; use that output for deployment and client generation rather than editing it by hand.

For a watch or formatting workflow, add scripts for `tsp compile . --watch` or `tsp format **/*.tsp` to that same project package file.

---

<span id="key-features"></span>
<span id="key-typespec-concepts-used"></span>

## 📝 Service Definition

Every contract starts the same way: import the platform library, bring `SAIF.Platform` into scope, then declare the service, servers, versions, and default auth.

```typespec title="main.tsp"
import "@typespec/http";
import "@typespec/rest";
import "@typespec/openapi";
import "@typespec/openapi3";
import "@typespec/versioning";
import "@saif/platform-typespec";

using TypeSpec.Http;
using TypeSpec.Rest;
using TypeSpec.OpenAPI;
using TypeSpec.Versioning;
using SAIF.Platform;

@service(#{ title: "Order Service API" })
@server("http://localhost:21000", "localhost development endpoint")
@server(
  "https://myproject.wiremockapi.cloud/",
  "WireMock Cloud endpoint",
  MockingServer
)
@versioned(Versions)
@extension("x-mocking", true)
@useAuth(Scopes<["Client.Read"]> | Roles<["App.Read"]>)
namespace OrderService;

enum Versions {
  v1,
}

import "./models/orders.tsp";
import "./routes/orders.tsp";
```

`MockingServer`, `Scopes<>`, and `Roles<>` all come from the platform library. Do not hand-write an `OAuth2Auth<>` block — see [Authentication](#authentication).

### Mocking

The `MockingServer` server entry identifies the mock backend. The namespace-level `@extension("x-mocking", true)` above supplies the default routing choice.

#### Endpoint Mocking

Set `@extension("x-mocking", true)` on an operation to route it to the mocking URL, or `@extension("x-mocking", false)` to route it to the application backend instead. An operation's value overrides the namespace default. See [API Mocking](../local-dev/mocking/index.md) to host or record the mock behavior.

---

## 🧱 Models

Models are plain TypeSpec. Two conventions make the operation templates work:

- `@resource("name")` supplies the route segment.
- `@key` marks the identifier used in instance routes.

```typespec title="models/orders.tsp"
using TypeSpec.Rest;
using TypeSpec.Versioning;

namespace OrderService;

@doc("Represents an order in the system")
@added(Versions.v1)
@resource("orders")
model Order {
  @doc("Unique identifier")
  @key
  @visibility(Lifecycle.Read)
  id: string;

  @doc("Customer who placed the order")
  customerId: string;

  @doc("Order line items")
  @minItems(1)
  items: OrderItem[];

  @doc("Order status")
  status: OrderStatus;

  @doc("Total order amount")
  @minValue(0)
  total: decimal;

  @doc("When the order was created")
  @visibility(Lifecycle.Read)
  createdAt: utcDateTime;

  @doc("When the order was last updated")
  @visibility(Lifecycle.Read)
  updatedAt?: utcDateTime;
}

model OrderItem {
  productId: string;

  @minValue(1)
  quantity: int32;

  @minValue(0)
  unitPrice: decimal;
}

@doc("Possible order states")
enum OrderStatus {
  Pending,
  Confirmed,
  Shipped,
  Delivered,
  Cancelled,
}
```

!!! tip "Do not define separate create/update request models"
    `ResourceCreate` derives its body from `ResourceCreateableProperties<Order>`, and `ResourcePartialUpdate` from `ResourceUpdateableProperties<Order>`. Marking server-owned fields `@visibility(Lifecycle.Read)` is what keeps `id` and `createdAt` out of the create body. A hand-written `CreateOrderRequest` duplicates the resource and drifts from it.

---

<span id="endpoints"></span>
<span id="methods"></span>
<span id="overriding-authorization"></span>

## 🛤️ Routes and Operations

Operations are declared with `is` against a platform template. The template applies `@autoRoute`, the HTTP verb, the REST resource decorator, and the standard error union.

```typespec title="routes/orders.tsp"
using TypeSpec.Http;
using TypeSpec.Rest;
using TypeSpec.Versioning;
using SAIF.Platform;

namespace OrderService;

@autoRoute
@added(Versions.v1)
@tag("Orders")
interface Orders {
  get is ResourceRead<Order>;
  all is ResourceList<Order>;

  @useAuth(Scopes<["Client.Write"]> | Roles<["App.Write"]>)
  post is ResourceCreate<Order>;

  @useAuth(Scopes<["Client.Write"]> | Roles<["App.Write"]>)
  put is ResourceCreateOrReplace<Order>;

  @useAuth(Scopes<["Client.Write"]> | Roles<["App.Write"]>)
  patch is ResourcePartialUpdate<Order>;

  @useAuth(Scopes<["Client.Delete"]> | Roles<["App.Admin"]>)
  delete is ResourceDelete<Order>;
}
```

That produces `GET /orders/{id}`, `GET /orders`, `POST /orders`, `PUT /orders/{id}`, `PATCH /orders/{id}`, and `DELETE /orders/{id}` — each with 400, 401, 403, and 5xx already documented, and 404 where applicable.

Full parameter and default tables: [operation templates reference](../../reference/platform-typespec.md#operation-templates).

### Query parameters

Pass a parameters model as the second template argument rather than writing a raw operation.

```typespec
model OrderFilter {
  @doc("Filter by customer")
  @query
  customerId?: string;

  @doc("Filter by status")
  @query
  status?: OrderStatus;

  @doc("Records to skip")
  @query
  skip?: int32 = 0;

  @doc("Page size")
  @query
  @maxValue(100)
  take?: int32 = 20;
}

@autoRoute
interface Orders {
  all is ResourceList<Order, OrderFilter>;
}
```

### Nested resources

Declare the child's parent with `@parentResource`. The templates then generate the nested route and include the parent key automatically.

```typespec
@resource("items")
@parentResource(Order)
model OrderLine {
  @key
  @visibility(Lifecycle.Read)
  lineId: string;

  productId: string;

  @minValue(1)
  quantity: int32;
}

@autoRoute
interface OrderLines {
  all is ResourceList<OrderLine>;      // GET /orders/{id}/items
  post is ResourceCreate<OrderLine>;   // POST /orders/{id}/items
  delete is ResourceDelete<OrderLine>; // DELETE /orders/{id}/items/{lineId}
}
```

### Actions

When an operation is a verb rather than a CRUD effect on the resource, use an action template.

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

### Long-running operations

Use the async action templates instead of modelling a polling contract by hand. They return `Accepted` (202).

```typespec
@autoRoute
interface Orders {
  // POST /orders/import → 202
  bulkImport is ResourceCollectionActionAsync<Order, {}, BulkImportRequest>;
}
```

---

## ❌ Error Handling

Every platform template already returns RFC 7807 Problem Details, so use the library's error models instead of defining your own `@error` models. The [error models reference](../../reference/platform-typespec.md#errors) lists each model, its status code, and when to use it.

To add an error, restate the full union in the template's `Error` parameter:

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

!!! warning "`Error` is a replacement, not an addition"
    Whatever you pass becomes the complete error union. Omitting `AuthErrors` or `ServerError` strips 401, 403, and 5xx from the OpenAPI document and from every generated client, even though APIM still returns them at runtime.

---

<span id="authorization"></span>

## 🔐 Authentication

Declare the service default at the namespace and tighten it per operation. `Scopes<>` and `Roles<>` cover both identity providers in one declaration, and the union means either satisfies the requirement.

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

Use only these helpers: do not hand-write `OAuth2Auth<>` or name platform-managed delegation scopes. The [Auth Helpers reference](../../reference/platform-typespec.md#auth-helpers) covers which claim each helper checks, the per-operation override pattern, and the names you must not use. [How Authentication Works](../../reference/architecture/how-authentication-works.md) explains how each provider maps the helpers to token claims.

---

## 🔢 Versioning

Versioning is plain TypeSpec. Add every new model, operation, and property with `@added` so existing versions stay stable.

```typespec
@versioned(Versions)
@service(#{ title: "Order Service API" })
namespace OrderService;

enum Versions {
  v1,
  v2,
}

@added(Versions.v1)
@resource("orders")
model Order {
  @key
  id: string;

  customerId: string;

  @added(Versions.v2)
  priority?: OrderPriority;
}
```

---

## ✔️ Validation

Field constraints are raw TypeSpec decorators and flow through to the OpenAPI schema and generated server-side validation.

```typespec
model RefundRequest {
  @minLength(1)
  @maxLength(100)
  reason: string;

  @minValue(0)
  amount: decimal;

  @pattern("^[A-Z]{2}[0-9]{4}$")
  referenceCode?: string;
}
```

Reusable formats are best expressed as scalars:

```typespec
@doc("Email address format")
@pattern("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$")
scalar email extends string;

model CustomerContact {
  email: email;
}
```

Validation failures surface as `BadRequest` with the offending fields in the `errors` array — you do not model that response yourself.

---

## 🧭 When to Drop to Raw TypeSpec

The library covers resource-shaped REST. Use raw TypeSpec when an operation is genuinely outside that shape:

| Situation | Approach |
| --------- | -------- |
| Non-resource endpoint with no key or collection | Raw `op` with explicit `@route`, still returning the platform error models |
| Response the wrappers don't cover (file download, streamed body) | Custom response model composed from `StatusCode<N>` |
| Status code the library doesn't name | Build it from `StatusCode<N>` and `ProblemDetailsProperties` |
| Non-HTTP protocol surface | Outside the library's scope |

!!! warning "Omit `type` when extending `ProblemDetails`"
    A custom model built from `StatusCode<N>` and `ProblemDetailsProperties` must omit `"type"` from that spread. See [Building custom status models](../../reference/platform-typespec.md#building-custom-status-models) for the worked example and the Kiota and C# server-emitter details.

Even then, keep the error union. `BadRequest | AuthErrors | ServerError` belongs on every operation the platform serves.

---

## 📤 Generated Output

`npm run build` compiles the contract to OpenAPI at the path configured in `tspconfig.yaml`:

```yaml title="infra/api/openapi/openapi.v1.yaml (generated)"
openapi: 3.0.0
info:
  title: Order Service API
  version: v1
paths:
  /orders:
    get:
      operationId: Orders_all
      # ...
```

Clients are generated from that document. Forge .NET projects usually add a `<KiotaReference>` to the consuming project instead of running the CLI; see [Calling APIs](calling-apis.md). To run the Kiota CLI directly:

```powershell
# C# client via Kiota
kiota generate -l CSharp -o ./src/MyApp.Client -d ./infra/api/openapi/openapi.v1.yaml
```

The [`SAIF.Platform.Kiota.HttpClientLibrary` CLI reference](../../reference/dotnet/SAIF.Platform.Kiota.HttpClientLibrary/index.md#kiota-cli-reference) maps each `kiota generate` option to its `KiotaReference` property.

---

## ✅ Checklist

The [conventions checklist](../../reference/platform-typespec.md#conventions-checklist) covers contract setup, models, operations, and security. Before publishing, also confirm the build output:

- [ ] `npm run build` succeeds with no warnings
- [ ] OpenAPI emitted to `infra/api/openapi`

---

## 📚 Resources

- [`@saif/platform-typespec` reference](../../reference/platform-typespec.md) — full library surface
- [`@saif/platform-typespec` package README](../../reference/typescript/saif-platform-typespec/index.md) — package installation and exports
- [Authorization](../../reference/authorization.md) — permission model
- [TypeSpec documentation](https://typespec.io/docs/) — the language itself
- [RFC 7807 Problem Details](https://datatracker.ietf.org/doc/html/rfc7807)
