An API is more than a collection of endpoints. It is an agreement between the applications that produce data and the applications that consume it. When that agreement changes unexpectedly, mobile apps fail, frontend integrations break, and independent development teams lose confidence in the services they depend on.
Contract-first API development solves this problem by defining the API’s structure and behavior before implementing its endpoints. With ASP.NET Core, OpenAPI, generated clients, automated contract checks, and schema governance, teams can build APIs that evolve predictably without forcing every consumer to change at the same time.
Why API Contracts Become Important as Systems Grow
Imagine a development team building an online shopping platform.
The backend team maintains an ASP.NET Core API. A separate team develops the website. Another team builds the mobile application. Several external partners use the same API to retrieve product information and submit orders.
Initially, coordination is simple.
A backend developer creates an endpoint:
GET /api/products/42It returns:
{
"id": 42,
"name": "Wireless Headphones",
"price": 89.99
}The frontend team integrates the response, and everything works.
Three months later, a backend developer decides that price is too vague. The application now supports multiple currencies, so the response changes:
{
"id": 42,
"name": "Wireless Headphones",
"pricing": {
"amount": 89.99,
"currency": "USD"
}
}The new structure is arguably better.
Unfortunately, the mobile application still expects:
product.priceThat property no longer exists.
The backend developer improved the design but accidentally broke an existing consumer.
The problem was not JSON, HTTP, or ASP.NET Core.
The problem was that the API contract changed without a controlled compatibility process.
Contract-first development establishes that process before implementation begins.
What Is Contract-First API Development?
In a traditional code-first workflow, developers implement endpoints and generate documentation from the resulting application.
The sequence looks like this:
Write endpoint
↓
Define request and response models
↓
Generate API description
↓
Consumers discover the contractContract-first development reverses the initial relationship.
Define API contract
↓
Review and approve contract
↓
Generate clients and supporting code
↓
Implement ASP.NET Core endpoints
↓
Verify implementation against contract
↓
Publish the approved contractThe API description becomes a shared design artifact.
Backend developers know what they must implement.
Frontend developers know what responses to expect.
Mobile developers can generate typed clients before the server is complete.
Test engineers can design contract tests against an agreed specification.
Architecture teams can review naming, security, pagination, and compatibility before implementation decisions become expensive.
This does not mean developers stop writing code.
It means the contract becomes the starting point instead of an accidental byproduct of implementation.
OpenAPI Provides a Common Language
OpenAPI is a widely adopted specification for describing HTTP APIs.
An OpenAPI document describes elements such as request paths, HTTP methods, parameters, request bodies, response structures, status codes, authentication requirements, and reusable schemas.
It is commonly written in YAML or JSON.
For example, our product endpoint might be described as:
openapi: 3.0.3
info:
title: Product API
version: 1.0.0
paths:
/api/products/{id}:
get:
operationId: getProductById
summary: Retrieve a product
parameters:
- name: id
in: path
required: true
schema:
type: integer
format: int32
responses:
'200':
description: Product found
content:
application/json:
schema:
$ref: '#/components/schemas/Product'
'404':
description: Product not found
components:
schemas:
Product:
type: object
required:
- id
- name
- price
properties:
id:
type: integer
format: int32
name:
type: string
price:
type: number
format: decimalThis example uses OpenAPI 3.0.3. The same contract-first principles also apply to OpenAPI 3.1 and newer versions, although their schema rules and tooling compatibility differ.
Notice what this document accomplishes.
Before writing the endpoint, we have established its URL, HTTP method, input parameter, successful response, missing-resource response, and product representation.
A frontend developer can read this contract without examining ASP.NET Core source code.
A client generator can use it to produce typed methods.
A contract test can use it to validate the implemented endpoint.
Most importantly, the team has something concrete to review before development begins.
Designing the Contract Before Writing Code
Contract-first development works best when the team treats API design as a deliberate engineering activity.
In our previous guide on Advanced API Design in ASP.NET Core, we explored how consistent API structures, predictable responses, and clear design conventions make integrations easier to maintain. Contract-first development takes those principles further by establishing the agreement before implementation begins.
Consider an order management API.
Before implementation, the team should answer several questions.
What resource does the endpoint represent?
Should creating an order use POST?
What information must the caller provide?
What happens when the order is invalid?
Which fields are required?
What response should successful creation return?
Can clients safely retry the operation?
How will authentication work?
How will the API evolve?
For example:
POST /api/ordersRequest:
{
"customerId": 125,
"items": [
{
"productId": 42,
"quantity": 2
}
]
}Successful response:
HTTP/1.1 201 Created
Location: /api/orders/8102{
"orderId": 8102,
"status": "pending",
"total": 179.98
}The team can agree on these details before the backend and frontend implementations begin.
This is particularly valuable when different teams work on independent release schedules.
Designing Consistent Request and Response Schemas
A good contract defines more than property names.
It establishes expectations about data types, required fields, nullability, validation, and structure.
Consider:
OrderRequest:
type: object
required:
- customerId
- items
properties:
customerId:
type: integer
format: int32
items:
type: array
minItems: 1
items:
$ref: '#/components/schemas/OrderItem'
OrderItem:
type: object
required:
- productId
- quantity
properties:
productId:
type: integer
format: int32
quantity:
type: integer
minimum: 1This makes several decisions explicit.
An order must include a customer identifier.
The items collection must contain at least one item.
Each item must identify a product.
Quantity cannot be less than one.
Those constraints can inform validation in ASP.NET Core, generated clients, and automated testing.
However, there is an important distinction.
Describing a constraint in OpenAPI does not automatically guarantee that every ASP.NET Core endpoint enforces it at runtime.
The implementation must still validate incoming requests. Contract tests should verify that actual behavior matches the published specification.
Implementing an Approved Contract in ASP.NET Core
Once the contract is reviewed, the backend team can implement it.
ASP.NET Core supports generating OpenAPI documents for HTTP endpoints through its OpenAPI integration.
For a modern Minimal API application, a simplified setup looks like this:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOpenApi();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
}
app.MapGet(
"/api/products/{id:int}",
(int id) =>
{
if (id != 42)
{
return Results.NotFound();
}
return Results.Ok(new ProductResponse(
42,
"Wireless Headphones",
89.99m));
})
.WithName("getProductById")
.Produces<ProductResponse>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound);
app.Run();
public record ProductResponse(
int Id,
string Name,
decimal Price);The AddOpenApi() method registers OpenAPI document generation services, while MapOpenApi() exposes the generated document endpoint.
For this example, the project must reference the appropriate Microsoft.AspNetCore.OpenApi package.
The generated document can then be compared with the approved contract.
This is a critical distinction.
ASP.NET Core’s generated OpenAPI document describes the implemented application. It does not automatically prove that the implementation conforms to an independently authored contract.
A contract-first pipeline must explicitly compare the two or test runtime behavior against the approved specification.
For implementation details, see the official ASP.NET Core OpenAPI documentation.
OpenAPI Generation Is Not Contract Governance
Generating an OpenAPI document is useful, but it is not the same as governing an API contract.
Imagine that a developer changes:
public record ProductResponse(
int Id,
string Name,
decimal Price);to:
public record ProductResponse(
int Id,
string Name,
decimal Amount);ASP.NET Core may generate a perfectly valid updated OpenAPI document.
But the generated document now describes a different response.
Existing consumers expecting price may break.
Nothing about successful document generation guarantees backward compatibility.
A governed contract-first workflow asks a different question:
Does the new implementation still satisfy the API agreement that consumers rely upon?
That question requires comparison, review, and testing.
Client Generation Removes Repetitive Integration Work
One of the biggest practical benefits of OpenAPI is client generation.
Without generated clients, frontend developers often write HTTP integration code manually.
For example:
async function getProduct(id) {
const response = await fetch(`/api/products/${id}`);
if (!response.ok) {
throw new Error("Request failed");
}
return await response.json();
}This is manageable for one endpoint.
But imagine an application with 150 endpoints.
Every consumer must manage URLs, HTTP methods, serialization, authentication, response types, and errors.
Duplicated integration code becomes a maintenance problem.
OpenAPI-based generators can produce clients in languages such as C#, TypeScript, Java, and others.
Tools commonly used for this include NSwag, OpenAPI Generator, and Kiota, depending on the target ecosystem and API design.
Generating a C# Client With NSwag
Suppose our approved contract is stored at:
contracts/product-api.yamlA development team can use NSwag to generate a C# client.
For example, after installing the appropriate NSwag CLI:
nswag openapi2csclient \
/input:contracts/product-api.yaml \
/classname:ProductApiClient \
/namespace:Company.ProductApi.Client \
/output:Generated/ProductApiClient.csThe generated client can expose typed methods and response models based on the contract.
Instead of constructing raw HTTP requests throughout the application, consumers work through a consistent client abstraction.
This can reduce manual serialization code, improve discoverability, and make contract changes easier to identify during compilation.
Generated code is not a replacement for testing, however.
A generated client can accurately implement a contract that the server fails to honor.
Both sides still need validation.
Why Operation IDs Matter
Consider this OpenAPI operation:
operationId: getProductByIdThe operation ID gives the endpoint a stable logical identity.
Client generators frequently use operation IDs to derive method names.
For example:
await client.GetProductByIdAsync(42);Without deliberate operation IDs, generated method names may depend on generator-specific naming conventions.
That can produce awkward or unstable client APIs.
Imagine a developer changing an endpoint summary or route and unexpectedly triggering a generated method rename.
A stable operation ID helps reduce that risk.
For contract-first development, operation IDs should be treated as part of the client-facing design.
Generated Clients Should Be Reproducible
A reliable development process does not depend on one developer manually generating client code on their laptop.
Client generation should be repeatable.
For example:
Approved OpenAPI contract
↓
Pinned generator version
↓
Client generation
↓
Compile generated clients
↓
Run integration tests
↓
Publish client packageThe generator version matters.
Different versions may produce different naming conventions, serialization behavior, or generated structures.
Pinning versions makes builds more predictable.
Teams should also avoid manually editing generated files.
Those changes will usually disappear the next time the client is regenerated.
Custom behavior belongs in supported extension points, wrappers, partial classes where available, or separately maintained application code.
Contract-First Development Enables Parallel Work
Imagine a team developing a new subscription feature.
The backend team needs to implement billing rules.
The frontend team needs to build the subscription management interface.
The mobile team needs to display subscription status.
Without an agreed contract, everyone waits for the backend implementation.
With contract-first development, the teams can agree on:
GET /api/subscriptions/{id}Response:
{
"subscriptionId": 205,
"plan": "premium",
"status": "active",
"renewalDate": "2026-12-01"
}The frontend team can generate its client and develop against mock responses.
The mobile team can do the same.
The backend team can implement the actual endpoint independently.
Once implementation is complete, integration testing confirms whether the service satisfies the shared contract.
This reduces unnecessary coordination and makes parallel delivery more practical.
Mock Servers Help Consumers Start Early
An OpenAPI contract can also support mock server generation.
A mock server returns responses based on the contract without requiring the real business logic or database.
For example:
Frontend application
↓
Generated API client
↓
Mock server
↓
Example responsesThis is especially useful when backend implementation is still underway.
However, mock servers have limitations.
A schema-valid response does not necessarily reproduce real business behavior.
For example, a mock server may return an active subscription without modeling expiration, payment failures, or cancellation.
Mocks accelerate development.
They do not replace end-to-end integration testing.
Schema Governance Prevents Accidental API Drift
As organizations grow, API contracts need ownership.
Without governance, different teams make inconsistent decisions.
One service uses:
{
"customerId": 100
}Another uses:
{
"customer_id": 100
}A third uses:
{
"customerID": 100
}All three can work.
But inconsistency makes integration harder.
Schema governance establishes shared expectations.
These might include naming conventions, error responses, authentication descriptions, pagination patterns, versioning policies, operation IDs, and deprecation procedures.
The goal is not bureaucracy.
The goal is to make API behavior predictable across teams.
Establish an API Style Guide
A practical API style guide should answer common design questions.
For example, the organization may decide that JSON properties use camelCase.
Collection endpoints use a consistent pagination structure.
Errors use a common response format.
Resource identifiers use consistent naming.
Successful resource creation returns HTTP 201 where appropriate.
Authentication requirements are documented in the contract.
Every operation has a stable operation ID.
These conventions reduce repeated debates and help generated clients behave consistently.
A good style guide should remain small enough that developers can actually follow it.
Use Standard Error Responses
Error handling is a common source of API inconsistency.
One endpoint returns:
{
"error": "Invalid order"
}Another returns:
{
"message": "Order validation failed",
"code": 400
}A third returns a plain string.
Consumers now need several error-handling strategies.
ASP.NET Core supports standardized problem details responses based on RFC 7807 and related specifications.
A response might look like:
{
"type": "https://example.com/problems/validation",
"title": "Validation failed",
"status": 400,
"detail": "Quantity must be greater than zero."
}Contract-first teams can define consistent error schemas and document which status codes each operation returns.
That improves both client generation and error-handling predictability.
Breaking Changes Are the Biggest Governance Challenge
Consider the original response:
{
"id": 42,
"name": "Wireless Headphones",
"price": 89.99
}Now imagine the backend changes it to:
{
"id": 42,
"name": "Wireless Headphones",
"amount": 89.99
}The response is still valid JSON.
The endpoint still returns HTTP 200.
Unit tests may still pass.
But existing consumers may fail because the expected price property disappeared.
This is a breaking change.
Other common breaking changes include removing endpoints, changing HTTP methods, introducing new required request fields, changing property types, tightening accepted input values, or changing authentication requirements.
A contract-first workflow must detect these changes before deployment.
Not Every Schema Change Is Backward-Compatible
Adding an optional response property is often compatible with clients that ignore unknown fields.
But not every consumer behaves that way.
Some clients use strict deserialization or exhaustive schema validation.
Similarly, adding a new enum value can break consumers that assume the list of possible values never changes.
Consider:
{
"status": "pending"
}A client may expect only:
pending
completed
cancelledLater, the server introduces:
partially_fulfilledThe schema change may appear additive, but some generated clients or application logic may not handle the new value correctly.
Compatibility depends on both the contract and the behavior of existing consumers.
This is why governance needs explicit compatibility rules rather than simplistic assumptions.
Automate Breaking-Change Detection
Manual review is important, but automated checks make the process repeatable.
A useful pipeline looks like this:
Developer proposes contract change
↓
Validate OpenAPI syntax
↓
Run API style checks
↓
Compare with released contract
↓
Detect potential breaking changes
↓
Review and approve
↓
Generate clients
↓
Implement and testTools such as oasdiff can compare OpenAPI documents and identify many classes of breaking changes.
For example:
oasdiff breaking \
contracts/released.yaml \
contracts/proposed.yamlA CI pipeline can fail when a prohibited change is detected.
However, schema comparison tools cannot prove every aspect of behavioral compatibility.
They may not detect changes to business rules, sorting behavior, side effects, or undocumented assumptions.
Automated comparison is a safeguard, not a complete replacement for contract testing and review.
Contract Testing Verifies Real Behavior
A published OpenAPI document may describe:
GET /api/products/42as returning:
200 OKwith a particular JSON schema.
But what if the implementation actually returns a different structure?
The document is valid.
The implementation compiles.
The generated client compiles.
Yet the contract is violated.
Contract testing closes this gap.
A test sends requests to the running API and verifies that responses satisfy the approved specification.
For example, tests can verify that successful responses include required fields, that property types match the schema, that documented status codes are used, and that invalid requests are rejected appropriately.
Some teams also use consumer-driven contract testing, where consumers publish the expectations they rely on and providers verify those expectations.
The central principle remains the same.
The contract must describe what the API actually does, not merely what developers intended it to do.
Versioning Is a Compatibility Strategy
Eventually, some breaking changes are unavoidable.
Perhaps a product API must adopt a new pricing model.
The old response is:
{
"price": 89.99
}The new response is:
{
"pricing": {
"amount": 89.99,
"currency": "USD"
}
}One strategy is to introduce a new API version.
For example:
GET /api/v1/products/42and:
GET /api/v2/products/42Existing clients continue using version 1 while newer clients adopt version 2.
But versioning should not become an excuse to introduce unnecessary breaking changes.
Maintaining multiple versions increases testing, documentation, security, and operational costs.
The preferred approach is to evolve contracts compatibly whenever practical and introduce new versions when a genuinely incompatible change is required.
Deprecation Must Be Managed Deliberately
Publishing version 2 does not automatically mean version 1 can disappear.
Mobile applications may remain installed for years.
External partners may upgrade slowly.
Enterprise customers may follow fixed release schedules.
A responsible deprecation process communicates which API versions are affected, what consumers need to change, when support will end, and how migrations should proceed.
The organization should also measure actual usage of older versions.
Removing an API because a deadline passed is risky if critical consumers are still using it.
Good governance combines policy with operational evidence.
Security Requirements Belong in the Contract
API contracts should document authentication requirements.
For example, an OpenAPI document can describe bearer-token authentication:
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
security:
- bearerAuth: []This helps client generators and documentation tools understand how callers authenticate.
However, describing security in OpenAPI does not enforce it.
ASP.NET Core must still implement authentication and authorization correctly.
A contract may say that an endpoint requires a bearer token, but only the application’s middleware, endpoint policies, and supporting infrastructure can enforce that requirement.
Security documentation and runtime security must remain aligned.
Governance Should Not Slow Every Small Change
One concern with contract-first development is that it can become overly bureaucratic.
Imagine requiring an architecture committee meeting whenever a developer adds an optional response field.
That would quickly make developers avoid the process.
A better approach uses automation for routine checks.
Syntax validation runs automatically.
Style rules run automatically.
Breaking-change detection runs automatically.
Client generation runs automatically.
Human review focuses on meaningful design decisions and exceptions.
The process should become stricter as the risk increases, not simply as the number of endpoints grows.
Organizing Contracts Across Multiple Teams
In a larger organization, API contracts need a predictable home.
A repository might contain:
api-contracts/
│
├── catalog/
│ ├── v1/
│ │ └── openapi.yaml
│ └── v2/
│ └── openapi.yaml
│
├── orders/
│ └── v1/
│ └── openapi.yaml
│
└── payments/
└── v1/
└── openapi.yamlThis structure makes contract ownership and version history visible.
However, a single central repository is not mandatory.
Some organizations keep contracts alongside their services and publish approved versions to an API registry or artifact repository.
Either model can work.
What matters is that consumers can identify the authoritative contract version and retrieve it reliably.
Contract Ownership Is as Important as Contract Storage
A contract without an owner becomes difficult to evolve.
Who approves breaking changes?
Who reviews security requirements?
Who decides when an old version can be retired?
Who maintains generated client packages?
Who investigates mismatches between the specification and implementation?
These responsibilities should be explicit.
For example, a service team may own its API contract while a platform team maintains shared linting rules and CI templates.
That arrangement balances team autonomy with organization-wide consistency.
Treat Contracts as Versioned Artifacts
An API contract should not exist only as an editable document on a shared drive.
Approved contracts should have version history.
A consumer should be able to identify which contract version its generated client came from.
A deployment pipeline should know which contract version the service claims to implement.
A breaking-change check should compare proposed changes with an appropriate released baseline.
This creates traceability between API design, implementation, generated clients, and deployed behavior.
Integrating Contract Checks Into CI/CD
A practical contract-first CI pipeline might follow this sequence:
OpenAPI change submitted
↓
Validate document
↓
Apply style rules
↓
Compare against released contract
↓
Review compatibility
↓
Generate clients
↓
Compile generated clients
↓
Run implementation tests
↓
Run contract tests
↓
Publish approved artifactsNot every project needs all these stages immediately.
In our earlier article on CI/CD Pipelines with Azure DevOps for ASP.NET Core, we examined how automated build, testing, and deployment workflows improve release reliability. Adding OpenAPI validation, breaking-change detection, and contract testing extends those safeguards to the APIs that other applications depend on.
A small team can begin with OpenAPI validation, breaking-change detection, and one integration test suite.
As the number of consumers increases, client generation, artifact publishing, and compatibility policies become more valuable.
The important thing is to make the process repeatable.
A Real-World Example: An Order Management Platform
Consider an ASP.NET Core platform with three services.
The Catalog API provides product information.
The Orders API creates and tracks purchases.
The Payments API processes payments.
The website, mobile application, and partner integrations consume these services.
Without contract-first development, each team may independently interpret request and response structures.
An Orders developer changes a field.
The website adapts immediately.
The mobile team discovers the change after its next release.
A partner integration breaks in production.
Now consider the contract-first approach.
The Orders team proposes a schema change in a pull request.
Automated checks identify that an existing response property is being removed.
The team decides to preserve the original property temporarily while introducing the new structure.
Generated clients are updated against the approved contract.
Integration tests verify that the implementation supports the documented responses.
Consumers migrate at their own pace.
The old property is removed only through an agreed deprecation or versioning process.
The technical change is still necessary.
But its consequences are managed rather than discovered accidentally.
Common Contract-First API Mistakes
The first mistake is assuming that generating OpenAPI documentation automatically makes an API contract-first.
It does not. A generated specification is useful, but contract-first development requires the contract to guide implementation and undergo compatibility checks.
Another mistake is treating every schema-valid change as backward-compatible. Client behavior, strict validation, and business assumptions can still create breaking changes.
Some teams generate clients but never test them against the actual service. That creates confidence in code generation without verifying runtime compatibility.
Others allow developers to manually edit generated clients, creating changes that disappear during regeneration.
Another common problem is creating a governance process that requires excessive approval for routine changes.
Finally, teams sometimes publish contracts without assigning ownership or establishing deprecation policies.
The solution is not more documentation.
It is a small, enforceable process connecting design, implementation, testing, and release.
Contract-First Development Is About Predictability
The greatest benefit of contract-first development is not YAML.
It is predictability.
Backend developers know what they are implementing.
Frontend developers know what they can rely on.
Generated clients reduce repetitive integration work.
Contract tests expose mismatches.
Schema comparisons identify risky changes.
Governance makes API evolution deliberate.
For a single application with one backend and one frontend, some of these practices may seem unnecessary.
But as teams, services, devices, and external integrations multiply, the cost of unmanaged API changes rises quickly.
Contract-first development helps prevent that cost from becoming a permanent source of production failures and coordination overhead.
Coming Next
In the next article, we’ll explore Building Asynchronous APIs in ASP.NET Core: Long-Running Operations and the 202 Accepted Pattern.
Not every operation can finish within a normal HTTP request.
Some jobs take seconds, minutes, or even hours.
We’ll examine how to accept work without keeping clients waiting, return HTTP 202 Accepted, expose operation-status endpoints, handle background processing safely, and design reliable APIs for long-running operations.
Closing Thoughts
An API contract is a promise.
It tells consumers which operations exist, what information they must provide, what responses they can expect, and how failures are represented.
When that promise changes unexpectedly, even a technically correct backend deployment can break the applications that depend on it.
Contract-first API development makes the promise explicit before implementation begins.
OpenAPI provides a common description format.
Client generation turns that description into usable integration code.
Contract testing verifies that implementations honor the agreement.
Schema governance helps teams detect breaking changes, enforce consistency, and manage API evolution responsibly.
The objective is not to make API development slower or more formal than necessary.
It is to make change safer.
A well-designed API is not simply one that works today. It is one that consumers can continue trusting as the system evolves.
Subscribe Now
Enjoying the series? Subscribe to ASP Today for practical ASP.NET Core tutorials, production architecture guidance, and advanced API engineering strategies. Join our Substack Chat to discuss OpenAPI, contract-first development, client generation, schema governance, API compatibility, and building dependable integrations across modern .NET applications.


