APIs done right
An API is a promise between the app team and the server team. A good one is easy to use correctly and hard to use wrongly: clear resources, honest status codes, one error format, pagination that does not lose rows, and POST requests that are safe to retry.
By the end you can
- Design REST endpoints with the right methods and status codes
- Explain why offset pagination loses or repeats rows, and build cursor pagination with Prisma
- Stop duplicate records from double taps and network retries with an idempotency key
- Validate input with DTOs and publish an OpenAPI contract from NestJS
- Say when GraphQL, gRPC or webhooks fit better than REST
The everyday picture
At a busy restaurant, nobody walks into the kitchen to explain what they want. There is a menu. Each dish has a number, a name and a price. You tell the waiter "number 23, two plates", he writes it on a ticket, and the kitchen cooks it. The menu is the contract. The kitchen can change its cooks, its stoves and its suppliers, and as long as number 23 still arrives as promised, you do not care.
A good waiter also handles problems in a fixed way. If you order a dish that is not on the menu, he tells you at once, before the kitchen starts. If you wave at him twice because he did not seem to hear, he does not bring the dish twice. He remembers your table and your order. An API should behave the same: a clear menu, clear "no" answers, and no double orders.
Resources and methods
In a REST API, the URL names a resource, a thing, and the HTTP method says what to do with it. Use nouns in paths, not verbs: POST /grns, not POST /createGrn. The running example is Mangrove's Store app, which records every delivery a store receives as a goods received note (GRN).
| Request | Meaning | Safe to repeat? |
|---|---|---|
GET /v1/grns?siteId=12 | List GRNs of store 12 | Yes, it only reads |
GET /v1/grns/501 | Read GRN 501 | Yes |
POST /v1/grns | Create a new GRN | No, each call creates one more |
PUT /v1/sites/12/prices/4711 | Set the price to this full value | Yes, same result each time |
PATCH /v1/grns/501 | Change some fields | Depends: "set note" yes, "add 5 to qty" no |
DELETE /v1/grns/501 | Remove or cancel GRN 501 | Yes, gone stays gone |
POST /v1/grns/501/approve | An action that changes state | Only if you make it so |
The last column is about being idempotent: doing it twice has the same effect as doing it once. This matters because networks fail and clients retry. Retrying a GET or a PUT is harmless. Retrying a POST can create a second GRN, a second order or a second payment. We will fix that below.
Some actions do not map neatly to create, read, update or delete. Approving a GRN is a state change with its own rules. A sub-resource like POST /grns/501/approve is a common and readable answer. Do not bend it into a PATCH that sets status: "APPROVED" if approval also checks permissions and writes stock.
Quick check
A scanner's request times out and the app cannot tell whether it worked. Which request can it retry with no risk of doing the work twice?
PUT sets a full value, so doing it twice leaves the same price. The POST would create a second GRN, and "add 5" would add 10. Both need extra protection before a retry is safe.