LRO, pagination, versioning, and request trait bundle
Use this bundle when the API involves long-running operations, collection pagination/filtering, version lifecycle, repeatability, conditional requests, or client request IDs.
Versioning
Add versioning at the first external release.
@versioned(Contoso.WidgetService.Versions)
namespace Contoso.WidgetService;
/** Service API versions. */
enum Versions {
/** Initial stable release. */
v2026_05_10: "2026-05-10",
}When changing the contract:
model Widget {
/** Widget name. */
name: string;
/** Added in the 2026-09-01 API version. */
@added(Versions.v2026_09_01)
color?: string;
}For Azure APIs, keep version strings date-based. For non-Azure APIs, use the organization standard, but avoid mixing date and semantic versioning in one API.
Preview version suffix
Azure REQUIRES the YYYY-MM-DD-preview suffix as the ONLY acceptable preview format (e.g., 2026-05-01-preview). Other suffixes - -beta, -rc, -alpha, -private, semver-style 1.0.0-preview.2 - are rejected by Azure API review. Promote to GA by dropping the -preview suffix; do not introduce intermediate suffixes.
Version-level retirement
Deprecating individual operations is not the same as retiring an entire API version. Retire a whole version when most of its operations are deprecated or its auth/error/pagination patterns are obsolete. A version retirement needs its own plan, not a pile of per-operation deprecations:
- Announce the retirement with a dated
Sunsetheader on EVERY operation in that version (12+ months out for public APIs) plus a version-to-version migration guide published before the announcement. - Track traffic to the old version as a whole - retire only when residual calls are trivial, then remove the enum member with
@removed(Versions.vOld)semantics in a new version projection. - Do not silently drop an enum member from the current
Versionsenum: existing clients still send the oldapi-version, and removing it without a documented window turns every such call into a 400.
Pagination
For Azure data-plane APIs, use Azure Core list templates and query parameter traits instead of hand-rolling paging.
op listWidgets is Operations.ResourceList<
Widget,
ListQueryParametersTrait<
StandardListQueryParameters &
SelectQueryParameter &
ExpandQueryParameter
>
>;For generic APIs, use a stable envelope:
/** A page of widgets. */
model PagedWidgets {
/** Items in this page. */
items: Widget[];
/** Opaque continuation token or next link. */
nextLink?: url;
}Long-running operations
Only use an LRO when the operation can outlive a normal HTTP request/response window or requires polling for final state.
Azure Core pattern:
interface Widgets {
/** Get widget operation status. */
getWidgetOperationStatus is Operations.GetResourceOperationStatus<Widget>;
/** Create or replace a widget asynchronously. */
@pollingOperation(Widgets.getWidgetOperationStatus)
createOrReplaceWidget is Operations.LongRunningResourceCreateOrReplace<Widget>;
/** Delete a widget asynchronously. */
@pollingOperation(Widgets.getWidgetOperationStatus)
deleteWidget is Operations.LongRunningResourceDelete<Widget>;
}The polling/status operation must be defined before operations that reference it.
Polling location safety
@pollingOperation MUST point to a status endpoint hosted on the same service host as the initiating operation. Some Azure SDK pollers block cross-host polling URLs to prevent token-leak and SSRF vectors, so a polling URL that drifts to a different hostname will fail at runtime even though the contract compiles. Keep the polling/status route under the same @server and the same auth scope as the LRO that produced it.
Repeatability
Use repeatable request support for create/action operations that can be retried safely after network failure. For Azure data-plane APIs, include SupportsRepeatableRequests in service traits where applicable.
alias ServiceTraits = SupportsRepeatableRequests & SupportsClientRequestId;
alias Operations = ResourceOperations<ServiceTraits>;Conditional requests
Use conditional request support for update/delete/read scenarios that need concurrency control.
alias ServiceTraits = SupportsConditionalRequests & SupportsClientRequestId;Client request ID
External APIs should accept a caller-supplied correlation/client request ID. For Azure APIs, prefer SupportsClientRequestId.
@added and @removed interactions
If a property is @removed(Versions.v2) on a model that itself was @added(Versions.v2), you MUST also add @added(Versions.v2) to the property - otherwise the emitter generates an orphan reference and the diagnostic is opaque (microsoft/typespec issue #7035). Pattern:
@added(Versions.v2)
model Foo {
@added(Versions.v2) @removed(Versions.v3) bar?: string;
}Breaking-change review
Before adding a version, classify changes:
- Additive: new optional property, new operation, new enum value if clients tolerate unknown values.
- Potentially breaking: required property, renamed property, removed value, changed format/type, changed status code, changed auth, changed route, changed error shape.
- Breaking: removed operation, required parameter added to existing operation, route identity changed, incompatible response body change.
Use @added, @removed, and versioned projections intentionally. Do not hide breaking changes behind undocumented emitter output differences.
Worked classification table
| Change type | Breaking? | Mitigation |
|---|---|---|
| Add new optional request property | No | Ship in next version with @added. |
| Add new operation | No | Ship in next version with @added. |
| Add new enum value (closed enum) | Yes - strict SDKs throw | Use an open union, or bump major version. |
| Make optional request property required | Yes | New API version; keep optional in old version via projection. |
| Rename property | Yes | Keep the old name as @deprecated alias for one version, then @removed. |
Tighten @pattern/@maxLength |
Yes | New API version; loosening is non-breaking. |
| Remove operation | Yes | @removed in a new version after a deprecation window. |
| Change response status code | Yes | New API version with explicit version-gated response. |
Change OAuth2 audience or tokenUrl |
Yes (security) | Major new API version; coordinate token rollover. |
Security-impacting breaking changes
Some changes look small but break clients in ways that fail-closed. Treat all of these as breaking and require a new API version:
- Removing a scope from an
OAuth2Authflow. Existing tokens may have been issued with the removed scope and will now be rejected (or new tokens will lack permissions the client expects). Adding a new scope is usually breaking too if it's required for existing operations. - Tightening a
@patternregex (or@maxLength,@minLength, numeric bounds). Payloads that previously validated will now fail at the contract boundary. Loosening these constraints is generally non-breaking. - Required to optional on response fields is non-breaking; optional to required on request fields IS breaking. Clients that previously omitted the field will start receiving 400 errors.
- Renaming an enum value is breaking. Adding a new enum value is breaking only for clients that treat unknown values as errors (true for many strict-typed SDKs). To rename safely, keep the old value as a
@deprecatedalias for at least one version. - Changing the audience or
tokenUrlon an OAuth2 flow. Even if scopes are unchanged, tokens for the old audience will not authenticate against the new one.
Audience changes
Changing the OAuth2 token audience invalidates ALL existing tokens regardless of scope overlap - the audience is part of the token signature payload and is validated independently of scope claims. Even if the new audience is a strict superset of the old one's permissions, every client that holds a cached token will fail-closed on its next request. Treat any audience change as a major breaking change that requires a new API version, coordinated client rollout, and a documented token-refresh window.