Case Study 01: Specification-First API Development
Building a REST API by Writing an OpenAPI Spec First
Background
Priya Sharma is a backend developer at a mid-sized fintech startup. Her team has been tasked with building a new Transaction Categorization API -- a service that allows their mobile banking app to automatically categorize user transactions (groceries, dining, transportation, etc.) and provide spending summaries. The API will be consumed by three separate frontend teams: iOS, Android, and web.
In previous projects, Priya's team had used a code-first approach, building the API and then generating documentation afterward. This consistently led to problems: the frontend teams would discover undocumented edge cases, the response formats would shift between iterations, and integration testing was a constant source of friction. For this project, Priya decided to try a specification-first approach, writing a complete OpenAPI specification before any implementation code existed.
The Challenge
The Transaction Categorization API needed to support:
- Ingesting raw transaction data from multiple bank feed formats
- Categorizing transactions using configurable rule sets
- Allowing users to override automatic categorizations
- Providing spending summaries by category, time period, and merchant
- Supporting bulk operations for historical transaction imports
- Enforcing rate limits and authentication for all endpoints
The complexity was significant: the API would have 12 endpoints, multiple query parameter combinations, paginated list responses, and detailed error handling for malformed financial data. Getting the contract right before implementation would save the three frontend teams from constantly adapting to API changes.
Phase 1: Writing the OpenAPI Specification
Priya began by drafting the OpenAPI specification in YAML format. She started with the data models, knowing from experience that getting the schemas right was the foundation for everything else.
openapi: 3.1.0
info:
title: Transaction Categorization API
version: 1.0.0
description: |
Categorizes bank transactions and provides spending analytics.
All endpoints require Bearer token authentication.
Rate limit: 1000 requests per minute per API key.
servers:
- url: https://api.fintrack.example.com/v1
description: Production
- url: https://staging-api.fintrack.example.com/v1
description: Staging
components:
schemas:
Transaction:
type: object
required: [id, amount, currency, merchant_name, date, category]
properties:
id:
type: string
format: uuid
amount:
type: number
format: decimal
description: "Positive for credits, negative for debits"
currency:
type: string
pattern: "^[A-Z]{3}$"
example: "USD"
merchant_name:
type: string
maxLength: 200
merchant_category_code:
type: string
pattern: "^\\d{4}$"
description: "ISO 18245 MCC code"
date:
type: string
format: date
category:
$ref: '#/components/schemas/Category'
category_confidence:
type: number
minimum: 0.0
maximum: 1.0
description: "AI confidence score for auto-categorization"
is_user_override:
type: boolean
default: false
notes:
type: string
maxLength: 500
Category:
type: object
required: [id, name, group]
properties:
id:
type: string
format: uuid
name:
type: string
enum: [groceries, dining, transportation, housing,
utilities, entertainment, healthcare, shopping,
travel, education, income, transfer, other]
group:
type: string
enum: [essentials, lifestyle, financial, other]
icon:
type: string
color:
type: string
pattern: "^#[0-9A-Fa-f]{6}$"
SpendingSummary:
type: object
required: [period_start, period_end, total_spent, by_category]
properties:
period_start:
type: string
format: date
period_end:
type: string
format: date
total_spent:
type: number
format: decimal
total_income:
type: number
format: decimal
by_category:
type: array
items:
type: object
properties:
category:
type: string
amount:
type: number
format: decimal
percentage:
type: number
minimum: 0
maximum: 100
transaction_count:
type: integer
ErrorResponse:
type: object
required: [error_code, message]
properties:
error_code:
type: string
description: "Machine-readable error code"
message:
type: string
description: "Human-readable error message"
details:
type: array
items:
type: object
properties:
field:
type: string
issue:
type: string
She then defined the endpoints, paying particular attention to error responses:
paths:
/transactions:
post:
operationId: createTransaction
summary: Create a single transaction
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateTransactionRequest'
responses:
'201':
description: Transaction created and categorized
content:
application/json:
schema:
$ref: '#/components/schemas/Transaction'
'400':
description: Invalid transaction data
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
examples:
invalid_amount:
value:
error_code: "INVALID_AMOUNT"
message: "Amount must be a valid decimal number"
invalid_currency:
value:
error_code: "INVALID_CURRENCY"
message: "Currency must be a valid ISO 4217 code"
'401':
description: Authentication required
'429':
description: Rate limit exceeded
headers:
Retry-After:
schema:
type: integer
description: Seconds until rate limit resets
get:
operationId: listTransactions
summary: List transactions with filtering
parameters:
- name: start_date
in: query
schema:
type: string
format: date
- name: end_date
in: query
schema:
type: string
format: date
- name: category
in: query
schema:
type: string
- name: min_amount
in: query
schema:
type: number
- name: max_amount
in: query
schema:
type: number
- name: merchant
in: query
schema:
type: string
description: "Partial match, case-insensitive"
- name: page
in: query
schema:
type: integer
minimum: 1
default: 1
- name: per_page
in: query
schema:
type: integer
minimum: 1
maximum: 100
default: 25
- name: sort
in: query
schema:
type: string
enum: [date_asc, date_desc, amount_asc, amount_desc]
default: date_desc
responses:
'200':
description: Paginated transaction list
content:
application/json:
schema:
type: object
properties:
transactions:
type: array
items:
$ref: '#/components/schemas/Transaction'
pagination:
$ref: '#/components/schemas/PaginationInfo'
The full specification ran to 450 lines, covering all 12 endpoints with complete request/response schemas.
Phase 2: Review and Validation
Before writing any code, Priya shared the OpenAPI specification with the three frontend teams. They loaded it into Swagger UI and could immediately see every endpoint, try sample requests, and review the response formats. The iOS team identified that they needed an additional field (original_currency and exchange_rate) for international transactions. The web team requested that the spending summary endpoint support grouping by week, not just by month. These changes were made to the YAML file in minutes -- far easier than refactoring implemented code.
Priya also used an OpenAPI linter to validate the specification for consistency and completeness, catching several issues: a missing required field in one schema, an endpoint that referenced a non-existent component, and inconsistent pagination parameter names.
Phase 3: AI-Powered Implementation
With the validated specification in hand, Priya used it as the primary prompt for her AI coding assistant. Her prompt strategy was methodical:
Prompt 1: Data models and database layer
Using the component schemas from this OpenAPI specification,
generate SQLAlchemy 2.x models with async support for PostgreSQL.
Include all constraints, relationships, and indexes suggested by the
schema. [attached: full OpenAPI spec]
The AI generated models that exactly matched her schema definitions -- UUID primary keys, decimal amounts with appropriate precision, enum constraints, and proper indexes for the query patterns implied by the list endpoint's filter parameters.
Prompt 2: API endpoints, one group at a time
Using this OpenAPI specification, implement the /transactions
endpoints (POST, GET, GET/{id}, PUT/{id}, DELETE/{id}) using FastAPI.
Use the SQLAlchemy models from the previous step. Implement all
defined error responses with the exact error codes shown in the spec.
[attached: OpenAPI spec + generated models]
Prompt 3: Spending analytics endpoints
Implement the /analytics/spending endpoint from this spec. It must
support grouping by day, week, and month. Use CTEs for the SQL queries
to maintain readability. [attached: relevant spec section + models]
Each prompt produced code that was remarkably close to the desired output. The AI matched the exact response formats, implemented all the error codes defined in the spec, and used the correct HTTP status codes because they were all explicitly defined.
Phase 4: Contract Testing
Because the specification existed before the code, Priya used Schemathesis (an API testing tool that generates test cases from OpenAPI specs) to automatically test every endpoint against the contract. The tool found three issues:
- The bulk import endpoint accepted arrays larger than the specified maximum of 500 items
- The merchant search did not properly handle special characters in query parameters
- One error response returned
errorinstead of the specifiederror_codefield name
All three were quick fixes. Without the specification as a contract, these inconsistencies might not have been caught until the frontend teams encountered them in integration.
Results and Lessons Learned
The specification-first approach delivered measurable benefits:
Time savings: The total project took 3 weeks. Priya estimated that a code-first approach would have taken 4-5 weeks, with the extra time spent on integration issues and API changes.
Fewer integration bugs: The frontend teams reported zero API contract surprises during integration. Every response matched what they had seen in the Swagger documentation.
Better AI output quality: Priya estimated that 80% of the AI-generated code was usable with minimal changes, compared to roughly 50% in previous projects using conversational prompts.
Living documentation: The OpenAPI specification served as always-current documentation, automatically rendered as interactive API docs for the team.
Key lessons Priya documented for her team:
-
Invest time in schema design first. The component schemas took the longest to write but paid off the most. Getting the data models right meant every endpoint built on a solid foundation.
-
Include error response examples in the spec. Specific error codes and messages in the specification caused the AI to generate specific error handling rather than generic try/except blocks.
-
Use the spec for validation, not just documentation. Tools like Schemathesis and openapi-spec-validator turned the specification into an automated quality gate.
-
Feed the spec to AI in logical sections, not all at once. The full 450-line specification was too long for a single prompt. Breaking it into model generation, then endpoint groups, then analytics produced better results.
-
Keep the spec as the source of truth. When changes were needed, Priya updated the specification first, then regenerated the affected code. This maintained the specification's role as the single source of truth.
Conclusion
Priya's experience demonstrates the core thesis of specification-driven prompting: the time invested in writing a clear specification is repaid many times over in reduced iteration, fewer bugs, and more predictable AI output. For APIs consumed by multiple teams, the specification-first approach is not just beneficial -- it is essential.
The OpenAPI specification served three roles simultaneously: a communication tool for the frontend teams, a precision prompt for the AI coding assistant, and a contract testing foundation for quality assurance. This triple duty makes API specifications one of the highest-value artifacts in specification-driven development.