ScreenshotNeo

BlogEngineering

Designing a RESTful Web API

A practical guide to resource modeling, HTTP semantics, errors, pagination, async work, versioning, and reliable REST API clients.

By the ScreenshotNeo team1 October 20267 min read

Direct answer: design a RESTful web API by modeling a stable domain contract as resources, assigning each resource a durable URI, using HTTP methods and status codes according to RFC 9110, and documenting representations, errors, collection behavior, and compatibility rules. JSON, plural nouns, and CRUD routes alone do not make an API RESTful.

HTTP provides a uniform interface for interacting with a resource by transferring representations. Your API should let clients understand what a request targets, what it changes, whether it is safe to retry, and how to discover the result without knowing your database schema.

1. Start with the domain contract

List the concepts a client needs: for an issue tracker, these might be projects, issues, comments, and users. Define identity, relationships, lifecycle, and ownership before choosing routes. Keep the public model separate from tables and internal services so storage changes do not silently break clients. See Microsoft’s API design guidance.

Resource checklist

  • What is the resource’s stable identifier?
  • Which fields are writable, read-only, computed, or sensitive?
  • Which relationships need links or embedded summaries?
  • What states and transitions are valid?
  • Which operations fit HTTP methods, and which are genuine domain commands?

2. Choose resource URIs

Use nouns for resources and let the method express the operation:

GET    /v1/projects
POST   /v1/projects
GET    /v1/projects/{projectId}
PATCH  /v1/projects/{projectId}
DELETE /v1/projects/{projectId}
GET    /v1/projects/{projectId}/issues
POST   /v1/projects/{projectId}/issues
GET    /v1/projects/{projectId}/issues/{issueId}

Choose identifiers that remain valid if an item moves between partitions. Keep paths predictable and document case sensitivity. URI style is a consistency choice; HTTP method semantics come from the standard.

Nested versus top-level resources

Nest a child when the parent scopes its identity and authorization, such as /projects/{id}/issues. Use a top-level /issues/{id} when clients routinely address an issue independently. Support both only when semantics and authorization are identical.

3. Apply HTTP methods correctly

Method Typical use Safety and idempotency
GET Retrieve a representation Safe and idempotent
POST Create in a collection or start a command Not inherently idempotent
PUT Replace a representation at a known URI Idempotent
PATCH Apply a partial change Depends on patch semantics
DELETE Remove or deactivate a resource Idempotent outcome
HEAD Retrieve headers without a body Safe and idempotent

Safe means the client does not request a state change. Idempotent means repeating the same request has the same intended effect as sending it once. These properties affect retries, caches, and intermediaries. Do not tunnel every operation through POST /doSomething; use a command endpoint only when the operation cannot be expressed as a resource state change.

PUT, PATCH, and conditional updates

Define whether PUT requires a complete replacement and whether omitted fields reset to defaults. For PATCH, document the media type and semantics. Add If-Match with an ETag to prevent lost updates:

PATCH /v1/projects/p_123 HTTP/1.1
If-Match: 'project-v7'
Content-Type: application/merge-patch+json

{'name':'Payments'}

Return 412 Precondition Failed when the ETag no longer matches. Return 409 Conflict for a domain conflict such as a duplicate slug.

4. Define representations and headers

Document request and response media types, required fields, nullability, enum values, date formats, and maximum sizes. Use Content-Type for the request body and Accept for the preferred response type. Include an ETag for cache validation and Location when POST creates a resource.

HTTP/1.1 201 Created
Location: https://api.example.com/v1/projects/p_123
ETag: 'project-v7'
Content-Type: application/json

{'id':'p_123','name':'Payments','created_at':'2026-10-01T12:00:00Z'}

Use UTC timestamps, state whether unknown response fields may be ignored, and keep sensitive fields out of representations by default. Offer explicit sparse-field parameters when clients need smaller shapes.

5. Standardize errors

Every error needs a stable machine-readable code, a human-readable message, and enough context to fix the request without exposing secrets. RFC 9457 problem details is a useful media type.

HTTP/1.1 422 Unprocessable Content
Content-Type: application/problem+json

{'type':'https://api.example.com/problems/validation-error','title':'Request validation failed','status':422,'detail':'One or more fields are invalid','errors':[{'field':'name','code':'required'}]}

Common status choices include 400 malformed syntax, 401 missing or invalid authentication, 403 authenticated but forbidden, 404 unknown resource, 405 unsupported method, 409 state conflict, 412 failed precondition, 413 body too large, 415 unsupported media type, 422 semantically invalid content, 429 rate limited, and 5xx server or upstream failure. Do not return 200 for a failed operation.

6. Design collections, filtering, and pagination

Specify default ordering, maximum page size, and consistency during traversal. Cursor pagination is safer than offsets when rows are inserted while a client is paging.

GET /v1/issues?status=open&limit=50&after=opaque-cursor

{'data':[{'id':'i_101','title':'Retry webhook'}],'links':{'self':'/v1/issues?status=open&limit=50','next':'/v1/issues?status=open&limit=50&after=next-cursor'},'meta':{'next_cursor':'next-cursor'}}

Make cursors opaque and bind them to their sort definition. Validate filters and reject unknown fields instead of silently ignoring typos. Return totals only when they can be computed consistently.

7. Handle long-running work asynchronously

If a request cannot finish within normal client timeouts, create an operation resource. Return 202 Accepted and a Location header:

POST /v1/exports

HTTP/1.1 202 Accepted
Location: /v1/operations/op_456
Retry-After: 5

{'id':'op_456','status':'pending'}

Clients poll until the operation is succeeded or failed. Include a result link on success and structured error details on failure. For webhooks, sign payloads, include an event id, and make consumers idempotent.

8. Authentication, authorization, and limits

Use TLS everywhere. Choose an authentication scheme such as OAuth 2.0 bearer tokens or scoped API keys. Authenticate before authorization, enforce resource-level checks on every route, and avoid secrets in query strings. Document scopes, expiry, clock-skew tolerance, and revocation.

Publish rate-limit behavior, the limiting dimension, returned status, and whether Retry-After is present. Apply body-size, timeout, and concurrency limits. Log request ids and policy decisions without credentials or unnecessary personal data.

9. Version and evolve deliberately

Prefer additive changes: new optional fields and endpoints. Treat removing or changing a field’s meaning as breaking. For incompatible semantics, use an explicit strategy such as /v2 or a media-type profile, publish migration guidance, and provide a sunset period. Version the contract, not every internal service.

10. Document and test the contract

Publish an OpenAPI description with examples for success and important errors. Explain authentication, pagination, idempotency, retries, limits, and compatibility. Contract tests should verify status codes, headers, schemas, and authorization boundaries; integration tests should cover persistence and downstream failures.

11. Richardson maturity model: a teaching aid

  1. Level 0: one endpoint and POST for operations.
  2. Level 1: separate URIs for resources.
  3. Level 2: HTTP methods and status codes carry standard semantics.
  4. Level 3: hypermedia guides next actions.

Use these levels to explain progress, not to grade an API. A 2021 Delphi study asked eight industry experts to assess 82 design rules; rules associated with level 2 were considered critical in that study, while level 3 was considered less important. That finding is not a universal quality score.

12. Runnable client examples

cURL

curl --fail-with-body --request POST \
  --url https://api.example.com/v1/projects \
  --header 'Authorization: Bearer YOUR_TOKEN' \
  --header 'Content-Type: application/json' \
  --data '{"name":"Payments"}'

Python

import requests

base = 'https://api.example.com/v1'
headers = {'Authorization': 'Bearer YOUR_TOKEN'}
r = requests.post(f'{base}/projects', json={'name': 'Payments'}, headers=headers, timeout=30)
r.raise_for_status()
print(r.json())

Node.js

const base = 'https://api.example.com/v1';
const res = await fetch(`${base}/projects`, {
  method: 'POST',
  headers: { Authorization: 'Bearer YOUR_TOKEN', 'Content-Type': 'application/json' },
  body: JSON.stringify({ name: 'Payments' })
});
if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
console.log(await res.json());

13. Performance, reliability, and cost

  • Use conditional GETs with ETags and suitable cache directives.
  • Bound payloads with pagination, field selection, and compression.
  • Set connect, read, and total deadlines.
  • Retry only transient failures with exponential backoff and jitter.
  • Use idempotency keys for retryable POST operations.
  • Propagate correlation ids and measure latency by route and status class.
  • Protect dependencies with circuit breakers, bulkheads, and bounded queues.
  • Estimate cost from request volume, egress, storage, and downstream calls.

14. Troubleshooting

Symptom Cause Fix
404 on a valid-looking URI Wrong version, identifier scope, or route Check the documented base path and parent collection.
405 Method Not Allowed Method undefined for target Read the Allow header and use a documented method.
415 Unsupported Media Type Missing or incorrect Content-Type Send the endpoint’s required media type.
412 or overwritten data Missing or stale concurrency control Retry with the latest ETag and If-Match.
429 responses Quota or burst limit exceeded Honor Retry-After and reduce concurrency.
Duplicate records after retry POST retried without an idempotency key Support a key and return the original result for repeats.
Clients break after release Removed field or changed enum meaning Restore compatibility or publish a versioned migration.

15. Or skip the browser setup: use ScreenshotNeo for API documentation images

When your API guide needs current screenshots of a documentation page, ScreenshotNeo provides one GET request that returns PNG, JPEG, WebP, or PDF. It accepts cookie banners before capture and removes 60+ known consent platforms, newsletter popups, and chat widgets; each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers report the page verdict and billing result.

See the ScreenshotNeo API documentation for all options. cURL:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

Python:

import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)

Node.js:

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

It also offers an MCP server with take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients. The free plan includes 1,000 shots a month with no card; paid plans start at $5 for 3,000 shots.

Create your free ScreenshotNeo account and use the API in your documentation workflow.

FAQ

No. Hypermedia is a REST constraint, but many practical HTTP APIs stop at resource-oriented URIs and standard methods. Decide based on client discovery needs.

Should I use POST for updates?

Use PUT or PATCH when their semantics fit. Reserve POST for collection creation or commands that are not naturally state replacement.

Is GraphQL RESTful?

GraphQL uses a different interaction model centered on a query endpoint. It can coexist with REST, but should not be labeled REST solely because it runs over HTTP.

How many versions should I support?

Support the minimum overlap that lets clients migrate safely. Publish a deprecation date, telemetry for old versions, and an upgrade guide.

Further reading