REST fundamentals
resource
The core noun of a REST API — a thing (a user, an order, a product) identified by a URI and manipulated via HTTP methods.
GET /users/42 # read the resource
DELETE /users/42 # remove the resource
💡 Good REST design is "resource-first": model the nouns, then attach verbs (HTTP methods) to them.
CRUD
Create, Read, Update, Delete — the four basic data operations, typically mapped to POST, GET, PUT/PATCH, and DELETE.
POST /orders # Create
GET /orders/9 # Read
PATCH /orders/9 # Update
DELETE /orders/9 # Delete
💡 Not every API operation fits neatly into CRUD — actions like "cancel" or "approve" often need their own endpoint.
PUT vs PATCH
PUT replaces the entire resource with the request body; PATCH applies a partial update to only the fields provided.
PUT /users/42 { "name": "Alice", "email": "a@b.com" } # full replace
PATCH /users/42 { "email": "new@b.com" } # partial update
💡 PUT is idempotent by definition; a naive PATCH implementation sometimes isn't — check the API docs.
nested resource
A resource scoped under a parent resource, expressed directly in the URI path.
GET /users/42/orders # this user's orders
GET /users/42/orders/17 # one specific order
💡 Nest no more than 2-3 levels deep — beyond that, flatten with query params instead.
content negotiation
The client and server agreeing on a response format via the Accept and Content-Type headers.
Accept: application/json
Content-Type: application/json; charset=utf-8
💡 A 406 Not Acceptable means the server can't produce any format the Accept header asked for.
HATEOAS
Hypermedia As The Engine Of Application State — responses include links to related actions so clients discover the API instead of hardcoding URLs.
{ "id": 42, "status": "pending",
"_links": { "cancel": "/orders/42/cancel", "self": "/orders/42" } }
💡 The most-cited-but-least-implemented part of the REST spec — most "RESTful" APIs skip it entirely.
request body / response body
The payload data sent with a request or returned with a response — distinct from headers (metadata) or the URL itself.
POST /users
Content-Type: application/json
{ "name": "Alice" } ← this JSON is the request body
💡 GET requests conventionally have no body — put filters in query params instead.
error envelope
A consistent JSON shape every error response follows, so clients can parse failures the same way everywhere.
{ "error": { "code": "VALIDATION_FAILED",
"message": "email is required",
"field": "email" } }
💡 Inconsistent error shapes across endpoints are one of the most common API design complaints.
Pagination & querying
offset pagination
Paging results with a page number and page size, e.g. ?page=2&limit=20 — simple to implement but slow and unstable as data changes.
GET /orders?page=2&limit=20
💡 If a row is inserted while paging, "page 2" can shift — you'll see a duplicate or miss a row.
cursor pagination
Paging with an opaque cursor token that points at a specific record, rather than a page number — stable even as data changes.
GET /orders?after=eyJpZCI6NDJ9&limit=20
💡 Preferred for large or fast-changing datasets — GitHub and Stripe APIs both use cursor pagination.
filtering
Query parameters that narrow down a result set to matching records.
GET /orders?status=shipped&customer_id=42
💡 Keep filter syntax consistent across every endpoint in the same API — don't mix status=x here and filter[status]=x there.
sorting
Query parameters that control the order of returned results, often with a "-" prefix for descending.
GET /orders?sort=-created_at,total
💡 Always document (and validate) the allowed sort fields — sorting on an unindexed column can be a performance trap.
sparse fieldset
Letting a client request only the specific fields it needs, to reduce response payload size.
GET /users/42?fields=id,name,email
💡 A lightweight REST answer to the over-fetching problem GraphQL was built to solve more generally.
GraphQL
query (GraphQL)
A GraphQL operation that reads data — the client specifies exactly which fields it wants back, nothing more.
query {
user(id: 42) { name email orders { id total } }
}
💡 One GraphQL query can replace several REST round-trips by fetching nested data in a single request.
mutation
A GraphQL operation that writes or changes data — the analogue of REST's POST, PUT, and DELETE.
mutation {
updateUser(id: 42, email: "new@b.com") { id email }
}
💡 By convention, mutations are named as verbs (createX, updateX, deleteX), unlike queries.
subscription
A GraphQL operation that opens a long-lived connection (usually WebSocket) and pushes updates to the client as the underlying data changes.
subscription {
orderStatusChanged(orderId: 42) { status updatedAt }
}
💡 The GraphQL equivalent of a webhook, but pushed directly to the connected client instead of a server endpoint.
resolver
The server-side function that fetches the actual data for one field in a GraphQL query.
const resolvers = {
Query: { user: (_, { id }) => db.users.findById(id) },
User: { orders: (user) => db.orders.findByUserId(user.id) },
};
💡 Every field in the schema has (or inherits) a resolver — this is where the N+1 problem usually creeps in.
schema (GraphQL)
The strongly-typed contract that defines every type, field, and operation a GraphQL API supports.
type User {
id: ID!
name: String!
orders: [Order!]!
}
💡 Unlike REST, the schema is queryable itself (introspection) — tools like GraphiQL read it to auto-generate docs.
fragment
A reusable, named selection of fields that can be shared across multiple GraphQL queries.
fragment UserCore on User { id name email }
query { user(id: 42) { ...UserCore } }
💡 Keeps large queries DRY — define a fragment once, reuse it everywhere that shape of data is needed.
over-fetching / under-fetching
The two REST problems GraphQL is designed to solve: over-fetching returns more fields than the client needs; under-fetching forces multiple requests to gather enough data.
# REST: GET /users/42 returns 40 fields when the UI needs 3 (over-fetching)
# REST: needs a 2nd call GET /users/42/orders to get order data (under-fetching)
💡 GraphQL trades this for a new problem: an unconstrained client query can still overload the server (see N+1 problem).
N+1 problem
A performance bug where resolving a list of N items triggers one extra database query per item instead of a single batched query.
# Naive: 1 query for orders, then N queries — one per order — for its user
# Fixed: batch-load all users in one query keyed by the order IDs (DataLoader)
💡 The single most common GraphQL performance bug — almost always fixed with a batching/caching layer like DataLoader.
gRPC & RPC
protocol buffers (protobuf)
Google's language-neutral binary serialization format — the schema file and compact wire format gRPC is built on.
message User {
int32 id = 1;
string name = 2;
}
💡 Smaller and faster to (de)serialize than JSON, but not human-readable on the wire — you need the .proto to decode it.
service definition
The .proto contract declaring a gRPC service's methods, and each method's request and response types.
service UserService {
rpc GetUser (GetUserRequest) returns (User);
}
💡 Code generators turn this one file into client and server stubs for every supported language.
unary RPC
A standard one request, one response gRPC call — the gRPC equivalent of a normal REST request/response.
rpc GetUser (GetUserRequest) returns (User);
💡 The default and most common gRPC call shape — reach for streaming only when you actually need it.
streaming RPC
A gRPC call where the client, the server, or both send a continuous stream of messages instead of a single request/response.
rpc WatchOrders (WatchRequest) returns (stream OrderUpdate); // server streaming
💡 Comes in three flavours: server streaming, client streaming, and bidirectional streaming.
stub (RPC)
The generated client-side code that lets you call a remote gRPC method as if it were a local function call.
const client = new UserServiceClient(address);
const user = await client.getUser({ id: 42 });
💡 Same word as the testing "stub", but a different concept — here it means auto-generated client code, not a fake dependency.
IDL
Interface Definition Language — a language (like .proto for gRPC, or WSDL for SOAP) for describing an API's types and operations independent of any one programming language.
// user.proto is the IDL file; protoc generates Go, Python, Java clients from it
💡 OpenAPI serves a similar role for REST APIs, though it's usually described as a "spec" rather than an IDL.
Versioning & lifecycle
API versioning
Signalling which version of an API contract a client is using — commonly via the URI path, a custom header, or a query parameter.
GET /v2/users/42
GET /users/42 (Accept-Version: 2)
💡 URI versioning (/v2/) is the most visible and cache-friendly; header versioning keeps URLs stable but is easy for clients to forget.
breaking change
A change to an API's contract that could break existing clients — removing a field, renaming an endpoint, tightening validation on an existing parameter.
// Breaking: removing "phone" from the User response
// Non-breaking: adding a new optional "phone2" field
💡 Adding new optional fields is safe; removing or renaming anything a client might already depend on is not.
deprecation
Marking an API version, endpoint, or field as scheduled for removal, usually with a communicated sunset date.
Sunset: Sat, 31 Dec 2026 23:59:59 GMT
Deprecation: true
💡 The Sunset and Deprecation HTTP response headers are the standard way to signal this machine-readably.
backward compatibility
The property that new versions of an API keep working correctly with clients written against an older version.
// v2 adds a required field with a sensible default so v1 clients don't break
💡 The core discipline behind "don't make breaking changes" — the goal most versioning strategies exist to protect.
semantic versioning (semver)
A MAJOR.MINOR.PATCH numbering scheme — bump MAJOR for breaking changes, MINOR for backward-compatible additions, PATCH for backward-compatible fixes.
v1.4.2 → v2.0.0 # a breaking change bumped the major version
💡 Widely used for package versions; APIs often borrow just the "major version = breaking change" convention without the full scheme.
Gateway, auth & ops
API gateway
A single entry point that routes, authenticates, rate-limits, and logs requests before they reach the actual backend services.
Client → API Gateway → { users-service, orders-service, payments-service }
💡 Centralizes cross-cutting concerns (auth, rate limiting, logging) so individual services don't each reimplement them.
rate limiting
Capping how many requests a client can make in a given time window, to protect the API from overload or abuse.
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 42
X-RateLimit-Reset: 1735689600
💡 A 429 Too Many Requests response usually comes with a Retry-After header telling the client when to try again.
API key
A simple, static credential a client includes with each request to identify itself and authorize access.
curl -H "X-API-Key: sk_live_abc123..." https://api.example.com/orders
💡 Simpler than OAuth but weaker — a leaked key grants full access until manually revoked, with no built-in expiry.
throttling
Deliberately slowing or delaying requests that exceed a rate limit, instead of rejecting them outright with an error.
# Instead of a hard 429, the gateway queues and delays excess requests
💡 Gentler than rate-limit rejection but can mask the actual load a client is generating.
OpenAPI spec (Swagger)
A machine-readable YAML/JSON document describing every endpoint, parameter, request/response shape of a REST API.
paths:
/users/{id}:
get:
parameters: [{ name: id, in: path, required: true }]
responses: { '200': { description: OK } }
💡 "Swagger" is the older brand name for the same spec format and its tooling (Swagger UI, Swagger Editor).
idempotency key
A client-generated unique value attached to a request so a retried POST is safely deduplicated by the server instead of processed twice.
POST /payments
Idempotency-Key: 7c3b1e9a-...
💡 Critical for payment APIs — a network timeout followed by a client retry must not charge the customer twice.