Contract First or Die Trying: The Only Sane Way to Design APIs
I need to get something off my chest. Something that has been festering in my soul since the first time I joined a project mid-development and asked the fateful question: “Where is the API documentation?”
The answer, invariably, was one of the following:
- “Check the Postman collection.” (Translation: a graveyard of 200 requests, half of which are outdated, named things like
GET users FINAL v2 (copy)) - “Just look at the code.” (Translation: reverse-engineer our spaghetti and good luck)
- “We’ll document it later.” (Translation: we will never document it)

This, my friends, is the consequence of Code First API design. And I am here to tell you that it is a disease. A contagious, project-killing disease that spreads through teams like a virus through a poorly ventilated office.
The Crime Scene: Code First
Let me paint you a picture. You are a backend developer. You receive a ticket: “Create endpoint to fetch user transactions.” You open your IDE, you create a Controller, you wire up a Service, you return some DTO you invented on the spot, and you push to develop. Done. Ship it.
Meanwhile, the frontend developer is waiting. They ask you: “What does the response look like?” You say: “Just call it and see.” They call it. They see a field called txn_dt. They ask: “Is this a timestamp? A string? What timezone?” You shrug. “It’s whatever Java’s LocalDateTime serializes to by default.”
Congratulations. You have just created a communication disaster wrapped in a technical debt sandwich.
The mobile developer, who joined the call five minutes late, now has to implement the same endpoint. They look at the “documentation” (the Postman collection, remember?) and find a request from three sprints ago that returns a completely different structure because someone refactored the DTO and forgot to update anything.
This is Code First. You write the code, and the contract is whatever the code happens to produce. The API specification is an afterthought, a side effect, an accident.
The Salvation: Contract First
Now imagine a parallel universe where sanity prevails.
Before anyone writes a single line of Java, Go, or whatever poison you prefer, the team sits down and defines the contract. This contract is a .yaml or .json file written in OpenAPI (formerly Swagger). It specifies:
- Every endpoint.
- Every HTTP method.
- Every request parameter and its type.
- Every response body and its structure.
- Every possible error code and what it means.
This file becomes the single source of truth. The backend generates server stubs from it. The frontend generates client SDKs from it. The mobile team generates their models from it. Everyone is working against the same specification, synchronized like a well-conducted orchestra.
openapi: 3.0.3
info:
title: Transaction API
version: 1.0.0
paths:
/users/{userId}/transactions:
get:
summary: Fetch user transactions
parameters:
- name: userId
in: path
required: true
schema:
type: string
format: uuid
responses:
'200':
description: A list of transactions
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Transaction'
'404':
description: User not found
components:
schemas:
Transaction:
type: object
properties:
id:
type: string
format: uuid
amount:
type: integer
description: Amount in cents
currency:
type: string
example: "EUR"
timestamp:
type: string
format: date-time
required:
- id
- amount
- currency
- timestamp
Look at that. Beautiful. Explicit. No ambiguity. The timestamp is a date-time. The amount is in cents (no floating point nightmares). The userId is a UUID, not “whatever string you feel like sending.”
If the backend deviates from this contract, the generated code won’t compile. If the frontend expects a different field, the generated client will scream at them. The contract enforces discipline.
“But Writing YAML is Boring”
Yes. It is. You know what else is boring? Spending four hours on a video call debugging why the Android app is crashing because someone on the backend decided to rename user_id to userId without telling anyone.
You know what is boring? Rewriting integration tests because the response structure changed and no one updated the mocks.
You know what is soul-crushingly boring? Being the poor bastard who joins a two-year-old project and has to understand 150 endpoints by reading Controller files scattered across 47 packages with no documentation whatsoever.
Writing the contract upfront is an investment. It is like brushing your teeth: tedious in the moment, but it prevents a root canal later.
The Tooling: Java Edition
Since I promised code examples, let me show you how this works in the Spring ecosystem. We use a Maven plugin called openapi-generator-maven-plugin. You point it at your contract, and it generates the interfaces for you.
<plugin>
<groupId>org.openapitools</groupId>
<artifactId>openapi-generator-maven-plugin</artifactId>
<version>7.2.0</version>
<executions>
<execution>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<inputSpec>${project.basedir}/src/main/resources/api/openapi.yaml</inputSpec>
<generatorName>spring</generatorName>
<apiPackage>me.doismiu.api.generated</apiPackage>
<modelPackage>me.doismiu.api.generated.model</modelPackage>
<configOptions>
<interfaceOnly>true</interfaceOnly>
<useSpringBoot3>true</useSpringBoot3>
<useTags>true</useTags>
</configOptions>
</configuration>
</execution>
</executions>
</plugin>
Run mvn compile, and suddenly you have a Java interface that looks like this:
@Generated(value = "org.openapitools.codegen.languages.SpringCodegen")
@Tag(name = "Transactions")
public interface TransactionsApi {
@Operation(summary = "Fetch user transactions")
@GetMapping(value = "/users/{userId}/transactions", produces = "application/json")
ResponseEntity<List<Transaction>> getUserTransactions(
@PathVariable("userId") UUID userId
);
}
Your Controller simply implements this interface:
@RestController
public class TransactionsController implements TransactionsApi {
private final TransactionService service;
public TransactionsController(TransactionService service) {
this.service = service;
}
@Override
public ResponseEntity<List<Transaction>> getUserTransactions(UUID userId) {
return ResponseEntity.ok(service.findByUser(userId));
}
}
If you try to change the return type to something that doesn’t match the contract, the compiler will slap you. If you try to add a parameter that isn’t in the spec, the compiler will slap you harder. The contract is law, and the code must obey.
The Cultural Problem
Here is the uncomfortable truth: Contract First is not a technical challenge. It is a cultural one.
Developers resist it because it feels like “extra work.” Product managers resist it because it “slows down delivery.” Everyone wants to move fast and break things, except the things they break are the sanity of their colleagues and the stability of the system.
Contract First forces you to think before you code. It forces you to have conversations about data types, edge cases, and error handling before they become production incidents. It forces discipline in an industry that worships “move fast.”
And that is exactly why most teams refuse to adopt it. Discipline is hard. Chaos is easy. Until it isn’t.
When NOT to Use Contract First
I’ll be fair. There are scenarios where Code First makes sense:
- Prototyping: You are exploring, you don’t know what the API will look like yet, and you need to iterate fast. Fine. But the moment it goes to production, you write that contract.
- Internal microservices with gRPC: If you are already using Protobuf (as I ranted about previously), the
.protofile is your contract. You are already doing Contract First without calling it that. - Solo projects: If you are the only developer, frontend and backend, you can keep the contract in your head. But the moment someone else joins, write it down.
For everything else, especially teams larger than one person, especially projects expected to live longer than a hackathon, Contract First is non-negotiable.
Conclusion
I have seen projects die because no one knew what the API was supposed to do. I have seen developers quit because they spent more time reverse-engineering undocumented endpoints than actually building features. I have seen production outages caused by “minor changes” that broke every client because there was no contract to validate against.
Contract First is not a methodology. It is a survival strategy. It is the difference between building on solid ground and building on quicksand while blindfolded.
Write the contract. Generate the code. Sleep at night.
Or don’t. Keep living in chaos. Keep debugging Postman collections at 2 AM. Keep explaining to the mobile team why the field is now called transactionDate instead of txn_dt for the third time this month.
Your call.
