API Prototyper: design before code
Lesson 8 of 12
8API Prototyper: design before code
A project without a single line of code: you turn a simple idea into a written contract any developer could build from, requirements in plain words, then resources and roles, then an endpoints table, then a valid OpenAPI file you verify yourself, with the AI as a conversation partner rather than a dictation machine.
What the picture shows: The idea becomes requirements, the requirements become resources and routes, and those become an OpenAPI file that passes the validator before any code.
Map of this part
The concepts in order, click any one to jump to it:
- Why we design before we write
- Requirements in plain language
- The API concept: resources, roles and authentication
- From resources to paths
- The structure of an OpenAPI file
- Schemas and $ref: write the shape once
- Mock responses: try the design before any code exists
- How to talk to the assistant in this project specifically
Two ways into the same idea
Before a contractor builds a house, they agree with the owner on a written plan: how many rooms, where the doors go. An API needs the same: a written contract saying which requests are allowed and what comes back. In this project you write that contract before any code, with AI helping.
design-first APIs: plain-language requirements become resources, endpoints, data schemas and error codes in an OpenAPI file in YAML. You validate it with a linter, and it becomes the shared reference for the front end, back end and tests.
Required fundamentals
Directed conversation (Part 3)
This whole project is a conversation. Whoever knows how to give context and ask for a specific output shape walks away with a ready table; whoever asks a vague question walks away with vague prose.
Verify before accepting (Part 7)
The assistant invents fields and paths that look plausible. The validator is the judge, not your trust.
HTTP basics
The methods GET, POST, PUT and DELETE, plus status codes. You need nothing more here.
Reading YAML
Indentation sets the nesting, not brackets. You do not need to memorise the syntax, but you do need to recognise the mistake when the validator points at it.
Core concepts
This project differs from everything before it on the track: there is no code in it at all. Its outputs are two artifacts only: an endpoints table and an openapi.yaml file. And the skill being taught is not writing YAML, but agreeing with other people on what the API will do before anyone writes a line.
1Why we design before we write
An API is like a restaurant menu: a published list of what another program may ask yours for, and in what shape the request arrives and the answer comes back. Writing that menu before building saves a long argument later, for three concrete reasons:
A contract between two teams
The front-end team starts work against the spec while the back end is still being built, instead of waiting weeks.
Changing paper, not code
Editing a line in the spec takes a minute; changing a path after three apps have built against it takes a week.
A machine-readable file
From the same file you generate interactive documentation, ready-made clients, and tests that check the responses.
2Requirements in plain language
Before any path or field, four questions answered in ordinary sentences:
| The question | An example from the campus library |
|---|---|
| The business goal | Let students reserve books before coming to the library. |
| Who will call it | A mobile app for students, and a dashboard for the librarian. |
| The use cases | "As a user I want to see available books", "to reserve a book", "to cancel my reservation". |
| What it is not | No payments, no late fees, no delivery. Naming what it does not do prevents the design from bloating. |
3The API concept: resources, roles and authentication
A resource is the "kind of thing" the API lets you read or change, and it is always a noun. In our example: a book, a reservation, a member. The practical rule: three to six resources is plenty.
| The decision | The options | When to pick each |
|---|---|---|
| Authentication | none | A public read-only API, or a learning exercise |
an API key | A secret string identifying the caller; simple and enough for an internal service | |
| login then a token | When you need to know which user, not which application | |
| Roles | guest / member / librarian | When permissions differ, which shows up in the "auth required?" column |
4From resources to paths
The resource name becomes a path, and the verb lives in the method, not in the path. The four CRUD operations distribute like this:
| Operation | Method | Path | Note |
|---|---|---|---|
| Create | POST | /books | returns 201 with a Location header |
| Read all | GET | /books | filters go in the query string: ?available=true |
| Read one | GET | /books/{id} | 404 if it does not exist |
| Full replace | PUT | /books/{id} | sends the whole object |
| Partial update | PATCH | /books/{id} | sends only the changed fields |
| Delete | DELETE | /books/{id} | 204 with no body |
Plan at least eight endpoints covering all four operations on at least two resources. So plan for that from the start instead of discovering it at the last step.
/getBooks, /createBook and /books/delete/3 are all wrong: the verb appears twice. The correct forms are GET /books, POST /books and DELETE /books/3. And plural for collections: /books, not /book.5The structure of an OpenAPI file
The file is a handful of sections, and fewer of them are required than you would expect:
| Section | What it describes | Required? |
|---|---|---|
openapi | The specification version, such as 3.0.3 | Yes |
info | Your API's name and version, note that this is your API's version, not the spec's | Yes (with title and version) |
servers | Server addresses | No |
paths | Every path with its methods, parameters, bodies and responses | In practice yes |
components/schemas | Reusable data shapes | No, but they stop repetition |
components/securitySchemes | Authentication methods and which operations need them | No |
6Schemas and $ref: write the shape once
A schema is the shape of a piece of data: its fields and their types. Instead of repeating the definition of "book" in six places, you define it once under components/schemas and point at it with $ref: '#/components/schemas/Book'. One change propagates through the whole file.
And a useful trick: separate what the client sends from what the server returns. The client does not send an id, because the server generates it:
NewBook: # what the client sends
type: object
required: [title, author]
properties:
title: { type: string }
author: { type: string }
Book: # what the server returns
allOf:
- $ref: '#/components/schemas/NewBook'
- type: object
properties:
id: { type: integer }
available: { type: boolean }Plan at least four schemas, among them an Error schema, the one most often forgotten.
7Mock responses: try the design before any code exists
A mock response is a hand-written example of what the API will return. Its value is that you see the shape of the reply with your own eyes and ask "is this really what a user expects?" before anything is built. You add it with an example field under the response:
example:
- id: 1
title: "The Pragmatic Programmer"
author: "Hunt & Thomas"
available: true8How to talk to the assistant in this project specifically
The difference between walking away with a good design and walking away with tidy prose is how you ask:
| Instead of saying | Say |
|---|---|
| "Design me an API" | "This is my domain, these are its users, these are its use cases. Propose 3 to 6 resources" |
| "Write me the paths" | "Give me a Markdown table with columns: Method, Path, Purpose, Auth Required" |
| "Is this correct?" | "Where does this design fail if the user count doubles? And what is the alternative?" |
| "Fix the error" | "Here is the validator's error text verbatim: … which line causes it and why?" |
How it works
Three pictures: the project's journey from idea to file, how one resource becomes five endpoints, and the conversation loop that separates a good design from tidy prose.
Diagram 7, The project's full journey: from a one-sentence idea to a verified contract. Note the one loop in the picture, between the validator and the fix, which is what guarantees the output is valid, not merely pretty.
Diagram 8, One resource and five operations. The path stays a noun on every line; the only thing that changes is the method.
Diagram 9, The conversation loop: context first, then understanding, then verification with a tool. Jumping from the answer straight to adoption is what produces a design you cannot defend.
Worked example, step by step
We will design the campus library API in full, then run it through a real OpenAPI validator. Everything below was actually executed, including the error messages.
Step 1, the path from idea to file
Six steps, and the last one is what settles it
Requirements. "An API that lets campus students reserve books before coming in." Its users: a mobile app for students and a dashboard for the librarian. What it is not: no payments, no fines.
Concept. Three resources: Book, Reservation and Member. Two roles: member and librarian. Authentication: a Bearer token, reading is open, every change needs a token.
The endpoints table. Eight endpoints covering all four operations on two resources, which is a sensible minimum.
The file. We turn the table into openapi.yaml with schemas wired by $ref and at least one example.
Validation. We run it through a real validator. The file is either valid or it is not, and nobody's opinion enters into it, neither yours nor the assistant's.
Breaking it on purpose. We introduce five common mistakes one at a time and read what the validator says, so you recognise the message when you actually meet it.
Step 2, the endpoints table (the project's first artifact)
| Method | Path | Purpose | Auth |
|--------|---------------------------------|-----------------------|------|
| GET | /books | List books | no |
| POST | /books | Add a book | yes |
| GET | /books/{bookId} | Get one book | no |
| PUT | /books/{bookId} | Replace a book | yes |
| DELETE | /books/{bookId} | Delete a book | yes |
| GET | /reservations | List reservations | yes |
| POST | /reservations | Reserve a book | yes |
| DELETE | /reservations/{reservationId} | Cancel a reservation | yes |Read the last column: reading is open and writing is restricted. That decision alone is the "permission model" any reviewer will ask you about.
Step 3, the OpenAPI file (the second artifact)
Abridged here, with the repeated paths folded behind ... to save space:
openapi: 3.0.3
info:
title: Campus Library API
version: "1.0.0"
description: Reserve books in a small campus library.
servers:
- url: https://api.campus-library.example/v1
paths:
/books:
get:
summary: List books
parameters:
- name: available
in: query
required: false
schema:
type: boolean
responses:
"200":
description: A list of books
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Book'
example:
- id: 1
title: "The Pragmatic Programmer"
author: "Hunt & Thomas"
available: true
post:
summary: Add a book
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/NewBook'
responses:
"201":
description: Created
headers:
Location:
schema:
type: string
content:
application/json:
schema:
$ref: '#/components/schemas/Book'
"400":
description: Invalid input
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
"401":
description: Missing or invalid token
/books/{bookId}:
parameters:
- name: bookId
in: path
required: true
schema:
type: integer
get:
summary: Get one book
responses:
"200":
description: The book
content:
application/json:
schema:
$ref: '#/components/schemas/Book'
"404":
description: Not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
put: { summary: Replace a book, ... }
delete: { summary: Delete a book, ... }
/reservations:
get: { summary: List reservations, ... }
post: { summary: Reserve a book, ... }
/reservations/{reservationId}:
delete: { summary: Cancel a reservation, ... }
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
schemas:
NewBook:
type: object
required: [title, author]
properties:
title: { type: string }
author: { type: string }
Book:
allOf:
- $ref: '#/components/schemas/NewBook'
- type: object
properties:
id: { type: integer }
available: { type: boolean }
Error:
type: object
required: [error]
properties:
error: { type: string }Step 4, validating with a real validator
$ python3 check.py
VALID OpenAPI 3.0.3 spec ✓
endpoints: 8
GET /books
POST /books
GET /books/{bookId}
PUT /books/{bookId}
DELETE /books/{bookId}
GET /reservations
POST /reservations
DELETE /reservations/{reservationId}
schemas: ['NewBook', 'Book', 'Member', 'NewReservation', 'Reservation', 'Error']
security schemes: ['bearerAuth']Eight endpoints and six schemas: we cleared the minimum on both. And notice the validator did not say "nice design", it said "valid". Validity is a necessary condition, not a sufficient one, and quality remains your judgement.
Step 5, five mistakes and what the validator actually says
1 info.version removed
'version' is a required property
2 $ref pointing at a schema that does not exist ($ref: '#/components/schemas/Buk')
'/components/schemas/Buk' does not exist within {'openapi': '3.0.3', ...}
3 an operation with no responses
'responses' is a required property
4 a bad parameter location (in: pathh)
{'name': 'bookId', 'in': 'pathh', ...} is not valid under any of the given schemas
5 required written as a string instead of a list (required: title)
{'type': 'object', 'required': 'title', ...} is not valid under any of the given schemasBuk alone tells you the problem is a schema name, not the file's structure.Myth vs. reality
| The myth | The reality |
|---|---|
| “The project has no code, so it is easier.” | Harder, in fact: no compiler tells you the design is wrong. The validator checks syntax only; "does this path make sense?" is answered by nobody but you. |
| “The assistant writes the spec and I hand it in.” | Discuss and edit "out loud in the chat". A spec whose every line you cannot defend will not survive the first question. |
| “If the validator accepted it, the design is good.” | Validity and quality are two things. POST /createBook passes the validator and is an obvious design mistake. |
“The version in info.version is the OpenAPI version.” | No. info.version is your API's version; the specification's version lives in the openapi field at the top of the file. |
“$ref is just for saving space.” | It is not shorthand but a single source of truth: change the shape once and it propagates everywhere, so the "this field exists here and is missing there" situation disappears. |
| “Mock responses are a cosmetic step.” | They are the cheapest test of the design: you see the shape of the reply before it is built, so you catch the missing field on paper rather than three weeks later. |
Try it yourself
The assistant proposed these four paths. Which is the only correct one under REST conventions?
The rule: the path is a noun and the verb lives in the method. The first and second repeat the verb inside the path. The fourth is worse: it uses GET for an operation that deletes data, and GET must be safe and change nothing, otherwise a browser or a proxy could delete your data while "caching the page". The third puts the noun in the path and the verb in the method.
Quick review flashcards
openapi.yaml file. No code and no separate notes./books, not /book and not /getBooks.info?title and version, and the latter is your API's version, not the specification's.$ref for?NewBook from Book?id and the server generates it; what is sent and what is returned are different shapes.Check your understanding
1. The assistant proposed a single resource called Data holding everything. What do you tell it?
That a resource must represent one "kind of thing" the user understands: a book, a reservation, a member. A resource called Data means every path needs parameters explaining what it means, which throws away the whole value of addresses. Ask it to break the resource into three to six resources named in the language of the domain itself.
2. What is the practical difference between PUT and PATCH, and when does it matter?
PUT replaces the whole object: anything you do not send is removed or reset to its default. PATCH changes only the fields you send. It matters when two users edit two different fields at nearly the same time: with PUT the second silently wipes the first's change, and with PATCH it does not.
3. (Spot the bug) Your file is valid, but every response returns 200 with a success field. What is wrong?
The problem is that the status code lies. The code is what tools and intermediaries read first, so hiding failure in the body breaks retries, caching and monitoring. Use 201 for creation, 204 for deletion, 400 for bad input and 404 for a missing resource, and let the body explain why, not what.
4. Why does the project ask for an Error schema specifically?
Because the shape of an error is part of the contract exactly as much as the shape of a success. Without it every developer guesses the error format, so handling differs between one app and the next. One shared schema makes every error read the same way.
5. (Decision) The assistant handed you a complete spec in its first reply. Do you adopt it?
Do not adopt it as-is, and do not reject it for coming from an assistant. Run it through the validator first, then read every path and ask: does it serve a use case you named? Is the auth column sensible? And delete what you do not need, an ambitious spec nobody builds is worse than a small complete one. Your value in this project is the decisions you can defend, not the line count.
Resources for this part
Sorted by kind: start with the official docs when you want the precise reference, or with an article or video when you want the explanation first.