Sitemap

Code-First vs Interface-First

7 min readOct 14, 2026

--

Does giving an AI a formal interface reduce the tokens it burns and improve the code it produces?

I wanted to test something. When you use AI to write code, does it fundamentally work code-first or interface-first? And does giving it interfaces actually improve two measurable things: the tokens it uses and the quality of the code that the AI produces? To do this, I set up a small experiment. I created two distinct NestJS projects, one called code-first and the other interface-first, and asked Claude Code to record each step of the session: the prompts I gave, the actions it took, and its own estimate of the tokens used for each. Before diving into the experiment and its results, let’s recap the two methodologies.

Code-first: developers draw on their experience and skills to turn the specification into working functionality. Today, this task is split between the developer and an AI assistant. The developer expresses the desired functionality in plain human language, delegating the programming syntax to the machine.

Interface-first: this approach, by contrast, requires a formal definition of the requirements in a human and machine-readable document, such as an OpenAPI specification file. This lets a team define what the service should expose and what its resources look like. The document becomes the de facto source of truth.

Code-first: building without a target

I set out to build a simple controller for a pet shop (yes, I know, not the pet shop again). So, as you would in code-first, I started by creating the controller scaffolding, but remember, I don’t code; the AI assistant does. So here’s my first prompt:

Add only the pets controller, using DTO definitions, and implement the CRUD operations in code-first

And as expected, in a matter of seconds, the AI has delivered some initial scaffolding; no news there, yet I was curious to look at the data in terms of token consumption

Press enter or click to view image in full size
Records of the token used

Ok, so I spent approximately 250 tokens to get my controller scaffolding. And this was the result

export class CreatePetDto {
name: string;
}

//////

interface Pet extends CreatePetDto {
id: number;
}

@Controller('pets')
export class PetsController {
private pets: Pet[] = [];
private nextId = 1;

@Post()
create(@Body() createPetDto: CreatePetDto): Pet {
const pet: Pet = { id: this.nextId++, ...createPetDto };
this.pets.push(pet);
return pet;
}

@Get()
findAll(): Pet[] {
return this.pets;
}

@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number): Pet {
return this.findOrThrow(id);
}

// ...
}

And then I said, let’s add a field, and of course I typed the wrong prompt:

In the code-first correct the Pet adding a name field of type string

Before I could even blink, the AI added code I didn’t want it to touch: it introduced an entity that I didn’t want, at least not at this stage. Granted, my prompt was not, by any stretch of the imagination, perfect. But what was done was done, and here is the result in terms of tokens.

Press enter or click to view image in full size

So I tried to roll back and delete the entity, again through prompting. After spending even more tokens, I was not closer to my goal of defining the full controller interface. At this rate, I would need thousands of tokens to barely describe the initial REST interface.

Interface-first: how to share knowledge

The problem I encountered with my prompting isn’t exclusively related to human-AI communication; it is much older and can be summarised as:

How do we communicate specifications and requirements effectively?

For REST APIs, this problem has already been solved by the OpenAPI (formerly known as Swagger) specification, which lets developers define, before coding, a human-machine contract describing endpoints and models.

Because OpenAPI is a formal and structured format, with explicit types, schemas and required fields, it is much less ambiguous than natural language, for example :

I want an endpoint x that accepts y with the verb z

Tools to auto-generate REST controller scaffolding already exist; the OpenAPI Generator comes to mind. But these tools rely on strict parsing, whereas an AI coding assistant can leverage its ability to understand context and human language.

Get Balint Maschio’s stories in your inbox

Join Medium for free to get updates from this writer.

Enough theory; let’s get practical. My first prompt for the interface-first project was:

in the interface first implement only the pets controller using the specifications in the @../interface-first/swagger.json and using the DTO

{
"Category": {
"type": "object",
"properties": {
"id": { "type": "integer", "format": "int64" },
"name": { "type": "string" }
}
},
"Pet": {
"type": "object",
"required": ["name", "cost", "category", "age"],
"properties": {
"id": { "type": "integer", "format": "int64" },
"category": { "$ref": "#/definitions/Category" },
"name": { "type": "string", "example": "doggie" },
"cost": { "type": "number", "format": "float" },
"age": { "type": "number", "format": "int32" },
"tags": {
"type": "array",
"items": { "$ref": "#/definitions/Tag" }
},
"status": {
"type": "string",
"description": "pet status in the store",
"enum": ["available", "pending", "sold"]
}
}
},
"Tag": {
"type": "object",
"properties": {
"id": { "type": "integer", "format": "int64" },
"name": { "type": "string" }
}
}
}

into

export interface CategoryDto {
id?: number;
name?: string;
}

export interface TagDto {
id?: number;
name?: string;
}

export enum PetStatus {
AVAILABLE = 'available',
PENDING = 'pending',
SOLD = 'sold',
}

export class CreatePetDto {
name: string;
cost?: number;
category?: CategoryDto;
tags?: TagDto[];
status?: PetStatus;
}

export class UpdatePetDto implements Partial<CreatePetDto> {
name?: string;
cost?: CreatePetDto['cost'];
category?: CreatePetDto['category'];
tags?: CreatePetDto['tags'];
status?: CreatePetDto['status'];
}

interface Pet extends CreatePetDto {
id: number;
}

and from this

"/pets/{petId}": {
"get": {
"tags": ["pet"],
"summary": "Find pet by ID",
"description": "Returns a single pet",
"operationId": "getPetById",
"produces": ["application/json"],
"parameters": [
{
"name": "petId",
"in": "path",
"description": "ID of pet to return",
"required": true,
"type": "integer",
"format": "int64"
}
],
"responses": {
"200": {
"description": "successful operation",
"schema": { "$ref": "#/definitions/Pet" }
},
"400": { "description": "Invalid ID supplied" },
"404": { "description": "Pet not found" }
}
}
}

to this

@Get(':petId')
findOne(@Param('petId', ParseIntPipe) petId: number): Pet {
return this.findOrThrow(petId);
}

and

@Post()
create(@Body() createPetDto: CreatePetDto): Pet {
const pet: Pet = { id: this.nextId++, ...createPetDto };
this.pets.push(pet);
return pet;
}

But let’s look at the tokens used.

Press enter or click to view image in full size

For fewer than 300 tokens, we have a complete translation of the OpenAPI definition into a NestJS controller. Where is the AI’s added value, then, you might ask? Let me show you something.

"patch": {
"tags": ["pet"],
"summary": "Partially update an existing pet",
"description": "",
"operationId": "patchPet",
"consumes": ["application/json"],
"produces": ["application/json"],
"parameters": [
{
"name": "petId",
"in": "path",
"description": "ID of pet to update",
"required": true,
"type": "integer",
"format": "int64"
},
{
"in": "body",
"name": "body",
"description": "Pet fields to update",
"required": true,
"schema": {
"$ref": "#/definitions/Pet"
}
}
],
"responses": {
"200": {
"description": "successful operation",
"schema": {
"$ref": "#/definitions/Pet"
}
},
"400": {
"description": "Invalid ID supplied"
},
"404": {
"description": "Pet not found"
},
"405": {
"description": "Validation exception"
}
}
}

In the spec, the PATCH operation takes a Pet as its body — and Pet marks name as required. Yet the resulting UpdatePetDto has all its fields optional.

import { CreatePetDto } from './create-pet.dto';

export class UpdatePetDto implements Partial<CreatePetDto> {
name?: string;
cost?: CreatePetDto['cost'];
category?: CreatePetDto['category'];
age?: CreatePetDto['age'];
tags?: CreatePetDto['tags'];
status?: CreatePetDto['status'];
}

That’s the AI introducing its own knowledge: a PATCH body should be entirely optional, because a partial update shouldn’t force you to resend the name.

A strict codegen tool would have propagated the required name straight from the spec; the AI overrode it based on what PATCH is supposed to mean.

Note that the AI did not provide any warning of the differences between the OpenAPI definition and the AI-generated code. I believe that with a specific skill, we could get the AI to check for mismatches and flag them.

And what about changes? Here is what happened when I updated the specification.

Press enter or click to view image in full size

The prompt does not describe the changes at all; it asks the AI to align the code with the new specification. The changes live in the Swagger.json that can be versioned and tracked.

Conclusions

Conclusions are always the hardest part to write. The aim of this article wasn’t to demonise or dismiss prompting, but to show that an interface-first approach can also be applied to AI-assisted programming.

Given the limits of this experiment, the findings are qualitative rather than quantitative: for a quantitative result, we would need to run the same experiment thousands of times and measure how token consumption varies.

What this approach brings is repeatability and traceability; the specifications are shared, and the AI is fed with a precise reference for understanding what the programmer wants at each iteration. After all, you wouldn’t hand a complex service specification to a human colleague over instant messaging.

--

--