TypeSpec API Design¶
Guide for designing APIs using TypeSpec in Forge projects.
📋 Overview¶
TypeSpec is a language for describing APIs that generates OpenAPI specifications and client code. Forge uses TypeSpec as the source of truth for API definitions.
🏗️ Project Structure¶
TypeSpec Project Layout¶
src/{AppName}.TypeSpec/
├── package.json
├── tspconfig.yaml
├── main.tsp
├── models/
│ ├── common.tsp
│ ├── orders.tsp
│ └── customers.tsp
├── routes/
│ ├── orders.tsp
│ └── customers.tsp
└── tsp-output/ # Generated output
├── openapi.yaml
└── @typespec/
Package Configuration¶
package.json:
{
"name": "@myapp/typespec",
"version": "1.0.0",
"type": "module",
"dependencies": {
"@typespec/compiler": "^1.3.0",
"@typespec/http": "^1.5.0",
"@typespec/rest": "^1.5.0",
"@typespec/openapi3": "^1.5.0",
"@typespec/versioning": "^1.5.0",
"@saif/platform-typespec": "^3.0.0"
},
"scripts": {
"build": "tsp compile .",
"watch": "tsp compile . --watch",
"format": "tsp format **/*.tsp"
}
}
tspconfig.yaml:
emit:
- "@typespec/openapi3"
options:
"@typespec/openapi3":
emitter-output-dir: "{project-root}/../../infra/api/openapi"
output-file: "openapi.v1.yaml"
📝 Basic Syntax¶
Service Definition¶
// main.tsp
import "@typespec/http";
import "@typespec/rest";
import "@typespec/openapi3";
import "@saif/platform-typespec";
using TypeSpec.Http;
using TypeSpec.Rest;
@service({
title: "Order Service",
version: "1.0.0"
})
@server("https://api.example.com", "Production")
namespace MyApp.Api;
// Import route files
import "./routes/orders.tsp";
import "./routes/customers.tsp";
Models¶
// models/orders.tsp
namespace MyApp.Api.Models;
@doc("Represents an order in the system")
model Order {
@doc("Unique identifier")
@key
id: string;
@doc("Customer who placed the order")
customerId: string;
@doc("Order line items")
items: OrderItem[];
@doc("Order status")
status: OrderStatus;
@doc("Total order amount")
@minValue(0)
total: decimal;
@doc("When the order was created")
createdAt: utcDateTime;
@doc("When the order was last updated")
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
}
Request/Response Models¶
// models/requests.tsp
namespace MyApp.Api.Models;
@doc("Request to create a new order")
model CreateOrderRequest {
customerId: string;
items: CreateOrderItemRequest[];
}
model CreateOrderItemRequest {
productId: string;
@minValue(1)
quantity: int32;
}
@doc("Response containing a list of orders")
model OrderListResponse {
items: Order[];
@doc("Total count for pagination")
totalCount: int32;
@doc("Continuation token for next page")
continuationToken?: string;
}
🛤️ Routes and Operations¶
RESTful Routes¶
// routes/orders.tsp
import "../models/orders.tsp";
using TypeSpec.Http;
using TypeSpec.Rest;
namespace MyApp.Api;
@route("/orders")
@tag("Orders")
interface Orders {
@doc("List all orders")
@get
list(
@query skip?: int32 = 0,
@query take?: int32 = 20,
@query status?: OrderStatus
): OrderListResponse | Error;
@doc("Get order by ID")
@get
@route("{id}")
get(@path id: string): Order | NotFoundError | Error;
@doc("Create a new order")
@post
create(@body request: CreateOrderRequest): {
@statusCode statusCode: 201;
@body order: Order;
} | ValidationError | Error;
@doc("Update an existing order")
@put
@route("{id}")
update(
@path id: string,
@body request: UpdateOrderRequest
): Order | NotFoundError | ValidationError | Error;
@doc("Delete an order")
@delete
@route("{id}")
delete(@path id: string): {
@statusCode statusCode: 204;
} | NotFoundError | Error;
}
Nested Resources¶
// routes/order-items.tsp
@route("/orders/{orderId}/items")
@tag("Order Items")
interface OrderItems {
@get
list(@path orderId: string): OrderItem[] | NotFoundError;
@post
add(
@path orderId: string,
@body item: CreateOrderItemRequest
): {
@statusCode statusCode: 201;
@body item: OrderItem;
} | NotFoundError | ValidationError;
@delete
@route("{itemId}")
remove(
@path orderId: string,
@path itemId: string
): {
@statusCode statusCode: 204;
} | NotFoundError;
}
❌ Error Handling¶
Error Models¶
// models/errors.tsp
namespace MyApp.Api.Models;
@error
model Error {
@statusCode statusCode: 500;
code: string;
message: string;
}
@error
model NotFoundError {
@statusCode statusCode: 404;
code: "NOT_FOUND";
message: string;
resourceType: string;
resourceId: string;
}
@error
model ValidationError {
@statusCode statusCode: 400;
code: "VALIDATION_ERROR";
message: string;
errors: ValidationErrorDetail[];
}
model ValidationErrorDetail {
field: string;
message: string;
code: string;
}
@error
model UnauthorizedError {
@statusCode statusCode: 401;
code: "UNAUTHORIZED";
message: string;
}
@error
model ForbiddenError {
@statusCode statusCode: 403;
code: "FORBIDDEN";
message: string;
requiredPermission?: string;
}
🔐 Authentication¶
OAuth2/OIDC Security¶
// main.tsp
import "@typespec/http";
using TypeSpec.Http;
@service({
title: "Order Service",
version: "1.0.0"
})
@useAuth(OAuth2Auth<[
{
type: OAuth2FlowType.authorizationCode,
authorizationUrl: "https://login.microsoftonline.com/{tenant}/oauth2/v2.0/authorize",
tokenUrl: "https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token",
scopes: ["User.Read", "User.Write"]
}
]>)
namespace MyApp.Api;
Scope-Based Authorization¶
// routes/orders.tsp
@route("/orders")
@tag("Orders")
interface Orders {
@doc("List orders - requires User.Read scope")
@get
list(): OrderListResponse;
@doc("Create order - requires User.Write scope")
@post
create(@body request: CreateOrderRequest): Order;
}
🔢 Versioning¶
API Versioning¶
import "@typespec/versioning";
using TypeSpec.Versioning;
@versioned(Versions)
@service({
title: "Order Service"
})
namespace MyApp.Api;
enum Versions {
v1: "1.0.0",
v2: "2.0.0"
}
// Model available in all versions
model Order {
id: string;
customerId: string;
@added(Versions.v2)
priority?: OrderPriority;
}
@added(Versions.v2)
enum OrderPriority {
Low,
Normal,
High,
Urgent
}
📐 Common Patterns¶
Pagination¶
// models/common.tsp
model PagedResponse<T> {
items: T[];
totalCount: int32;
pageSize: int32;
pageNumber: int32;
hasNextPage: boolean;
hasPreviousPage: boolean;
}
// Usage
@get
list(
@query pageNumber?: int32 = 1,
@query pageSize?: int32 = 20
): PagedResponse<Order>;
Search/Filter¶
model OrderSearchRequest {
@doc("Filter by customer ID")
customerId?: string;
@doc("Filter by status")
status?: OrderStatus;
@doc("Filter by date range - start")
fromDate?: utcDateTime;
@doc("Filter by date range - end")
toDate?: utcDateTime;
@doc("Search in order ID or product names")
searchText?: string;
}
@route("/search")
@post
search(@body request: OrderSearchRequest): PagedResponse<Order>;
Async Operations¶
model AsyncOperationResponse {
@doc("Operation ID for status polling")
operationId: string;
@doc("Current operation status")
status: OperationStatus;
@doc("URL to poll for status")
statusUrl: url;
}
enum OperationStatus {
Pending,
InProgress,
Completed,
Failed
}
@route("/bulk-import")
@post
bulkImport(@body request: BulkImportRequest): {
@statusCode statusCode: 202;
@body response: AsyncOperationResponse;
};
@route("/operations/{operationId}")
@get
getOperationStatus(@path operationId: string): AsyncOperationResponse;
✔️ Validation¶
Field Validation¶
model CreateOrderRequest {
@minLength(1)
@maxLength(100)
customerId: string;
@minItems(1)
@maxItems(100)
items: CreateOrderItemRequest[];
@pattern("^[A-Z]{2}[0-9]{4}$")
referenceCode?: string;
@minValue(0)
@maxValue(100)
discountPercent?: float32;
}
Custom Decorators¶
// Define custom validation
@doc("Email address format")
@pattern("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$")
scalar email extends string;
@doc("Phone number format")
@pattern("^\\+?[1-9]\\d{1,14}$")
scalar phoneNumber extends string;
// Usage
model CustomerContact {
email: email;
phone?: phoneNumber;
}
📤 Generated Output¶
OpenAPI Specification¶
TypeSpec generates OpenAPI 3.0 spec at the configured output path:
# infra/api/openapi/openapi.v1.yaml (generated)
openapi: 3.0.0
info:
title: Order Service
version: 1.0.0
paths:
/orders:
get:
operationId: Orders_list
# ...
Client Generation¶
# Generate TypeScript client
npx @typespec/openapi3 ./src/MyApp.TypeSpec
# Generate C# client using Kiota
kiota generate -l CSharp -o ./src/MyApp.Client -d ./infra/api/openapi/openapi.v1.yaml
✅ Checklist¶
API Design¶
- Clear, descriptive operation names
- Consistent naming conventions
- Proper HTTP verbs for operations
- Appropriate status codes for responses
- Error responses documented
Models¶
- All fields have
@docdescriptions - Validation decorators applied
- Nullable fields marked with
? - Enums used for fixed value sets
Security¶
- Auth scheme defined
- Scopes documented
- Error responses for 401/403
Output¶
- OpenAPI spec generates without errors
- Output path configured correctly
- Client generation works