Backend APIs cheat sheet
HTTP APIs on a few screens: methods and which are safe to retry, the status codes worth knowing, headers, resource design, pagination, error bodies, auth, rate limits, idempotency keys, webhooks, versioning, NestJS and Prisma snippets, and how to test and document it all. Keep it open while you design or review an endpoint.
HTTP methods
Safe means the request does not change anything on the server. Idempotent means sending it twice has the same effect as sending it once, so a client or proxy may retry it after a timeout (APIs, API design).
| Method | Use it to | Safe / idempotent | Lesson |
|---|---|---|---|
GET | Read a resource or a list; never change state | Safe, idempotent, cacheable | API design |
HEAD | Same as GET without the body (check existence or size) | Safe, idempotent | APIs |
POST | Create a resource, or run an action such as /orders/123/cancel | Neither; add an idempotency key to make retries safe | API design |
PUT | Replace a resource with the full body sent | Idempotent | API design |
PATCH | Change some fields of a resource | Not guaranteed idempotent (for example "add 1 to quantity") | API design |
DELETE | Remove a resource | Idempotent; a second call may return 404, but the state is the same | API design |
OPTIONS | Ask what is allowed; browsers send it as the CORS preflight | Safe, idempotent | Auth and sessions |
Status codes worth knowing
The first digit tells the client what to do: 2xx done, 3xx look elsewhere, 4xx the client must change the request, 5xx the server failed and a retry may help. Never return 200 with an error inside the body (APIs).
| Code | Meaning | Return it when | Lesson |
|---|---|---|---|
| 200 OK | Success with a body | GET, or an update that returns the resource | APIs |
| 201 Created | A new resource exists | POST created it; add a Location header with its URL | API design |
| 202 Accepted | Taken, not finished | Work went to a queue; return a job id or status URL | Jobs and retries |
| 204 No Content | Success, no body | DELETE, or an update with nothing to return | API design |
| 301 / 308 | Moved permanently | A URL changed for good; 308 keeps the method and body | APIs |
| 302 / 307 | Moved temporarily | Short-term redirect; 307 keeps the method and body | APIs |
| 304 Not Modified | Your cached copy is still good | The client sent If-None-Match and the ETag matches | Caching |
| 400 Bad Request | Malformed request | Broken JSON, wrong types, missing required fields | API design |
| 401 Unauthorized | Not authenticated | No token, expired token, bad signature | Auth and limits |
| 403 Forbidden | Authenticated, not allowed | A store manager for S-014 asks for S-102's sales | Access control |
| 404 Not Found | No such resource (for this caller) | Also when hiding that something exists from a caller who may not see it | Access control |
| 405 Method Not Allowed | Wrong method for this URL | DELETE on a read-only collection; send an Allow header | APIs |
| 409 Conflict | Clashes with current state | Duplicate unique value, or the same idempotency key with a different body | Prisma data |
| 412 Precondition Failed | The version you based your change on is old | If-Match ETag no longer matches (optimistic locking) | Transactions |
| 415 Unsupported Media Type | Wrong body format | Client sent XML or form data where JSON is required | APIs |
| 422 Unprocessable Content | Well-formed but breaks a business rule | Quantity of 0, delivery slot in the past. Some teams use 400 for all validation; pick one and document it | API design |
| 429 Too Many Requests | Rate limit hit | Add Retry-After with seconds to wait | Rate limiting |
| 500 Internal Server Error | Unexpected failure | A bug; log it with a request id, show no stack trace | Tests and docs |
| 502 Bad Gateway | A proxy got a bad answer upstream | The load balancer could not get a valid response from your app | Load balancers |
| 503 Service Unavailable | Temporarily overloaded or down | Maintenance or load shedding; add Retry-After | Timeouts and retries |
| 504 Gateway Timeout | Upstream took too long | The proxy gave up waiting; check slow queries and timeouts | Timeouts and retries |
Headers you will set or read
Header names are case-insensitive. Custom headers no longer need an X- prefix (APIs).
| Header | What it does | Lesson |
|---|---|---|
Content-Type: application/json | Says what format the body is in | APIs |
Accept | Says which formats the client can read | APIs |
Authorization: Bearer <token> | Carries an access token | Auth and limits |
Idempotency-Key | Lets the server spot a retried POST (common convention, not yet a final standard) | Jobs and retries |
ETag, If-None-Match, If-Match | Version of a resource for caching (304) and safe updates (412) | Caching |
Cache-Control | no-store for private data, public, max-age=60 for shared | Caching |
Location | URL of the created resource (201) or redirect target (3xx) | API design |
Retry-After | Seconds (or a date) to wait before trying again, with 429 or 503 | Rate limiting |
X-Request-Id or traceparent | Ties logs and traces of one request together across services | Observability |
Access-Control-Allow-Origin | CORS: which web origins may call the API from a browser; never * with credentials | Auth and sessions |
Resource design rules
- Nouns in the path, plural collections:
GET /stores/S-014/orders,GET /orders/ord_81f2. The method is the verb. - Actions that are not simple CRUD get a sub-resource:
POST /orders/ord_81f2/cancel. - Nest only one level, for ownership. Filter with query parameters:
GET /orders?storeId=S-014&status=packed. - Money in integer cents with a currency:
{ "totalCents": 1299, "currency": "USD" }. Never floats. - Times in UTC ISO 8601:
"2026-03-01T09:30:00Z". Dates without time as"2026-03-01". - Ids are opaque strings to clients. Do not expose auto-increment ids if guessing them leaks volume or data.
- Pick one casing for JSON fields (
camelCaseis common in Node) and keep it everywhere. - Return the created or updated resource so the client does not need a second call.
See API design and Data and API.
Pagination: offset or cursor
Always paginate lists and cap the page size (for example 100). The Partner app's order history once returned every order a shop ever placed in one response (API design, Everyday SQL).
Offset (?page=3&limit=50) | Cursor (?after=eyJpZCI6...&limit=50) | Lesson | |
|---|---|---|---|
| SQL | LIMIT 50 OFFSET 100 | WHERE (created_at, id) < ($1, $2) ORDER BY created_at DESC, id DESC LIMIT 50 | Indexes |
| Deep pages | Slow: the database still reads and skips every earlier row | Fast with an index on the sort columns | Query plans |
| Rows added while paging | Items repeat or are skipped | Stable | API design |
| Jump to page 40 | Yes | No; only next and previous | API design |
| Use for | Small admin tables in the Back office | Feeds, sync, infinite scroll, exports | API design |
One error format everywhere
Clients should handle every error the same way. RFC 9457 "Problem Details" (which replaced RFC 7807) is a ready-made shape with the media type application/problem+json. Keep a stable machine-readable code, a human message, and a request id the support team can search for (API design, Tests and docs).
{
"type": "https://api.mangrove.example/errors/slot-full",
"title": "Delivery slot is full",
"status": 409,
"detail": "The 18:00-19:00 slot at store S-014 has no capacity left.",
"code": "SLOT_FULL",
"requestId": "req_7Hq2c",
"errors": [{ "field": "slotId", "message": "Choose another slot" }]
}Auth: which one?
Authentication proves who is calling; authorization decides what they may do. Check authorization on every request, on the server, for the specific record (Auth and limits, Access control, Auth and sessions).
| Caller | Use | Key points | Lesson |
|---|---|---|---|
| Your own web app (Back office) | Server session in an HttpOnly, Secure, SameSite cookie | Easy to revoke; protect state-changing requests against CSRF | Auth and sessions |
| Mobile app (Mangrove app) | OAuth 2.0 authorization code flow with PKCE; short access token plus refresh token | Tokens in the device's secure storage; rotate refresh tokens | Auth and limits |
| Single-page app on another domain | Authorization code with PKCE, or a backend-for-frontend that keeps tokens server-side | The implicit flow is deprecated; avoid tokens in localStorage | Auth and sessions |
| Service to service | OAuth 2.0 client credentials, or mutual TLS | One identity per service, least privilege | Auth and limits |
| Partner or integration (Lagoon Digital's Partner app backend) | API key per client, sent in a header | Store only a hash; allow rotation with two active keys; scope each key | Auth and limits |
| "Sign in with Google" and similar | OpenID Connect on top of OAuth 2.0 | Validate the ID token's issuer, audience, expiry and signature | Identity |
JWT rules
- A JWT is signed, not encrypted: anyone can read its payload. Never put secrets or personal data in it.
- Verify the signature with a fixed, allowed algorithm list. Reject
alg: none. - Check
exp,issandaudon every request. - Keep access tokens short (minutes); you cannot easily revoke a JWT before it expires.
- Load roles and store access from the database for sensitive actions instead of trusting old claims.
Rate limits, idempotency and retries
Rate limit per API key or user, return 429 with Retry-After. Clients retry only idempotent requests, or POSTs with an idempotency key, with exponential backoff and jitter and a retry cap (Rate limiting, Timeouts and retries, Jobs and retries).
| Step | Idempotency key on POST /orders | Lesson |
|---|---|---|
| 1 | Client makes a random key (UUID) per user action, not per HTTP attempt, and resends the same key on retry | Jobs and retries |
| 2 | Server inserts the key into a table with a unique constraint, in the same transaction as the order | Transactions |
| 3 | If the key exists with the same body, return the stored response; with a different body, return 409 or 422 | Jobs and retries |
| 4 | If the first request is still running, return 409 and let the client retry later | Jobs and retries |
| 5 | Expire keys after a set time, such as 24 hours | Jobs and retries |
Webhooks
Receiving them (a payment provider tells Mangrove a payment succeeded) and sending them (Mangrove tells the Partner app an order shipped) follow the same rules (No overselling, Jobs and retries).
- Verify the signature (usually an HMAC of the raw body with a shared secret) with a constant-time compare, and reject old timestamps to stop replays.
- Answer 2xx fast, then do the work in a queue. Slow handlers cause the sender to time out and retry.
- Expect duplicates and out-of-order delivery: store the event id with a unique constraint, and check state before acting.
- When sending: retry with backoff for hours, sign every request, include an event id and type, and give receivers a way to replay.
- Run a reconciler that asks the provider about anything still pending; webhooks get lost.
Versioning and change
Mobile apps stay installed for months, so the Mangrove app version from last year still calls today's API (API design, Safe migrations).
| Change | Breaking? | How | Lesson |
|---|---|---|---|
| Add an optional request field or a response field | No | Ship it; clients must ignore unknown fields | API design |
| Add a value to an enum in a response | Often yes | Tell clients to handle unknown values; announce first | API design |
| Rename or remove a field, change a type | Yes | Add the new field, migrate clients, then remove the old one | Safe migrations |
| Make an optional field required | Yes | New version, or default it on the server | API design |
| A real redesign | Yes | New version in the path (/v2/orders) or a header; announce a sunset date for v1 | API design |
NestJS and Prisma snippets
The Mangrove platform runs NestJS on Node with Prisma for Postgres. A controller with validation, a guard and a scoped query (NestJS service, Prisma data, Auth and limits):
// main.ts: reject unknown fields and convert types for every DTO
app.useGlobalPipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true }));
// create-order.dto.ts
export class CreateOrderDto {
@IsString() storeId: string;
@IsArray() @ValidateNested({ each: true }) @Type(() => LineDto) lines: LineDto[];
}
// orders.controller.ts
@Controller('orders')
@UseGuards(AuthGuard)
export class OrdersController {
constructor(private readonly orders: OrdersService, private readonly prisma: PrismaService) {}
@Post()
@HttpCode(201)
create(@Body() dto: CreateOrderDto, @Headers('idempotency-key') key: string, @Req() req) {
return this.orders.create(req.user, dto, key);
}
@Get(':id')
async findOne(@Param('id') id: string, @Req() req) {
const order = await this.prisma.order.findFirst({
where: { id, storeId: { in: req.user.storeIds } }, // scope by the caller, not just by id
select: { id: true, status: true, totalCents: true }, // never return whole rows by default
});
if (!order) throw new NotFoundException();
return order;
}
}| Prisma: I want to... | Do this | Lesson |
|---|---|---|
| Avoid N+1 when listing orders with lines | include: { lines: true } or select the nested fields | Prisma data |
| Change two tables together or not at all | prisma.$transaction(async (tx) => { ... }) | Prisma data, Transactions |
| Decrement stock only if enough is left | updateMany({ where: { sku, qty: { gte: n } }, data: { qty: { decrement: n } } }) and check the count | No overselling |
| Spot a duplicate unique value | Catch error code P2002 and return 409 | Prisma data |
| Change the schema in production | prisma migrate deploy in the pipeline; migrate dev only locally | Prisma data |
| Run raw SQL safely | The $queryRaw tagged template, which parameterizes values; avoid the Unsafe variants with user input | Injection |
Testing and documenting
Test at the HTTP level: send a real request to the app with a test database, and assert on status, body and side effects (Tests and docs, Unit and API tests, API testing).
- For every endpoint test: happy path, validation error (400 or 422), no token (401), wrong store (403 or 404), not found, duplicate (409), and a retry with the same idempotency key.
- NestJS with Supertest:
await request(app.getHttpServer()).post('/orders').set('Authorization', `Bearer ${token}`).send(body).expect(201). - Generate an OpenAPI document from code (
@nestjs/swagger) and publish it; generate client types from it. - Contract tests catch the day the Partner app and the API stop agreeing, before users do (Contract tests).
- Load test the endpoints that face festival peaks, such as checkout and slot booking (Load testing).
- Log one structured line per request: method, route, status, duration, user id and request id. Never log tokens, passwords or full card numbers.
Security checklist for every endpoint
- Who may call this, and for which records? Check ownership in the query, not only in the UI (Access control).
- Validate and allow-list input; parameterize every query (Injection).
- Return only the fields the caller needs; never return password hashes or internal notes.
- Set body size limits and timeouts; rate limit login, OTP and search (Auth and limits).
- Secrets come from the environment or a secret manager, never from the repository (Security).
- Check the OWASP API Security Top 10 before launch.
Glossary
Plain definitions of the API terms used across the backend courses.
- Endpoint
- One method plus path that the API answers, such as
POST /orders. APIs - REST
- A style of API built from resources addressed by URLs and changed with standard HTTP methods. API design
- Resource
- A thing the API exposes, such as an order or a store, with its own URL. API design
- Idempotent
- Doing it twice has the same effect as doing it once. Jobs and retries
- Idempotency key
- A unique value sent with a request so the server can recognise a retry and return the first result. Jobs and retries
- DTO (data transfer object)
- A class describing the shape of a request or response, used for validation. NestJS service
- Guard
- NestJS code that runs before a route handler and decides whether the request may continue. Auth and limits
- Pipe
- NestJS code that validates or transforms input before the handler sees it. NestJS service
- ORM
- A library that maps database rows to objects in code, such as Prisma. Prisma data
- Migration
- A versioned script that changes the database schema, run in order in every environment. Safe migrations
- Cursor pagination
- Paging by "give me items after this one" instead of a page number; stable and fast. API design
- ETag
- A version tag for a resource, used for conditional GETs and safe concurrent updates. Caching
- CORS
- Browser rules that decide which other web origins may call your API from JavaScript. Auth and sessions
- CSRF
- An attack where another site makes the user's browser send a request with their cookies; blocked with SameSite cookies and tokens. Auth and sessions
- Session
- Server-side record of a signed-in user, referenced by a random id in a cookie. Auth and sessions
- JWT (JSON Web Token)
- A signed, base64url-encoded set of claims used as a self-contained access token. Auth and limits
- OAuth 2.0
- A framework for letting an app get limited access tokens for a user or service. Auth and limits
- PKCE
- Proof Key for Code Exchange: a one-time secret that stops a stolen authorization code being used by another app. Auth and limits
- OpenID Connect
- An identity layer on OAuth 2.0 that adds an ID token saying who the user is. Identity
- Refresh token
- A long-lived token used only to get new short-lived access tokens. Auth and limits
- Rate limit
- A cap on requests per client per period, answered with 429 when exceeded. Rate limiting
- Webhook
- An HTTP call one system makes to another when something happens. No overselling
- HMAC signature
- A hash of the message with a shared secret, proving it came from the sender and was not changed. No overselling
- OpenAPI
- A standard machine-readable description of an HTTP API, used for docs, client generation and tests. Tests and docs
- Contract test
- A test that checks a provider still meets what its consumers expect. Contract tests
- Problem Details
- RFC 9457's standard JSON shape for HTTP API errors. API design
Courses on Kolom
- Building APIs with Node.js: design, NestJS, Prisma, auth, jobs, tests and docs for Mangrove's order API.
- How the Web Works: what happens between the phone and the server, and API basics.
- Application Security for Developers: access control, injection and sessions done right.
- Scaling Building Blocks: caching, queues and rate limiting behind the API.
- Distributed Systems: timeouts, retries, outbox and sagas when one API calls another.
What to read next
- MDN Web Docs on HTTP at developer.mozilla.org. The clearest reference for methods, status codes, headers, caching and CORS.
- RFC 9110, HTTP Semantics at rfc-editor.org. The source for what each method and status code means.
- OWASP API Security Top 10 at owasp.org. The most common ways APIs leak data.
- The OpenAPI Specification at spec.openapis.org.
- The NestJS documentation at docs.nestjs.com and the Prisma documentation at prisma.io/docs.