The Hard Part of an API Isn't Calling It
When I started working with APIs, it was easy to think of them as simple plumbing:
Request → API → Response
Call an endpoint, parse JSON, display the result.
That mental model works for a while.
Then you build software that depends on real APIs, and you realize the difficult part isn't making the HTTP request.
The difficult part is deciding what your application should do when the API doesn't behave exactly as expected.
A successful HTTP request doesn't mean successful data
Consider:
GET /transactions
You might receive:
{
"transactions": []
}
The request succeeded.
But what does an empty result mean?
No transactions exist?
Wrong account?
Wrong date range?
Temporary backend issue?
Incorrect query parameters?
The HTTP request alone doesn't tell you the business meaning.
Your application needs to interpret the response.
Treat external APIs as boundaries
A useful mental model is:
Your application
↓
API boundary
↓
External system
Everything outside your application is something you don't fully control.
The API might:
- Return an unexpected status code
- Change a field
- Return missing data
- Take too long
- Fail temporarily
- Return malformed data
- Reject a request
- Return a valid response with unexpected business meaning
So the API layer should be treated as a boundary where external data becomes internal application data.
Don't spread API models everywhere
Suppose an API returns:
{
"user_id": 42,
"display_name": "Devanshu",
"created_at": "2026-09-22T00:00:00Z"
}
You don't necessarily want every part of your application to depend directly on that structure.
A useful approach is to map external data into your own model:
API Response
↓
DTO
↓
Mapper
↓
Domain Model
↓
Application
For example:
data class UserDto(
val user_id: Long,
val display_name: String,
val created_at: String
)
could become:
data class User(
val id: Long,
val name: String,
val createdAt: Instant
)
Now the rest of the application doesn't need to know how the external API names its fields.
Error handling should have meaning
This is another place where simplistic API handling causes problems.
You don't want every failure to become:
Something went wrong.
Different failures can require different behavior.
For example:
401 → Authentication problem
403 → Permission problem
404 → Resource doesn't exist
429 → Rate limit
500 → Server-side failure
Timeout → Network problem
The application can then decide what makes sense for each case.
Maybe a timeout should be retried.
Maybe a 401 should require authentication again.
Maybe a 404 should show an empty state.
Maybe a 500 should be surfaced as a temporary error.
The important part is that the API layer shouldn't throw away useful information.
Retries are not always harmless
It's tempting to automatically retry every failed request.
But imagine an endpoint that creates a transaction:
POST /transactions
The request reaches the server.
The server creates the transaction.
Then the network connection fails before the client receives the response.
The client sees:
Request failed
Should it send the request again?
Maybe.
But if the operation isn't idempotent, you might create the transaction twice.
This is why retries need to be designed around the operation.
A read request and a money-moving operation shouldn't necessarily have the same retry strategy.
Timeouts are part of API design
A request without a sensible timeout can leave the application waiting unnecessarily.
Conceptually:
Request
↓
Waiting...
↓
Waiting...
↓
Waiting...
The user doesn't care that the TCP connection technically still exists.
They care that the application appears stuck.
A timeout gives the system a defined failure state:
Request
↓
Timeout
↓
Handle failure
↓
Retry / cached data / error UI
Failure is easier to manage when it has boundaries.
The API should not define your entire architecture
This is probably the biggest lesson.
If your application structure mirrors an external API too closely:
API
↓
Everything else
then an API change can ripple through the entire codebase.
Instead, I prefer thinking in terms of:
External API
↓
Integration layer
↓
Application model
↓
Business logic
↓
UI
The external API is an input to the system.
It shouldn't automatically become the system's architecture.
What I'm learning
Working with APIs has changed the way I think about network code.
The important question isn't:
"How do I call this endpoint?"
It's:
"How does my application safely depend on a system I don't control?"
That question leads to better decisions around:
- DTOs
- Mapping
- Validation
- Error handling
- Timeouts
- Retries
- Caching
- Idempotency
- Logging
The HTTP request is usually the easy part.
The engineering starts after you realize the response isn't guaranteed to be exactly what you wanted.
Top comments (0)