Преиспитајте ги вашите API-а: Направете ги да функционираат како човек би функционирал
Most API problems are not caused by HTTP, JSON, or an unfortunate choice of framework. They happen when an interface reflects the database, the service layer, or a team’s internal vocabulary instead of the person trying to get work done.
A human does not think, “I need to mutate the order_status column.” They think, “I need to cancel this order,” “I need to send this invoice,” or “I need to see whether payment cleared.” Good APIs preserve that intent. They make the common path obvious, make failure understandable, and avoid forcing every consumer to learn the machinery behind the product.
Model actions, not tables
CRUD is a useful implementation pattern, but it is not always a useful public contract. A resource-shaped API works beautifully when the user is managing a stable thing: a customer profile, a product catalog entry, or a saved address.
But business workflows are often verbs with rules. Consider an order that may be paid, shipped, cancelled, or refunded. Exposing a general update endpoint invites consumers to attempt invalid state transitions:
PATCH /orders/ord_123
{
"status": "shipped"
}
Who verifies stock allocation? Who records the shipment timestamp? What happens if the order has already been cancelled? The endpoint has made internal state writable without explaining the business operation.
An action-oriented boundary can be clearer:
POST /orders/ord_123/cancel
{
"reason": "customer_request"
}
This does not mean every endpoint should become a verb. It means the interface should express meaningful operations where rules matter. The API can validate whether cancellation is allowed, calculate a refund when appropriate, emit the right domain event, and return a result the client can act on.
Make the happy path easy to discover
A human-friendly API reduces guessing. Names should describe the domain, request fields should be unsurprising, and responses should contain enough context to support the next likely decision.
For example, a payment confirmation response is more useful when it tells the caller what changed and what remains:
{
"id": "ord_123",
"status": "paid",
"payment_status": "confirmed",
"next_actions": [
{
"rel": "shipment",
"available_when": "fulfilled"
}
]
}
The exact response shape will vary, but the principle is stable: do not make clients reconstruct the state of the world from scattered fields or follow-up requests they should not need.
Consistency matters more than cleverness. If a customer is called customer in one endpoint, calling it buyer elsewhere creates needless cognitive work. If dates are returned in one standard format, return all timestamps that way. If identifiers are strings, do not quietly switch to numbers for a related resource because the database uses a different key type.
Errors are part of the product
Every API will reject requests. The difference between a frustrating API and a trustworthy one is how it behaves at that moment. A useful error tells the caller what failed, where it failed when possible, and whether retrying could help.
{
"error": {
"code": "order_not_cancellable",
"message": "This order cannot be cancelled because it has already shipped.",
"details": {
"order_id": "ord_123",
"status": "shipped"
}
}
}
A stable machine-readable code lets applications branch safely. A clear message helps the developer during integration and may be suitable for a user interface after appropriate product review. Avoid leaking stack traces, database errors, or internal service names. Those details rarely help a consumer and can expose implementation information you may later need to change.
It is equally important to distinguish a request that should be corrected from one that might succeed later. Invalid input and forbidden actions are not retry candidates. A temporary dependency failure may be. When an operation can be retried, design it to be safe.
Plan for retries from day one
Networks fail after a server has completed work but before the client receives the response. Without protection, a retry can create duplicate payments, duplicate emails, or duplicate orders. For operations that create something consequential, accept an idempotency key and persist the outcome associated with it.
POST /orders
Idempotency-Key: 8f7e3c1b-unique-client-value
Content-Type: application/json
If the same client repeats the same request with the same key, return the original outcome rather than performing the operation again. This is not merely an API feature; it requires careful backend design. The key, request identity, and resulting response need durable handling, usually within the same transactional boundary as the business change or with an intentional recovery strategy.
Keep persistence behind the boundary
PHP applications often begin with an ORM model doing double duty as a database record and an API response. It is convenient until it is not. Columns become fields by accident. Renaming a relationship becomes a breaking change. A performance fix in the query layer unexpectedly changes client-visible behavior.
Use dedicated request validation and response transformation layers, even if they are thin. They give the API a stable vocabulary while allowing the schema, ORM, cache strategy, and service decomposition to evolve. A database is an implementation detail; an API is a promise.
This separation also helps performance work remain honest. Avoid returning deeply nested relationships by default simply because the ORM can load them. Decide what each endpoint needs, select only the required data, and offer explicit expansion or dedicated endpoints where additional detail is genuinely useful. Measure before adding caches, and treat cache invalidation as a consistency decision rather than a reflex.
Design for operations, not just requests
An endpoint is only successful if it behaves predictably in production. Define timeouts between services. Make background work observable. Use asynchronous processing for work that does not need to finish before the response, but return a clear representation of the accepted work rather than implying it is complete.
For example, an export request may return an identifier and a pending state. The client can then retrieve its status or receive a product-specific notification mechanism. What matters is that the contract clearly separates “we accepted this” from “the file is ready.”
- Set explicit limits for pagination, payload size, and expensive filters.
- Log request identifiers and propagate them through downstream work.
- Version deliberately when a change alters meaning, removes a field, or changes a workflow.
- Document authorization behavior as part of each operation, not as an afterthought.
The interface is a conversation
The best API design question is not, “What routes match our entities?” It is, “What is this caller trying to accomplish, and what do they need to know next?” That question leads to clearer actions, safer retries, better errors, and boundaries that survive refactoring.
Make your API behave like a capable colleague: direct, consistent, explicit about constraints, and dependable when things go wrong. Developers will still need documentation, but they will spend far less time reading it just to discover how to complete a simple task.