12 minute read

Most ASP.NET Core APIs are built code-first. You define the endpoints in C#, run the application, and generate an OpenAPI document from what the application exposes. If the document is mainly there for Swagger UI, documentation, client generation, or other tooling, that is a perfectly reasonable workflow.

The problem starts when the OpenAPI document is supposed to be more than a description of an implementation that already exists.

If several teams need to agree on an API boundary, a generated document tells consumers what the producer decided to build. An authored contract records what producers and consumers agreed to build. Reviewing the generated document in CI can catch changes, but the implementation is still the proposal and the contract still comes afterward.

The contract is most useful while it can still influence what gets built. That does not mean designing the entire API up front and freezing it. It means making the boundary explicit early enough that the people affected by it can reason about it before implementation details harden into decisions for everyone else.

I wrote earlier about making OpenAPI the source of truth for ASP.NET Core Minimal APIs. That post focused on the mechanics of taking an authored OpenAPI document and generating the Minimal API surface from it. This one is about why I think that workflow matters in the first place.

Conversations do not scale as contracts

My team builds services used by several other teams. In a small integration, an informal conversation between producer and consumer can be enough. We can discuss what needs to be exchanged, agree on a rough shape, and one side can implement it.

I have worked exactly that way before. We had a conversation about the data two teams needed to exchange, then my team decided the concrete API shape and the other team caught up with what we built. For that particular project, the scope was small and the API did not change much, so it worked well enough.

That model becomes much harder to sustain once one producer has several independent consumers.

Decisions made in meetings or point-to-point chats are easy to lose or interpret differently. When contract changes happen through pull requests, the proposed change, its reviewers, and the discussion around it have a durable place alongside the contract.

We already work this way for gRPC contracts. The producing team owns the repository, but consumer teams are welcome to collaborate there. A proposed change becomes a pull request. The change is visible as a diff, the discussion stays attached to it, and the resulting contract is versioned in Git.

Ownership and authority are not quite the same thing here. The producer can own the repository and the service while still involving consumers in decisions that affect them. Breaking changes should be agreed with the teams that bear their cost. Additive changes do not need a committee, but they should still be designed with enough care so that avoidable breaking changes are not baked in for later.

For this kind of API, the OpenAPI document becomes a versioned, reviewable artifact owned by the producer but open to collaboration from its consumers. Changes happen there first, so the contract is not just a description of the boundary; it is where the boundary is negotiated.

The goal is not governance for its own sake. It is to give the boundary one place where the people who depend on it can collaborate.

HTTP APIs leave a lot of decisions open

Part of the reason this matters to me is that designing an HTTP API leaves a surprisingly large decision space to every team.

We decide route shapes, HTTP methods, request and response models, required fields, parameter locations, status codes, error representations, naming conventions, and a long list of smaller details.

That flexibility is one reason HTTP APIs work so well across team and technology boundaries. They do not force everybody into one programming model. But every open choice is still a choice somebody has to make.

Contract-first does not remove that flexibility. It resolves those choices once for a particular boundary, in a place where the affected teams can see and discuss them. After that, the implementation should not have to make the same decisions again.

This is familiar to me from WCF and later gRPC. In both cases, I got used to a workflow where the boundary exists independently of the implementation. The service code is written inside that boundary rather than defining it as a side effect. Applying the same idea to HTTP APIs felt like a natural progression.

The point is not to take useful decisions away from developers. It is to avoid making the same transport decisions again after the boundary has already been agreed, leaving more attention for the behavior that actually makes the service useful.

Contract-first does not mean contract-final

The obvious objection is that this starts to sound like big design up front. I do not think it needs to be.

A useful contract can start with a handful of endpoints and a few shared components. It can grow as the producer learns more about the implementation and consumers learn more about what they actually need. There is no requirement to complete the entire API before anybody writes code.

Implementation can also prove the contract wrong. If a supposedly convenient request shape turns out to be awkward, or a response model does not support a real use case, the contract should change. The difference is that the change goes through the same explicit collaboration point. Each team involved can project what the proposed variation means for its own code before that variation becomes the new reality.

It also does not magically prevent breaking changes. It makes them visible while they are still proposals. That is valuable because it is cheaper to disagree about authored YAML than about YAML derived from C# that somebody has already implemented. At that point, the producer may already need to rework the endpoint and one or more consumers may already have adapted their clients to the previous shape.

Moving some of the discussion earlier is deliberate. The disagreement already exists; the only question is whether it happens while changing the boundary still means editing a few lines in a pull request, or after one or more teams have committed implementation work to the previous assumption.

The contract should belong to the boundary

When I talk about an authored OpenAPI document with .NET developers, one question comes up quickly: why YAML? Why not expose the contract as C#?

Sharing C# types can work very well when every participant is using .NET. We already publish contract packages in other contexts for exactly that reason. But a C# package is a .NET representation of the contract, not a language-neutral description of the HTTP boundary itself.

The boundary is more than DTOs. It includes paths, methods, parameter locations, status codes, content types, request and response shapes, and transport-level constraints. Once those decisions are represented as C#, the implementation language of one side has started leaking into the agreement between both sides.

OpenAPI gives that boundary an implementation-independent representation. A .NET consumer can still consume generated or packaged C# contracts. A TypeScript consumer can generate whatever fits its stack. Another consumer may use the OpenAPI document directly. Those are all projections of the same source.

That distinction is important to me:

A C# package tells a .NET consumer how to represent the contract. OpenAPI tells every consumer what the contract is.

Markdown could also describe an agreement, but a machine-readable contract can do more than preserve the discussion. Consumers can generate clients, validate assumptions, or use the contract to create mocks before the real server exists. The same document can also be used as structured input when scaffolding a mock server in the consumer’s stack.

Git is still the minimum viable source of truth. Packaging is a distribution convenience, not what makes the contract authoritative. In our gRPC workflow, .NET teams can consume the contract packages we publish from the contract repository, while other consumers are free to use the source in whatever way makes sense for them. The same model can apply to OpenAPI.

A source of truth has to affect the build

Putting openapi.yaml in a repository and calling it authoritative does not guarantee anything by itself.

If developers still recreate routes, request models, response types, and endpoint metadata independently in the server, the document and implementation can drift. A stale specification does not become useful just because the team has declared it the source of truth.

This is where I think the implementation should deliberately give up some freedom. The transport-level decisions have already been made in the contract. The server should implement the behavior behind those decisions, not reinterpret the HTTP surface a second time.

NSwag has supported generating server-side code from OpenAPI for years, especially around controller-based APIs. MinimalOpenAPI applies the same source-of-truth idea to ASP.NET Core Minimal APIs. It takes the authored OpenAPI document and generates the HTTP-facing plumbing: strongly typed contracts, endpoint shapes, dependency-injection registration, mappings, and the metadata needed to expose the endpoints. The handwritten application owns dependencies and behavior. The generated layer owns the shape that was already agreed.

For example, making an existing isbn property mandatory on a response can be a very small diff:

 Book:
   type: object
   required:
     - id
     - title
+    - isbn
   properties:
     id:
       type: string
     title:
       type: string
     isbn:
       type: string

The diff is small, but the decision may not be. Before it is merged, every affected team can see exactly what is becoming mandatory.

With a document that is only generated from code, the implementation can change first and the new contract appears afterward. With MinimalOpenAPI, the contract changes first. Once the producer consumes that updated document, the generated response type gains the required member. Handwritten code that constructs that response without supplying the new value no longer compiles, so the implementation has to catch up before the build is green again.

That does not prove that the service behaves correctly at runtime. It does not validate every business invariant, and OpenAPI cannot express every concern of a service. But it removes one important class of drift: the application is no longer free to describe one HTTP boundary in YAML and implement another one independently in C#.

Good code-first tooling made the mismatch more obvious

One of the things that pushed me further in this direction was a session by Sander ten Brinke at Swetugg 2026 about generating good OpenAPI documents from ASP.NET Core applications.

The session was not an argument against code-first. Quite the opposite: it covered the best practices and gotchas needed to make that workflow work properly.

What struck me was how much the application has to participate when using ASP.NET Core’s Microsoft.Extensions.ApiDescription.Server build-time OpenAPI generation. The application still needs to build and start far enough for the tooling to inspect it. Depending on its registrations, dependencies that make sense at runtime may need substitutes or special handling just so the application can start in that context. .NET 11 RC1 even added a hook to select the hosting environment used during build-time OpenAPI generation.

All of that solves real problems in an implementation-first workflow. It also highlights a boundary question: if several teams need the OpenAPI document independently, why should obtaining it require bootstrapping the implementation it describes?

The extra machinery is not proof that code-first is wrong. It is a clue that we may be asking an implementation-first workflow to solve a collaboration problem that exists before the implementation.

For APIs where OpenAPI is primarily generated documentation, none of this bothers me. For APIs where the document is the thing multiple teams are supposed to agree on, authoring it directly feels much less bolted on.

You can migrate without rewriting the API

Existing APIs can move toward this model incrementally.

A practical migration can start from the code-first application you already have: run it, generate the OpenAPI document, and save that document as a file. At that point, the generated artifact becomes the starting point rather than the final output.

Then refine it. Clean up shapes that exist mostly because they were convenient in C#. Move operation-specific request or response schemas inline when they are only meaningful to one endpoint. Put the document somewhere all stakeholders can review it and start treating future changes there as the authoritative ones.

From there, the server can move gradually as well. Start by bringing a subset of the API under the contract-first path, then expand that surface over time rather than switching the entire application at once.

That incremental path matters because the point of contract-first is not ceremony. It is to create a better collaboration boundary. If adopting it requires stopping delivery until an existing API has been redesigned from scratch, the process has defeated the purpose.

OpenAPI is not the whole service agreement

I use “contract” here deliberately, but OpenAPI does not capture every agreement around a service.

It describes the HTTP-facing surface well: operations, paths, parameters, request and response bodies, status codes, schemas, and many transport-level constraints. That is already a large and valuable part of what producers and consumers need to agree on.

Other concerns sit outside it or are only partially represented. Authorization policies, organizational ownership, environment-specific hosts, deployment details, and many business semantics require other documentation or mechanisms.

That is why I see contract-first as broader than schema-first. The OpenAPI document is the main machine-readable artifact for the HTTP boundary, not a complete description of every responsibility the service has.

What changed since April

When I wrote the previous post, MinimalOpenAPI was still a prerelease experiment. It has since reached 1.0, which makes this a useful moment to step back from the implementation mechanics and look at the workflow the library is trying to support.

Taking the project to 1.0 kept bringing me back to the same question: if the contract is the agreement between independently moving teams, how much of that boundary should the server be allowed to reinterpret?

MinimalOpenAPI is one answer for ASP.NET Core Minimal APIs. It is not the reason to work contract-first; it is a way to make that workflow practical without giving up the Minimal API programming model.

I will also be talking about this workflow at the September 28 Swenug Stockholm meetup, including the implementation mechanics and trade-offs behind MinimalOpenAPI.

Recap

Code-first OpenAPI remains a good fit when the implementation naturally is the source of truth. If one team owns both sides and the document mainly exists for documentation or tooling, generating it from the application is simple and effective.

Contract-first becomes more interesting when the boundary has to exist independently of one implementation. A versioned, reviewable OpenAPI document gives producers and consumers one place to agree on that boundary, lets each team reason about changes before they harden into code, and gives tooling something useful to consume before the server exists.

MinimalOpenAPI makes that agreement affect the ASP.NET Core implementation directly instead of merely documenting it afterward. The important choice is not YAML versus C#. It is whether the boundary is an agreement before it becomes an implementation detail.

This article was produced using an AI-assisted editorial process and was reviewed with AI assistance. Read about my editorial process.

Support this blog

If you liked this article, consider supporting this blog by buying me a pizza!