API Design Vocabulary

38 terms for talking about REST, GraphQL, and gRPC APIs in plain English — what each one means, an example, and the gotcha worth knowing.

Last reviewed:

Sections

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.

English phrases engineers use

  • "This is a breaking change — we need to bump the major version."
  • "Let's switch to cursor pagination, the offset approach is skipping rows under load."
  • "That N+1 problem is killing our GraphQL response time — we need a DataLoader."
  • "Don't forget the idempotency key on the retry, or we'll double-charge the customer."
  • "Is this field required by the OpenAPI spec, or just convention?"
  • "We're deprecating v1 — check the Sunset header for the exact date."

Frequently Asked Questions

What is the actual difference between PUT and PATCH?

PUT replaces the entire resource with whatever is in the request body — any field you omit is effectively cleared or reset. PATCH applies a partial update, changing only the fields included in the request and leaving the rest untouched. PUT is idempotent by definition (sending it twice produces the same result), while a naive PATCH implementation is not always guaranteed to be, so it is worth checking the specific API docs.

Why is cursor pagination generally preferred over offset pagination for large datasets?

Offset pagination (?page=2&limit=20) is simple but unstable: if a row is inserted or deleted while a client is paging through results, later pages can skip a row or show a duplicate. Cursor pagination points at a specific record via an opaque token instead of a page number, so it stays stable even as the underlying data changes — this is why APIs like Stripe and GitHub use cursor-based pagination for large or fast-changing collections.

What is the N+1 problem in GraphQL and how is it usually fixed?

The N+1 problem happens when resolving a list of N items triggers one extra database query per item instead of a single batched query — for example, fetching 50 orders and then making 50 separate queries to fetch each order's user. It is almost always fixed with a batching and caching layer like DataLoader, which collects the individual lookups within a request tick and issues one query keyed by all the needed IDs.