REST: Architectural Style, Not a Protocol
ROY FIELDING, 2000REST (Representational State Transfer) was defined by Roy Fielding in his 2000 PhD dissertation. It is a set of architectural constraints on how a distributed hypermedia system should behave — not a spec, not a library, not a protocol.
Most "REST APIs" in the wild only partially implement these constraints. Understanding the full model lets you make deliberate trade-offs rather than accidental ones.
The 6 REST Constraints
- 1Client–Server — Separation of concerns: the UI/client and data storage/server evolve independently. Neither knows the internals of the other.
- 2Stateless — Each request contains all information needed to process it. The server holds no session state between requests. Enables horizontal scaling and fault tolerance.
- 3Cacheable — Responses must declare themselves cacheable or non-cacheable. Caching eliminates some client–server interactions, improving scalability and perceived performance.
- 4Uniform Interface — The central REST constraint. Four sub-constraints: resource identification in requests, manipulation through representations, self-descriptive messages, and HATEOAS.
- 5Layered System — The client cannot tell whether it's connected to the end server or an intermediary (load balancer, CDN, API gateway). Each layer only sees adjacent layers.
- 6Code on Demand (optional) — Servers can extend client functionality by transferring executable code (JavaScript). The only optional constraint.
🏛️ Analogy: REST is like a well-designed postal system. You write a self-contained letter (stateless request) addressed to a specific location (resource URL). The postal network (layered system) routes it without the sender knowing the path. The letter format is standardised (uniform interface). Replies can be archived (cacheable). The sender and postal network are independent organisations (client–server).
Richardson Maturity Model
The RMM measures how thoroughly an HTTP API adheres to REST. Most production APIs should aim for Level 2.
POST /api {"action":"getUser","id":1}
POST /users/1 {"action":"get"}
GET /users/1 → 200 OK
{"id":1, "_links":{"self":"/users/1","posts":"/users/1/posts"}}
Practical takeaway: Design to Level 2 consistently. Add HATEOAS only if you have clients you cannot coordinate deployments with (public APIs, third-party integrations). The stateless constraint is the most important to enforce — sessions stored on the server destroy horizontal scaling.
Resources Are Nouns, Not Verbs
CORE PRINCIPLEThe single biggest mistake in URL design is using verbs. In REST, HTTP methods are the verbs. Your URLs should identify things (resources) — nouns. The action is expressed by the combination of HTTP method + URL.
URL Naming Conventions
| Rule | Good | Bad | Why |
|---|---|---|---|
| Use plural nouns | /users, /orders | /user, /getOrder | Consistent regardless of count; collections are plural |
| Lowercase, hyphen-separated | /blog-posts | /blogPosts, /BlogPosts | URLs are case-sensitive; hyphens improve readability |
| No file extensions | /users/42 | /users/42.json | Use Accept header for content negotiation |
| No trailing slashes | /users/42 | /users/42/ | Trailing slash implies a directory; inconsistency causes 404s |
| IDs in path for single resources | /users/42 | /users?id=42 | Path params for identity, query params for filtering |
| Hierarchy mirrors relationships | /users/42/posts | /posts?author=42 | Either works; use nesting max 2 levels deep |
Collections vs Items
| URL Pattern | Represents | Typical Operations |
|---|---|---|
/users | Collection of all users | GET (list), POST (create) |
/users/42 | Single user with id=42 | GET, PUT, PATCH, DELETE |
/users/42/posts | Posts belonging to user 42 | GET (list), POST (create) |
/users/42/posts/7 | Post 7 by user 42 | GET, PUT, PATCH, DELETE |
Nesting: When to Stop
Maximum 2 Levels of Nesting
DESIGN RULEDeep nesting creates URLs that are hard to remember, hard to construct, and tightly coupled to your data model. After 2 levels, use query parameters or a flat URL with filter params.
The resource /comments/2 is unambiguous — comment IDs are globally unique. The parent context is available from the comment's own post_id field in the response.
Special Actions That Don't Fit the Resource Model
Action Sub-Resources
PRAGMATISMSome operations are inherently procedural and don't map cleanly to CRUD on a resource: "send a password reset email", "archive a conversation", "retry a failed payment". The pragmatic approach is to use an action sub-resource with POST.
| Action | REST-ish URL | Method |
|---|---|---|
| Send password reset | /users/42/password-reset | POST |
| Archive a post | /posts/7/archive | POST |
| Publish a draft | /posts/7/publish | POST |
| Retry payment | /payments/9/retry | POST |
| Transfer ownership | /orgs/5/ownership-transfers | POST (creates a transfer resource) |
Note: "archive" creates an archived state, "publish" creates a publication event — these can be modelled as resource state transitions triggered by POST. The verb form (/archive) is tolerated here because the action sub-resource approach is the least bad option.
Query Parameters: Filtering, Sorting, Searching
| Operation | Convention | Example |
|---|---|---|
| Filter by field | ?field=value | GET /users?status=active&role=admin |
| Sort | ?sort=field, ?sort=-field (minus = desc) | GET /users?sort=-created_at |
| Full-text search | ?q=term or ?search=term | GET /posts?q=kubernetes |
| Sparse fieldsets | ?fields=f1,f2 | GET /users/42?fields=id,name,email |
| Embedded relations | ?include=rel1,rel2 | GET /posts/7?include=author,comments |
| Pagination | see Req & Resp tab | GET /users?page=2&per_page=20 |
Safety and Idempotency
RFC 9110Two properties of HTTP methods govern how clients, proxies, and servers can safely retry and cache requests. Violating these contracts breaks the entire HTTP caching and reliability infrastructure.
Safe
A method is safe if it does not alter server state. Clients, crawlers, and prefetch logic can freely call safe methods without side effects. GET, HEAD, OPTIONS, TRACE are safe.
Idempotent
A method is idempotent if calling it N times has the same effect as calling it once. Safe methods are inherently idempotent. PUT and DELETE are idempotent but not safe.
Method Reference
| Method | Safe | Idempotent | Has Body | Semantics |
|---|---|---|---|---|
| GET | ✅ | ✅ | No | Retrieve resource or collection. Parameters via query string only. Responses should be cacheable. |
| HEAD | ✅ | ✅ | No | Same as GET but response body omitted. Used to check existence, get ETag, or measure response size without downloading. |
| POST | ❌ | ❌ | Yes | Create a new resource in a collection. Server assigns the ID. Returns 201 Created with Location header pointing to the new resource. |
| PUT | ❌ | ✅ | Yes | Full replacement of a resource. Client sends the complete representation. If resource doesn't exist, create it (optional). Idempotent — sending same request twice has same result. |
| PATCH | ❌ | ❌* | Yes | Partial update. Only the specified fields are modified. Not inherently idempotent (a {"views": {"op":"increment"}} patch is not). JSON Merge Patch (RFC 7396) or JSON Patch (RFC 6902) for structured updates. |
| DELETE | ❌ | ✅ | Rare | Delete a resource. First call returns 200/204; repeat calls should return 404 (or 204 again for strict idempotency). Soft-delete returns 200 with archived representation. |
| OPTIONS | ✅ | ✅ | No | Returns allowed methods for a URL. Used by browsers in CORS preflight. Also useful for API discovery. |
*PATCH can be made idempotent by using a conditional update with If-Match: "etag-value" — the server only applies the patch if the current ETag matches, preventing lost updates.
PUT vs PATCH: Choosing the Right One
PUT — Full Replacement
Client must send the entire resource. Missing fields are set to null/default, not preserved.
Use when: client owns the full resource and wants a clean replacement.
PATCH — Partial Update
Client sends only the fields to change. Other fields are preserved.
Use when: only a subset of fields should change, or partial updates are the main use case.
JSON Merge Patch vs JSON Patch
| Format | RFC | Mechanism | Best for |
|---|---|---|---|
| JSON Merge Patch | RFC 7396 | Merge the patch object into the resource. Set field to null to delete it. | Simple field updates, human-readable patches |
| JSON Patch | RFC 6902 | Array of operations: add, remove, replace, move, copy, test. Operates on JSON Pointer paths. | Complex mutations, array element updates, transactional multi-field updates |
// JSON Patch (RFC 6902) example [ { "op": "replace", "path": "/email", "value": "new@example.com" }, { "op": "add", "path": "/tags/-", "value": "premium" }, { "op": "remove", "path": "/legacy_id" }, { "op": "test", "path": "/version", "value": 3 } // fails patch if version != 3 ]
Idempotency Keys for POST
Making POST Safe to Retry
DISTRIBUTED SYSTEMSPOST is not idempotent — retrying a failed payment POST could charge twice. The solution is an idempotency key: a client-generated UUID sent with the request. The server stores the key + response, and on retry returns the cached response without re-executing.
Stripe, PayPal, and most payment APIs use this pattern. Idempotency key storage: Redis or DB table with TTL of 24 hours is standard.
Status Codes Are Semantic Contracts
RFC 9110Returning the wrong status code breaks client retry logic, caching, monitoring dashboards, and on-call alerting. Always use the most specific code available. Never return 200 for errors ("200 with an error body" is a Level 0 anti-pattern).
2xx — Success
| Code | Name | Use when |
|---|---|---|
| 200 | OK | GET, PUT, PATCH succeeded. Return the updated representation in body. |
| 201 | Created | POST created a new resource. Include Location: /resources/new-id header. Optionally return the created resource. |
| 202 | Accepted | Request accepted but processing not yet complete (async job queued). Return a job/status resource URL. |
| 204 | No Content | DELETE succeeded, or PATCH/PUT with no response body needed. No body — not even {}. |
| 206 | Partial Content | Response is a range of a larger resource (used with Range header for resumable downloads). |
3xx — Redirection
| Code | Name | Use when |
|---|---|---|
| 301 | Moved Permanently | Resource has a new permanent URL. Clients and search engines should update their bookmarks. Method may change to GET. |
| 302 | Found | Temporary redirect. Often misused as permanent redirect. Browsers change POST → GET. |
| 303 | See Other | After a POST (create), redirect client to the created resource with GET. The Post/Redirect/Get pattern. |
| 304 | Not Modified | Conditional GET (with If-None-Match or If-Modified-Since) — cached version is still fresh. No body. |
| 307 | Temporary Redirect | Like 302 but method is preserved. POST → same POST at new URL. |
| 308 | Permanent Redirect | Like 301 but method is preserved. POST → same POST at new URL permanently. |
4xx — Client Errors
| Code | Name | Use when |
|---|---|---|
| 400 | Bad Request | Request is malformed, missing required fields, or fails validation. Include error details in body. |
| 401 | Unauthorized | Not authenticated. Despite the name, means "please authenticate". Include WWW-Authenticate header. |
| 403 | Forbidden | Authenticated but not authorised. Client identity is known; the action is not permitted for this identity. |
| 404 | Not Found | Resource doesn't exist. Also used to hide existence of protected resources (instead of 403). |
| 405 | Method Not Allowed | HTTP method not supported for this URL. Include Allow: GET, POST header listing supported methods. |
| 409 | Conflict | State conflict — duplicate key, version mismatch (optimistic locking), or concurrent modification. |
| 410 | Gone | Resource existed but was permanently deleted. Use when you want clients/crawlers to remove cached reference. |
| 422 | Unprocessable Entity | Well-formed request but semantic validation failed (e.g., start_date > end_date). Distinguished from 400 (malformed). |
| 429 | Too Many Requests | Rate limit exceeded. Include Retry-After and X-RateLimit-* headers. |
5xx — Server Errors
| Code | Name | Use when |
|---|---|---|
| 500 | Internal Server Error | Unhandled exception, bug, or unexpected state. Never expose stack traces. Log internally, return safe error message. |
| 502 | Bad Gateway | Upstream server (DB, microservice) returned an invalid response. Set by proxies/load balancers. |
| 503 | Service Unavailable | Server temporarily overloaded or in maintenance. Include Retry-After header. Clients should back off. |
| 504 | Gateway Timeout | Upstream server didn't respond in time. Set by proxies/load balancers. Distinguished from 503. |
⚠️ The 401 vs 403 trap: 401 = "I don't know who you are, please authenticate". 403 = "I know who you are, but you can't do this". Never return 403 to an unauthenticated request — it reveals that the resource exists and is protected. Prefer 401 or 404 depending on whether you want to expose existence.
Content Negotiation
Accept & Content-Type
RFC 9110HTTP allows clients and servers to negotiate representation format without changing the URL. Use the standard headers — don't invent /api/users.json URL-based formats.
| Header | Direction | Meaning |
|---|---|---|
Accept | Client → Server | Formats the client can handle, by preference: Accept: application/json, application/xml;q=0.8 |
Content-Type | Both | Format of the body being sent: Content-Type: application/json |
Accept-Language | Client → Server | Preferred language for localised responses |
Accept-Encoding | Client → Server | Compression formats supported: Accept-Encoding: gzip, deflate, br |
If the server cannot satisfy the Accept header, return 406 Not Acceptable.
Pagination Strategies
Three Pagination Patterns
SCALABILITYOffset Pagination
Pros: Simple, random access to any page, total count available.
Cons: Inconsistent results when items are inserted/deleted during pagination (items shift). Slow for high offsets (DB scans all preceding rows). Not recommended past page 100.
Cursor Pagination
Pros: Stable under mutations (no shifting). O(1) regardless of page depth. The only scalable strategy for high-volume feeds.
Cons: No random access (must walk from start). No total count without a separate query.
Keyset Pagination (Time-based)
Variant of cursor pagination using a meaningful timestamp or composite key instead of an opaque cursor. Natural for time-series data (activity feeds, logs, notifications).
Pagination Response Envelope
Use the Link header (RFC 5988) for hypermedia pagination links — this is the standard HTTP way. The response body pagination object is a convenience for clients that don't parse headers. Provide both.
Response Envelope vs Bare Resources
Bare Resource (simple)
Clean, minimal. Standard for single-resource responses. Client knows it's talking to /users/42.
Envelope (for collections)
Wraps collection with metadata. JSON:API and HAL both use envelope patterns.
Caching with ETags
Conditional Requests
PERFORMANCEETags (entity tags) enable cache validation and optimistic locking. The server returns an ETag (hash of resource content) with each response. Clients send it back on subsequent requests.
RFC 7807 — Problem Details
Structured Error Responses
RFC 7807RFC 7807 defines a standard JSON format for HTTP API error responses. Adopting it means clients can parse errors predictably, and monitoring tools can aggregate them by type.
Content-Type: application/problem+json
| Field | Type | Required | Description |
|---|---|---|---|
type | URI | Yes | A URI identifying the problem type. Should resolve to human-readable documentation. Use about:blank if no docs exist. |
title | string | Yes | Short, human-readable summary. Should not change between occurrences of the same problem type. |
status | integer | Yes | HTTP status code. Must match the response status. |
detail | string | No | Human-readable explanation specific to this occurrence. |
instance | URI | No | URI identifying this specific occurrence. Can be used to look up a log entry or support ticket. |
RFC 7807 Examples
Validation Error (400/422)
Not Found (404)
Rate Limit (429)
API Versioning Strategies
When and How to Version
EVOLVABILITYYou need versioning when a change is breaking — removes a field, changes a field type, alters semantics, removes an endpoint. Additive changes (new optional fields, new endpoints) are non-breaking and don't require a new version.
| Strategy | Example | Pros | Cons |
|---|---|---|---|
| URL path | /v1/users, /v2/users |
Explicit, easily visible in logs and browsers, cacheable | URL changes break bookmarks/hardcoded URLs |
| Query parameter | /users?version=2 |
URL stable, easy to test in browser | Easy to forget, pollutes query string, cache complications |
| Accept header | Accept: application/vnd.api+json;version=2 |
Purist REST, URL unchanged, proper content negotiation | Hard to test in browser, verbose, poorly supported by API gateways |
| Custom header | API-Version: 2026-03-01 |
URL stable, Stripe-style date versioning is explicit | Non-standard header, requires documentation |
Industry recommendation: URL path versioning (/v1/, /v2/) is the most widely adopted. It is explicit, easy to understand, and works with every HTTP client. Stripe uses date-based header versioning successfully, but they have exceptional API docs — for most teams, URL versioning is safer. Never version endpoints individually — version the whole API.
Deprecation Process
- 1Add deprecation headers to old API responses:
Deprecation: true,Sunset: Sat, 01 Jan 2028 00:00:00 GMT,Link: </v2/users>; rel="successor-version" - 2Publish changelog with migration guide. List every breaking change and its replacement. Provide a diff of request/response shapes.
- 3Email active consumers from your API key records. Give at least 6 months notice for significant breaking changes.
- 4Monitor old version traffic — track last-call timestamps per API key. Sunset date should be after the last active consumer migrates (or times out).
- 5Hard sunset — return 410 Gone with a body pointing to the migration guide. Never return 404 (implies the endpoint never existed).
HTTP Request Parsing in C (Minimal)
#include <string.h> #include <stdio.h> typedef struct { char method[16]; char path[256]; char version[16]; char host[256]; char content_type[64]; int content_length; char body[8192]; } http_request_t; int parse_request(const char *raw, http_request_t *req) { /* Parse request line: "METHOD /path HTTP/1.1\r\n" */ if (sscanf(raw, "%15s %255s %15s", req->method, req->path, req->version) != 3) return -1; /* Parse headers line by line */ const char *p = strstr(raw, "\r\n") + 2; req->content_length = 0; while (p && *p != '\r') { if (strncasecmp(p, "Host: ", 6) == 0) sscanf(p + 6, "%255[^\r]", req->host); else if (strncasecmp(p, "Content-Type: ", 14) == 0) sscanf(p + 14, "%63[^\r]", req->content_type); else if (strncasecmp(p, "Content-Length: ", 16) == 0) sscanf(p + 16, "%d", &req->content_length); p = strstr(p, "\r\n"); if (p) p += 2; } /* Body starts after \r\n\r\n */ const char *body_start = strstr(raw, "\r\n\r\n"); if (body_start && req->content_length > 0) { body_start += 4; int len = req->content_length < (int)sizeof(req->body) - 1 ? req->content_length : (int)sizeof(req->body) - 1; memcpy(req->body, body_start, len); req->body[len] = '\0'; } return 0; }
⚠️ This is for learning only. Production HTTP parsing is complex: chunked encoding, multipart bodies, header folding, request smuggling defences, binary content. Use a battle-tested HTTP parser library (llhttp — the Node.js HTTP parser, or picohttpparser for C) in production code.
HTTP Response Builder in C
#include <stdio.h> #include <string.h> #include <time.h> typedef struct { int status; char content_type[64]; char body[65536]; int body_len; } http_response_t; int build_response(http_response_t *resp, char *out, int out_size) { const char *status_text; switch (resp->status) { case 200: status_text = "OK"; break; case 201: status_text = "Created"; break; case 204: status_text = "No Content"; break; case 400: status_text = "Bad Request"; break; case 404: status_text = "Not Found"; break; case 422: status_text = "Unprocessable Entity"; break; case 500: status_text = "Internal Server Error"; break; default: status_text = "Unknown"; } /* RFC 7231 date format */ char date_buf[64]; time_t now = time(NULL); strftime(date_buf, sizeof(date_buf), "%a, %d %b %Y %H:%M:%S GMT", gmtime(&now)); int n = snprintf(out, out_size, "HTTP/1.1 %d %s\r\n" "Content-Type: %s\r\n" "Content-Length: %d\r\n" "Date: %s\r\n" "Connection: keep-alive\r\n" "\r\n", resp->status, status_text, resp->content_type, resp->body_len, date_buf); if (n + resp->body_len >= out_size) return -1; memcpy(out + n, resp->body, resp->body_len); return n + resp->body_len; } /* Helper: respond with JSON */ void respond_json(int fd, int status, const char *json) { http_response_t resp = { .status = status, .body_len = strlen(json) }; strncpy(resp.content_type, "application/json", sizeof(resp.content_type)); memcpy(resp.body, json, resp.body_len); char out[66000]; int len = build_response(&resp, out, sizeof(out)); if (len > 0) send(fd, out, len, MSG_NOSIGNAL); } /* Helper: RFC 7807 problem response */ void respond_problem(int fd, int status, const char *type, const char *title, const char *detail) { char body[1024]; snprintf(body, sizeof(body), "{" "\"type\":\"%s\"," "\"title\":\"%s\"," "\"status\":%d," "\"detail\":\"%s\"" "}", type, title, status, detail); http_response_t resp = { .status = status }; strncpy(resp.content_type, "application/problem+json", sizeof(resp.content_type)); resp.body_len = strlen(body); memcpy(resp.body, body, resp.body_len); char out[66000]; int len = build_response(&resp, out, sizeof(out)); if (len > 0) send(fd, out, len, MSG_NOSIGNAL); }
Consuming a REST API with libcurl (C)
#include <curl/curl.h> #include <string.h> #include <stdlib.h> #include <stdio.h> /* Compile: gcc rest_client.c -lcurl -o rest_client */ typedef struct { char *data; size_t len; } buf_t; static size_t write_cb(void *ptr, size_t sz, size_t nmemb, buf_t *b) { size_t n = sz * nmemb; b->data = realloc(b->data, b->len + n + 1); memcpy(b->data + b->len, ptr, n); b->len += n; b->data[b->len] = '\0'; return n; } /* GET /users/42 with Bearer token */ int get_user(const char *base_url, int user_id, const char *token) { CURL *curl = curl_easy_init(); if (!curl) return -1; char url[256]; snprintf(url, sizeof(url), "%s/users/%d", base_url, user_id); buf_t resp = {NULL, 0}; /* Auth header */ char auth[512]; snprintf(auth, sizeof(auth), "Authorization: Bearer %s", token); struct curl_slist *hdrs = curl_slist_append(NULL, auth); hdrs = curl_slist_append(hdrs, "Accept: application/json"); curl_easy_setopt(curl, CURLOPT_URL, url); curl_easy_setopt(curl, CURLOPT_HTTPHEADER, hdrs); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_cb); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &resp); CURLcode rc = curl_easy_perform(curl); long http_code = 0; curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); if (rc == CURLE_OK) { printf("HTTP %ld\n%s\n", http_code, resp.data); } else { fprintf(stderr, "curl error: %s\n", curl_easy_strerror(rc)); } free(resp.data); curl_slist_free_all(hdrs); curl_easy_cleanup(curl); return rc == CURLE_OK ? 0 : -1; } /* POST /users with JSON body */ int create_user(const char *base_url, const char *json_body) { CURL *curl = curl_easy_init(); if (!curl) return -1; char url[256]; snprintf(url, sizeof(url), "%s/users", base_url); buf_t resp = {NULL, 0}; struct curl_slist *hdrs = curl_slist_append(NULL, "Content-Type: application/json"); hdrs = curl_slist_append(hdrs, "Accept: application/json"); curl_easy_setopt(curl, CURLOPT_URL, url); curl_easy_setopt(curl, CURLOPT_POST, 1L); curl_easy_setopt(curl, CURLOPT_POSTFIELDS, json_body); curl_easy_setopt(curl, CURLOPT_HTTPHEADER, hdrs); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_cb); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &resp); CURLcode rc = curl_easy_perform(curl); long http_code = 0; curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); if (rc == CURLE_OK) { printf("HTTP %ld\n%s\n", http_code, resp.data); /* 201: check Location header for new resource URL */ } free(resp.data); curl_slist_free_all(hdrs); curl_easy_cleanup(curl); return rc == CURLE_OK ? (int)http_code : -1; }
Simple Router in C
typedef void (*handler_fn)(int fd, http_request_t *req); typedef struct { const char *method; const char *path_prefix; handler_fn handler; } route_t; void handle_get_users (int fd, http_request_t *req); void handle_get_user (int fd, http_request_t *req); void handle_create_user(int fd, http_request_t *req); static route_t routes[] = { { "GET", "/users/", handle_get_user }, /* /users/42 */ { "GET", "/users", handle_get_users }, /* /users */ { "POST", "/users", handle_create_user}, { NULL, NULL, NULL } }; void dispatch(int fd, http_request_t *req) { for (int i = 0; routes[i].method; i++) { if (strcmp(req->method, routes[i].method) == 0 && strncmp(req->path, routes[i].path_prefix, strlen(routes[i].path_prefix)) == 0) { routes[i].handler(fd, req); return; } } /* 404 fallthrough */ respond_problem(fd, 404, "about:blank", "Not Found", "The requested resource does not exist."); }
🔬 Lab 1 — Design a REST API (Paper Exercise)
TOOLS: pen & paper / whiteboardGoal: Practice resource modelling before writing code. Design the URL structure for a simple task management API.
Domain: Users create projects. Projects contain tasks. Tasks have comments. Tasks can be assigned to users.
METHOD /path → STATUS and describe the request body and response body.GET /projects/:id/tasks. Choose between offset and cursor pagination and justify your choice. Design the full response envelope including headers.🔬 Lab 2 — Build a REST API Server in C
TOOLS: gcc · curl · jqGoal: Build a minimal in-memory REST API for the Users resource using the TCP server and HTTP helpers from this module.
parse_request() + dispatch().user_t users[MAX_USERS] with fields id, name, email. Implement handle_get_users to return all users as a JSON array.handle_create_user: parse the JSON body (use simple sscanf or hand-roll a tiny parser), validate required fields (400/422 on failure), assign an auto-increment ID, return 201 with Location: /users/:id.handle_get_user: extract the ID from the path (/users/42), look up in the store, return 200 with user JSON or 404 RFC 7807 problem.curl -s -X POST http://localhost:8080/users -H "Content-Type: application/json" -d '{"name":"Alice","email":"a@b.com"}' | jqcurl -s http://localhost:8080/users | jqcurl -s http://localhost:8080/users/1 | jqcurl -s http://localhost:8080/users/999 | jq
If-Match ETag check to PATCH — return 412 if ETag doesn't match the current version hash.🔬 Lab 3 — Consume a Public REST API with libcurl
TOOLS: gcc · libcurl · jqGoal: Write a C program that consumes the GitHub REST API to list your own repositories.
sudo apt install libcurl4-openssl-dev (Debian) or brew install curl (macOS).create_user / get_user libcurl code as a template. Call GET https://api.github.com/users/YOUR_USERNAME/repos?per_page=5&sort=updated. Set header User-Agent: my-rest-client/1.0 (GitHub requires this).name and stargazers_count.Link header from the response. Extract the rel="next" URL and fetch the next page. Print all repos across pages.Authorization: Bearer TOKEN. Verify you can now see private repos.🔬 Lab 4 — Test Your API with Behaviour-Driven Tests
TOOLS: bash · curl · jq · diffGoal: Write a shell test suite for your REST server from Lab 2. Practice test-first API design thinking.
test_api.sh. Start with a helper function assert_status(url, method, expected_code) using curl's -o /dev/null -s -w "%{http_code}" output.Content-Type: application/problem+json header.Module Mastery Checklist
M03 COMPLETEYou have mastered this module when you can check off every item below without referring to notes.
REST Fundamentals
- State the 6 REST constraints and identify which one is "uniform interface"
- Describe the 4 sub-constraints of the uniform interface (resource identification, manipulation through representations, self-descriptive messages, HATEOAS)
- Place a given API at the correct Richardson Maturity Model level and explain what changes would elevate it
- Explain why the stateless constraint is the most important for horizontal scaling
Resource Design
- Rewrite a set of verb URLs as correct REST resource URLs
- Explain the difference between path parameters (identity) and query parameters (filtering/sorting)
- Design URLs for a nested resource hierarchy with maximum 2 levels of nesting
- Design action sub-resources for non-CRUD operations (publish, archive, retry)
- Define the correct query parameter conventions for filtering, sorting, and full-text search
HTTP Methods
- Define safe and idempotent; classify GET, POST, PUT, PATCH, DELETE correctly on both axes
- Explain the difference between PUT (full replacement) and PATCH (partial update)
- Describe JSON Merge Patch (RFC 7396) vs JSON Patch (RFC 6902) and when to use each
- Explain idempotency keys: why they are needed for POST, how they work, where to store them
- Explain how to make a PATCH idempotent using conditional requests with
If-Match
Status Codes
- State the correct status codes for: create (201), no body (204), validation error (422), auth failure (401 vs 403), conflict (409), async accepted (202)
- Explain the difference between 401 and 403 — and why returning 403 to an unauthenticated user leaks information
- Explain when to use 410 Gone instead of 404 Not Found
- State which headers to include with 405 (Allow), 429 (Retry-After, X-RateLimit-*), and 201 (Location)
Request & Response Design
- Compare offset pagination vs cursor pagination — state two advantages of cursor and one disadvantage
- Design a paginated response with correct
Linkheader (RFC 5988) and response envelope - Explain ETags: how they enable conditional GET (304) and optimistic locking on PATCH
- Use content negotiation correctly:
Acceptin request,Content-Typein response, 406 if unsatisfied
Errors & Versioning
- Write a correct RFC 7807 problem detail response for a validation error, including the
errorsextension array - Name 4 versioning strategies and state the pros and cons of URL path versioning
- Describe the full API deprecation process from announcement headers through hard sunset
Implementation
- Use libcurl in C to perform GET and POST requests with custom headers and JSON body, checking the response status code
- Build a minimal HTTP router in C using a method + path prefix dispatch table
- Return RFC 7807 problem+json responses from a C HTTP handler
Next modules in Phase 1: M04 covers OpenAPI specification and gRPC — moving from documentation by convention to contract-first design. M05 covers GraphQL — a fundamentally different query model that solves over-fetching and under-fetching at the cost of caching complexity.