EthicsComputerCourses
Join training +91 90532 10052

API Design: REST, gRPC, Pagination & Versioning

System Design · Lesson 15 of 29 Advanced 8 min read Updated

By the end of this lesson you will be able to design a clear REST API for a feature, choose correct methods and status codes, paginate large lists without duplicates, change an API without breaking old apps, and decide when gRPC suits better than REST.

What an API is

An API (application programming interface) is the contract between two programs: which requests are allowed, what they must contain and what comes back. In system design, the API is usually the first concrete thing you write after the requirements, because it shows exactly what the system must do.

Think of the menu at a restaurant in Lucknow. You do not walk into the kitchen; you order from the menu. The menu is the API. A good menu is clear, does not change the meaning of "Dal Makhani" every week, and tells you politely when something is not available. The same qualities make a good API.

REST: resources and methods

REST is a style for HTTP APIs built around resources: the nouns of your system, each with a URL. You act on them with standard HTTP methods.

MethodMeaningExampleIdempotent?
GETReadGET /restaurants/42/menuYes
POSTCreate, or run an actionPOST /ordersNo
PUTReplace entirelyPUT /users/77/addressYes
PATCHChange some fieldsPATCH /orders/8812Not guaranteed
DELETERemoveDELETE /cart/items/5Yes

Idempotent means doing it twice has the same effect as doing it once. Deleting an item twice leaves it deleted. Creating an order twice creates two orders. This matters a lot when networks fail and clients retry, which is the subject of Async Processing & Idempotency.

Use nouns in paths and let the method be the verb: POST /orders, not POST /createOrder. Nest only one level when it shows ownership: /restaurants/42/reviews.

Status codes and errors

Status codes tell the client what happened without reading the body.

CodeMeaningWhen
200 OKSuccess with a bodyA read or update
201 CreatedNew resource madeAfter POST /orders
204 No ContentSuccess, nothing to returnAfter a delete
400 Bad RequestThe request is malformedMissing field, wrong type
401 UnauthorizedNot logged inMissing or expired token
403 ForbiddenLogged in but not allowedViewing someone else's order
404 Not FoundNo such resourceWrong order ID
409 ConflictClashes with current stateSeat already booked
429 Too Many RequestsRate limit hitSee Rate Limiting
500 / 503Server problem / temporarily unavailableBug, overload, maintenance

Return errors in one consistent shape. The IETF standard "Problem Details for HTTP APIs" (RFC 9457) defines a common JSON format:

HTTP
POST /v1/bookings HTTP/1.1
Host: api.railyatra.example
Content-Type: application/json
Idempotency-Key: 7f3c9a10-4b2e-4d6f-9c1a-2b8e5d0f1a77

{"train_no": "12951", "date": "2026-11-20", "class": "3A", "passengers": 2}

HTTP/1.1 409 Conflict
Content-Type: application/problem+json

{
  "type": "https://api.railyatra.example/problems/no-seats",
  "title": "No seats available",
  "status": 409,
  "detail": "Class 3A on train 12951 for 2026-11-20 is full. Waitlist is open.",
  "waitlist_available": true
}

Pagination: offset vs cursor

Lists can be huge, so APIs return them in pages. There are two common styles.

Offset pagination says "skip 40, give me 20": GET /orders?offset=40&limit=20. It is simple and allows jumping to page 7. But it has two problems. The database must still walk past all skipped rows, which gets slow deep into a list. And if new items arrive while a user is scrolling, items shift and the user sees duplicates or misses some.

Cursor (keyset) pagination says "give me 20 items after this one": GET /orders?after=<cursor>&limit=20. The cursor encodes the position of the last item seen, such as its timestamp and ID. The database jumps straight there using an index, and new items at the top do not shift anything.

Step 1: run the simulation. A user reads page 1 of a feed, then three new posts arrive, then the user asks for page 2.

PythonRun it yourself
feed = [f"post-{i}" for i in range(100, 90, -1)]          # newest first: post-100 ... post-91

def offset_page(items, offset, limit):
    return items[offset:offset + limit]

def cursor_page(items, after, limit):
    start = items.index(after) + 1 if after else 0
    return items[start:start + limit]

page1 = offset_page(feed, 0, 4)
cursor = page1[-1]
print("Page 1          :", page1)

feed = ["post-103", "post-102", "post-101"] + feed           # 3 new posts arrive

print("Page 2 (offset) :", offset_page(feed, 4, 4))
print("Page 2 (cursor) :", cursor_page(feed, cursor, 4))
Output
Page 1          : ['post-100', 'post-99', 'post-98', 'post-97']
Page 2 (offset) : ['post-99', 'post-98', 'post-97', 'post-96']
Page 2 (cursor) : ['post-96', 'post-95', 'post-94', 'post-93']

Step 2: compare. Offset pagination repeated three posts the user had already seen (post-99 to post-97), because everything shifted down by three. The cursor continued exactly where the user stopped. In SQL, a cursor page is a keyset query:

SQL
SELECT id, created_at, text
FROM posts
WHERE (created_at, id) < ('2026-11-02 10:15:00', 98812)
ORDER BY created_at DESC, id DESC
LIMIT 20;

Clients should treat the cursor as an opaque string they pass back unchanged. Encoding it (for example as base64 of the timestamp and ID) lets you change its contents later without breaking anyone.

PythonRun it yourself
import base64, json

def make_cursor(created_at, item_id):
    raw = json.dumps({"t": created_at, "id": item_id}).encode()
    return base64.urlsafe_b64encode(raw).decode()

def read_cursor(cursor):
    return json.loads(base64.urlsafe_b64decode(cursor.encode()))

c = make_cursor("2026-11-02T10:15:00Z", 98812)
print("next_cursor:", c)
print("decoded    :", read_cursor(c))
Output
next_cursor: eyJ0IjogIjIwMjYtMTEtMDJUMTA6MTU6MDBaIiwgImlkIjogOTg4MTJ9
decoded    : {'t': '2026-11-02T10:15:00Z', 'id': 98812}

Versioning without breaking clients

Mobile apps stay installed for months. If you rename a field, old apps crash. So:

  1. Prefer additive changes. Adding a new optional field or a new endpoint does not break anyone. Clients must ignore fields they do not know.
  2. Never change the meaning of an existing field. If price was in rupees, do not make it paise. Add price_paise instead.
  3. When you must break, make a new version. Put it in the path (/v2/orders) or a header. Path versions are the easiest to see in logs and docs.
  4. Deprecate slowly. Announce the end date, measure who still calls the old version, and send a Sunset header (RFC 8594) with the shutdown date.

gRPC and other styles

gRPC is a framework for calling functions on another service. You describe messages and methods in a .proto file using Protocol Buffers, a compact binary format, and tools generate client and server code in many languages. gRPC runs over HTTP/2 and supports streaming in both directions.

Text
syntax = "proto3";

service Dispatch {
  rpc FindDriver (RideRequest) returns (DriverOffer);
  rpc TrackRide (RideId) returns (stream Location);   // server streams updates
}

message RideRequest {
  string rider_id = 1;
  double pickup_lat = 2;
  double pickup_lng = 3;
}
REST + JSONgRPC + Protobuf
FormatText, human-readableBinary, smaller and faster to parse
ContractOptional (OpenAPI)Required .proto file
Browser supportNativeNeeds a proxy (gRPC-Web)
StreamingLimited (SSE, WebSockets)Built in
Best forPublic APIs, mobile and web clientsInternal service-to-service calls

GraphQL is a third option: the client sends a query describing exactly which fields it wants, and one endpoint returns them. It suits apps whose screens need data from many sources, at the cost of harder caching and the need to protect the server from very expensive queries.

Common mistakes

  • Verbs in URLs (/getOrders, /deleteUser) instead of methods on resources.
  • Returning 200 with {"error": ...} in the body, which hides failures from clients, proxies and monitoring.
  • Offset pagination on large, changing lists, causing slow queries and duplicate items.
  • Breaking changes without a new version, crashing old mobile apps.
  • Unbounded list endpoints with no maximum limit.

Key points

  • REST models resources as URLs and uses HTTP methods as verbs; GET, PUT and DELETE are idempotent, POST is not.
  • Use precise status codes and one consistent error format such as RFC 9457 problem details.
  • Cursor pagination is fast and stable on changing lists; offset pagination is simple but shifts and slows down.
  • Evolve APIs with additive changes, version only for breaking changes, and deprecate slowly.
  • gRPC with Protocol Buffers suits internal, high-volume calls and streaming; REST suits public clients.

Exercise

Design the REST endpoints for a "college library" service: list books with search and pagination, view one book, borrow a book, return a book, and list a student's current loans. For each, give the method, path, a success status code and one likely error status code. Write them as a Python list of tuples and print them as a table.

Show solution
Python
endpoints = [
    ("GET",    "/v1/books?q=&after=&limit=",      200, 400),
    ("GET",    "/v1/books/{book_id}",             200, 404),
    ("POST",   "/v1/loans",                       201, 409),   # body: book_id; 409 if no copy free
    ("DELETE", "/v1/loans/{loan_id}",             204, 404),   # returning ends the loan
    ("GET",    "/v1/students/{student_id}/loans", 200, 403),
]
for method, path, ok, err in endpoints:
    print(f"{method:6} {path:34} {ok}  {err}")
Output
GET    /v1/books?q=&after=&limit=         200  400
GET    /v1/books/{book_id}                200  404
POST   /v1/loans                          201  409
DELETE /v1/loans/{loan_id}                204  404
GET    /v1/students/{student_id}/loans    200  403

Borrowing creates a loan resource, so it is a POST that returns 201; returning a book deletes the loan. A student viewing another student's loans gets 403. Some teams model a return as PATCH /v1/loans/{id} with {"returned": true} to keep the loan history; both are reasonable if you are consistent.

Quick Quiz

1. Which HTTP method is NOT idempotent?

2. Why does offset pagination show duplicates on a busy feed?

3. Which change is safe for old mobile clients?

4. Where is gRPC usually the best fit?

Learn System Design with a mentor

Free lessons get you started. Our Computer Science Fundamentals training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call