Handbook
/
Product & Engineering
API-First Thinking: Why It Matters Early
Designing APIs before building UIs leads to cleaner architecture, faster iteration, and options you'll be glad you have later.
Many startups build UI-first: design the interface, then build the backend to support it. This works initially but creates problems as you scale. API-first thinking—designing the data model and API before the UI—produces cleaner systems and keeps your options open.
What API-First Means
API-first development means:
1.
Design your data model first. What entities exist? How do they relate?
2.
Design your API second. What operations can be performed? What’s the interface?
3.
Build the API third. Implement the designed interface.
4.
Build clients last. Web, mobile, integrations—all consume the same API.
This sequence forces you to think about the system abstractly before getting lost in UI details.
Why It Matters
Cleaner Architecture
When UI drives architecture, you end up with endpoints that do whatever the current screen needs. Over time, this creates a mess of inconsistent, one-off endpoints.
API-first produces consistent, well-designed interfaces because you think about them in the abstract, not in service of a specific screen.
Multiple Clients
Eventually, you’ll want multiple ways to access your product:
Web app
Mobile app
Third-party integrations
Internal tools
Command-line interfaces
Automations
If you build API-first, all these clients use the same API. If you build UI-first, you’ll need to retrofit or maintain multiple APIs.
Third-Party Integrations
Customers will ask for API access. Partners will want integrations. Developers will want to build on your platform.
If your API is an afterthought, these requests are painful. If you’re API-first, they’re natural extensions.
Parallel Development
With a defined API, frontend and backend teams can work in parallel. Frontend builds against the API spec while backend implements it. No waiting, no blocking.
Testing
APIs are easier to test than UIs. Automated tests can verify behavior without dealing with DOM, rendering, or visual elements. API-first leads to more testable systems.
Flexibility
Requirements change. With API-first, you can rebuild the UI entirely without touching the backend. You can add new clients without changing existing ones. The API is a stable foundation.
Designing Your API
Start with Resources
Think in terms of resources (nouns), not actions (verbs):
Good:
/users
/projects
/tasks
Avoid:
/getUser
/createProject
/doTask
Resources map to your data model. Actions are expressed through HTTP methods (GET, POST, PUT, DELETE).
Define Clear Operations
For each resource, what operations are possible?
GET /tasks – List tasks
POST /tasks – Create a task
GET /tasks/:id – Get a single task
PUT /tasks/:id – Update a task
DELETE /tasks/:id – Delete a task
Design Relationships
How do resources relate?
Nested resources:
GET /projects/:id/tasks – Tasks in a project
Query parameters:
GET /tasks?project_id=123 – Filter tasks by project
Both work. Be consistent.
Plan for Evolution
APIs need to evolve without breaking clients:
Use versioning (/v1/tasks)
Add fields without removing them
Deprecate before removing
Document breaking changes
Write the Spec
Document your API before building:
OpenAPI/Swagger spec
Simple markdown documentation
Type definitions (TypeScript interfaces)
The format matters less than having a written contract.
Practical Implementation
Start Simple
You don’t need a perfect API from day one. Start with basics:
// types.ts - Your data model interface User { id: string; email: string; name: string; createdAt: string; } interface Project { id: string; name: string; ownerId: string; createdAt: string; } interface Task { id: string; projectId: string; title: string; completed: boolean; createdAt: string; }
// api.ts - Your operations interface API { // Users getUser(id: string): Promise<User>; // Projects listProjects(): Promise<Project[]>; createProject(data: CreateProject): Promise<Project>; getProject(id: string): Promise<Project>; // Tasks listTasks(projectId: string): Promise<Task[]>; createTask(data: CreateTask): Promise<Task>; updateTask(id: string, data: UpdateTask): Promise<Task>; deleteTask(id: string): Promise<void>; }
This spec can guide both backend implementation and frontend consumption.
Use Type Safety
TypeScript (or similar) ensures your API contract is enforced:
Backend generates types from your API spec
Frontend imports those same types
Changes to the API surface break compilation, not production
Consider GraphQL
For complex, evolving frontends, GraphQL offers advantages:
Clients request exactly what they need
Single endpoint, flexible queries
Strong typing built in
Good tooling
But GraphQL adds complexity. For simple APIs, REST is often better.
Build Internal First, External Later
Your internal API will evolve fast. Don’t promise external stability yet.
When you’re ready for a public API:
1.
Create a stable subset of your internal API
2.
Version it explicitly
3.
Document it thoroughly
4.
Commit to backwards compatibility
Common Patterns
Pagination
Large lists need pagination:
GET /tasks?limit=20&offset=40 GET /tasks?cursor=abc123
Cursor-based pagination is more reliable for changing data.
Filtering and Sorting
GET /tasks?status=completed&sort=-createdAt
Design consistent patterns across endpoints.
Error Responses
Standardize error format:
{ "error": { "code": "VALIDATION_ERROR", "message": "Title is required", "field": "title" } }
Consistent errors help clients handle them gracefully.
Webhooks
For real-time needs, complement request/response with webhooks:
Client registers a URL
Your system POSTs events to that URL
Client processes events asynchronously
Design webhook payloads consistently with your API resources.
Anti-Patterns
Screen-Specific Endpoints
GET /homepage-data that returns exactly what the homepage needs. This couples API to a specific UI. Instead, let clients compose from general endpoints.
Action Endpoints
POST /tasks/123/complete vs PATCH /tasks/123 { completed: true }. The latter is more flexible and consistent.
Inconsistent Conventions
Some endpoints use camelCase, others use snake_case. Some return data, others return the resource directly. Pick conventions and stick to them.
Forgetting Mobile
Mobile clients have different constraints (battery, network). Design APIs that:
Minimize requests
Support offline caching
Handle poor connectivity gracefully
Key Takeaways
Design data model and API before building UI
API-first enables multiple clients, integrations, and parallel development
Think in resources and operations, not screens and buttons
Write a spec (even a simple one) before implementing
Use type safety to enforce the contract
Plan for evolution with versioning and backward compatibility
Start internal, then expose public API when stable
AIMake has access to all of this
Our AI has access to the entire Startup Handbook. Ask it anything about building your startup.
Get started
Next
When to Build vs Buy vs Integrate