AlgoMaster Logo

Designing Read APIs

High Priority15 min readUpdated September 13, 2026
Listen to this chapter
Unlock Audio

Reading a resource may seem like a simple lookup, but what the API returns depends on who is asking and what they expect to find. A customer opening a saved reading list expects its current name and contents. A support agent opening the same address may have different permissions. Someone following an old bookmark needs to understand whether the list is unavailable or the service has failed.

A read API defines what callers can observe about a resource, how they find it, and what the returned data means.

This chapter uses a fictional bookstore API to explain individual and collection reads, representation boundaries, access checks, errors, and freshness. The examples use JSON over HTTPS at https://api.bookstore.example.

1. Single-Resource Retrieval

Use a single-resource endpoint when the client already has a resource's address or identifier. For example, /reading-lists/list_801 identifies one list, while /reading-lists identifies a collection.

GET retrieves a selected representation of the target resource. A representation is the information the API returns about that resource in a particular format. It is not necessarily every field the service stores internally or every related record.

The owner requests a list:

The service returns the list's metadata:

This list belongs to cust_42; the service created it at the displayed timestamp. The created_at value describes the resource's creation, not the time of this read. A field should retain the same meaning wherever the API exposes it.

This endpoint always returns one object on an ordinary successful GET. It does not alternate between an object and an array, and it does not return 200 with null when the list is unavailable. A stable success shape gives clients a clear parsing contract.

The GET request has no body. Its path selects the list, and Accept expresses the desired response format. Do not design an ordinary read endpoint that depends on a GET body; such content has no generally defined semantics and can have interoperability problems.

Each JSON body occupies one line without a trailing newline. Credentials are placeholders.

2. Collection Reads

A collection read answers a different question: which resources belong to the requested collection and are visible under its rules?

For this API, GET /reading-lists returns lists the authenticated customer owns. It does not mean every list in the bookstore or every public list the customer could open. Public discovery would need a separately defined collection or search contract.

Collection responses use an object containing items and next_cursor. items is always an array of reading-list representations. next_cursor is either an opaque continuation value, which the client returns unchanged to request more results, or null when there are no more results for this traversal. These names are application conventions.

The customer requests a page:

Assume this customer owns only the list in this response:

An authenticated customer with no lists receives the same envelope:

An empty result is successful retrieval of an empty collection. Returning 404 would confuse “this collection has no members” with “the requested target is unavailable.” Returning 204 would force clients to handle an additional success shape without useful collection metadata.

Keep collection work bounded. This API defaults to 20 items, permits limit values from 1 through 100, and rejects invalid limits. The limit is an upper bound, not a promise to fill every page. The API orders lists by created_at descending and then id ascending to resolve ties. Continuation behavior must preserve a documented ordering policy; clients should not infer one from storage order.

An object envelope leaves room for collection metadata without changing the top-level type. Do not include an exact total automatically: it may be expensive, and it must obey the same access rules as the returned items. The result count must never reveal private records the collection excludes.

3. Representation Boundaries

A reading list can contain thousands of books. Returning all of them whenever a client needs the list's name would make a small metadata read grow with the customer's history.

This API keeps the list representation focused on metadata and exposes membership through /reading-lists/list_801/entries. That child collection has its own page-size limit. A membership item can identify a book without embedding the entire book, its author, every review, and all inventory locations.

The diagram shows the boundaries between these reads:

Each response has a defined size and purpose. Navigating to related data is explicit, so one list read does not accidentally become a full account export.

A small embedded summary can still be useful. A list item might include a book's title to avoid an extra request for a common display. Define whether that title reflects current catalog data or a saved snapshot. Using the same field name for both meanings would make clients display stale information without realizing it.

If clients need optional field selection or relationship expansion, define an allowlist and limits. A field selector must not expose internal fields, and an expansion must preserve the related resource's permissions. Adding arbitrary recursive expansion makes response cost and authorization difficult to predict.

Also define absence precisely. This list endpoint omits membership because a separate response provides it. The omission does not mean there are no books. An empty items array in the authorized membership collection does mean the service returned no memberships. Use null only where the field contract gives it a specific meaning, rather than as a substitute for missing, hidden, or failed data.

4. Access and Missing Resources

Knowing an ID is not permission to read its resource. This bookstore requires authentication for private lists and checks whether the caller is the owner or an authorized support user. Public-list access can use a separate, documented representation that excludes owner-only information.

For private lists, the API deliberately gives the same response when the list does not exist and when the caller may not know it exists. Another authenticated customer requests list_801:

The API conceals it:

This concealment policy uses HTTP's allowance for a 404 response when the server is unwilling to disclose a resource's existence. Do not undermine it with an error field saying the list belongs to another customer.

The following diagram assumes valid credentials and a well-formed identifier. It expresses the public decision, not a required order of database queries:

Access checks apply to every representation, including collection entries, embedded objects, and metadata-only reads. Filtering private records only after computing counts or building related objects can still disclose information.

Authentication failures are distinct. For example, the following request omits credentials. The service responds with 401 Unauthorized:

Where existence is safe to disclose but the API prohibits a read, an API can use 403 Forbidden. Choose the disclosure policy deliberately rather than returning different statuses according to whichever internal check fails first.

A 404 does not establish permanent removal. 410 Gone can express likely permanent unavailability when the service knows that and is willing to disclose it. This reading-list API uses its generic 404 policy for deleted private lists as well.

For nested reads, verify the parent relationship. A list item ID that exists under another list must not become accessible merely because a handler ignores the parent ID in the URL.

5. Request Validation

Read requests still have input contracts. Validate identifier syntax, query parameter names, values, and combinations before running expensive work. Reject a bad selector rather than silently returning a different result set.

For example, this API accepts limit as a decimal integer from 1 through 100. A client sends zero:

The server rejects the parameter:

The status and envelope are this API's policy. It also rejects unknown query parameters and repeated limit parameters, so ?limit=20&limit=80 has no ambiguous interpretation. A malformed continuation value is an input error, not an empty collection.

A well-formed ID that resolves to nothing is different from an ID that violates the published grammar. Keep those behaviors consistent with the API's validation and concealment policies.

Do not place credentials or sensitive customer information in query strings. URLs commonly reach access logs, browser history, and monitoring systems. Accepting input on a read endpoint does not make it suitable for inclusion in a URL.

6. Safe Reads and Metadata

GET is a safe method: the caller is requesting retrieval, not a business state change. Opening a book page must not reserve stock, consume a discount, or mark an order as shipped. Automated clients can follow links or repeat reads without a person explicitly choosing those effects.

Operational logging and metrics can still occur. Safety concerns the requested semantics; it does not require a server to perform literally zero internal writes. If marking a notification as read is a meaningful user action, expose that change through a separate operation.

A repeated GET can return different data because other actions changed the resource. Idempotency does not promise identical response bytes or freeze the resource between reads.

When the endpoint supports it, HEAD lets a client request representation metadata without response content. Apply the same access policy as GET. HEAD is useful for metadata checks, but it does not reserve the resource or guarantee that a later GET will succeed. It also does not guarantee cheap execution if selecting the representation requires expensive work.

Avoid an automatic HEAD before every GET when the client needs the body anyway. It adds another request and leaves time for the resource or its permissions to change between the two.

7. Freshness and Read Consistency

Decide what “current” means for each read. A customer who just created a list needs different guarantees from someone browsing a search index that updates periodically.

This bookstore promises that an authorized read of the returned list address can retrieve a successfully created list immediately. This is a read-after-write guarantee: a completed write is visible to the subsequent read to which that promise applies. It does not imply that all collections, search results, or analytics update at the same moment.

The diagram contrasts direct retrieval with a discovery view the service updates asynchronously:

These paths can have different visibility times. Route reads or coordinate storage so the direct-read guarantee holds; do not tell clients to treat intermittent replica-generated 404 responses as normal after promising immediate retrieval.

HTTP caching is a separate layer. The private-list examples use Cache-Control: no-store, which instructs HTTP caches not to store or reuse those exchanges. That directive does not make a database replica current, and it does not replace authorization or encrypted transport.

Other resources may permit caching. Set an explicit policy for both successes and errors. A cache may still report a resource as missing after its creation, or retain a response after access rules change. If the API supports validators such as entity tags, bind them to the selected representation and evaluate access before reporting that a client's saved copy remains valid.

A read also provides an observation, not permission for a future action. Seeing available stock does not reserve it. An operation that consumes stock must evaluate its own rules when it executes.

8. Dependency Failures and Incomplete Data

A read can fail even when the target resource exists. The database might be unavailable, or a required downstream service might time out. Do not turn those failures into 404 or an empty collection: clients could conclude that records have disappeared.

For a temporarily unavailable reading-list store, this API returns 503 Service Unavailable with a structured service error. Clients can distinguish failure to retrieve data from a successful read showing no members. Exact status selection depends on the failure and the server's role.

Keep required response fields dependable. If an endpoint promises both order details and a shipment status, a failed shipment lookup must not silently become "shipment_status":null when null means “not shipped.” That would manufacture a business fact from an infrastructure failure.

There are two reasonable designs: fail the read when required data is unavailable, or explicitly define a response that can describe unavailable components. Choose according to what clients can safely do with incomplete information. The service may omit optional enrichment only under a documented contract that distinguishes omission from a real empty or absent value.

Returning reading-list metadata separately from recommendations also helps. If clients fetch recommendations separately, a recommendation service outage need not make the metadata request fail. Include dependencies because the read requires their information, and make their failure behavior part of the contract.

Summary

Give single-resource and collection reads clear targets, stable response shapes, and explicit access rules. Distinguish empty results, unavailable resources, invalid requests, and service failures so clients can respond correctly.

Keep representations bounded, preserve safe retrieval behavior, and define freshness according to the decisions callers need to make. A useful read API returns information whose scope, completeness, and meaning remain dependable as the system changes.