Collection vs Singleton Resource Naming

This page sits under Resource Modeling Best Practices in the API Design Fundamentals & Architecture reference. Most resources are members of a collection, a few genuinely are not, and confusing the two produces URL shapes that fight the HTTP method semantics for the life of the API.

The decision trigger

Situation Model as
Zero or more of these exist per parent collection: /orders, /orders/{id}
Exactly one exists per parent, always singleton: /accounts/{id}/settings
One exists per authenticated caller singleton alias: /me
One exists now but may become many collection — the migration later is breaking
A computed view over other resources singleton: /reports/monthly-summary
A command rather than a thing neither — see the sub-resource verb pattern

The fourth row is the expensive one. “There is only one billing address per customer” holds until the day it does not, and turning /customers/{id}/address into a collection changes every client’s URL construction. If plurality is plausible in the domain, model the collection now and let it hold one member.

Spec snippet: both shapes

# openapi.yaml (OpenAPI 3.1.0)
paths:
  # ── Collection ─────────────────────────────────────────────
  /orders:
    get:
      operationId: listOrders          # 200 with an envelope
    post:
      operationId: createOrder         # 201 + Location, server assigns the id
  /orders/{orderId}:
    get:
      operationId: getOrder            # 200 or 404
    put:
      operationId: replaceOrder        # full replacement
    patch:
      operationId: updateOrder         # partial update
    delete:
      operationId: deleteOrder         # 204, then 404 on subsequent GET

  # ── Singleton ──────────────────────────────────────────────
  /accounts/{accountId}/settings:
    get:
      operationId: getAccountSettings  # 200 — always exists
    put:
      operationId: replaceAccountSettings   # 200, not 201: no creation
    patch:
      operationId: updateAccountSettings
    # No POST: there is nothing to create.
    # No DELETE: "no settings" is not a state this resource can be in.

  # ── Singleton alias for the authenticated principal ────────
  /me:
    get:
      operationId: getCurrentUser      # same schema as /users/{userId}

The comments encode the rule: a singleton has no POST because creation is meaningless, and its PUT returns 200 rather than 201 because nothing comes into existence. Getting these status codes right matters because clients branch on them — a 201 from a settings update tells a client a new resource was created and it should record the Location.

Method support for collections versus singletons A collection URL supports GET for listing and POST for creation, and its member URL supports GET, PUT, PATCH and DELETE. A singleton URL supports GET, PUT and PATCH only, with PUT returning 200 rather than 201 and no POST or DELETE. Collection Singleton /orders GET → 200 page envelope POST → 201 + Location /orders/{orderId} GET → 200 or 404 PUT → 200 (or 201 if client-assigned id) PATCH → 200 DELETE → 204, then 404 membership can grow, shrink and be filtered /accounts/{id}/settings GET → 200 (always exists) PUT → 200 — never 201 PATCH → 200 POST → 405 (nothing to create) DELETE → 405 unless absence is a state /me — alias, not a type same schema as /users/{id}; echoes the canonical id exactly one, for the life of the parent

Step-by-step

Step 1 — Test for plurality in the domain, not in today’s data

Ask whether two of the thing could ever exist under the same parent. A user has one primary email address today; the day they can have two, /users/{id}/email becomes a lie and /users/{id}/emails is a breaking change. If two are conceivable, model the collection now — a collection with one member costs one level of URL nesting and nothing else.

Step 2 — Name collections plurally and consistently

✓  /orders            /orders/{orderId}
✓  /customers         /customers/{customerId}
✓  /payment-methods   /payment-methods/{paymentMethodId}

✗  /order/{orderId}                 singular collection
✗  /customerList                    type name in the path
✗  /getOrders                       verb in a path
✗  /orders/{orderId}/getInvoice     verb in a sub-path

Consistency beats correctness in this specific argument: whichever convention you pick, a client author should never have to check the reference to know whether it is /order or /orders. The naming rules can be enforced mechanically — see Writing Custom Spectral Rules.

Step 3 — Make /me an alias, not a type

// me-route.ts — /me resolves to the same handler and the same representation
app.get("/me", requireAuth, async (req, res) => {
  const user = await users.byId(req.principal.userId);

  // The canonical id travels in the body AND in a Content-Location header,
  // so a client can cache, link or re-request by the stable URL.
  res.setHeader("Content-Location", `/users/${user.id}`);
  res.json(user);                         // identical schema to GET /users/{id}
});

Content-Location is the specified way to say “this representation also lives at that URL” (RFC 9110 §8.7). Without it, a client holding a /me response has no way to construct a link to the same user for another caller.

Step 4 — Decide what DELETE on a singleton means

A settings resource that always exists cannot be deleted in the ordinary sense. Three honest options:

Intent Correct expression
Reset to defaults PUT the default representation, or POST /settings/reset
Clear an optional value PATCH with null for that member
The resource genuinely can be absent DELETE204, then GET404

Choosing the third silently — DELETE returning 204 while GET still returns 200 with defaults — is the failure mode: the client is told the resource is gone and observes it is not.

Collection, singleton or command Ask whether more than one can exist per parent. If yes, model a collection. If no, ask whether it always exists. If yes, model a singleton. If it is an action rather than a thing, model a command sub-resource with POST. can two exist per parent? yes — or maybe collection /things · /things/{id} no is it a thing, or an action? a thing singleton /parent/{id}/settings an action command sub-resource POST /orders/{id}/cancel when unsure choose the collection — singleton → collection is a breaking change

Standard compliance

Concern Standard Clause Note
Resource identification RFC 9110 §3.1 A URI identifies a resource; plurality is convention
POST semantics RFC 9110 §9.3.3 Process the representation per the target’s semantics
PUT semantics RFC 9110 §9.3.4 Replace the target resource’s state
201 Created + Location RFC 9110 §15.3.2 Only when a new resource comes into existence
405 Method Not Allowed RFC 9110 §15.5.6 Must include Allow
Content-Location RFC 9110 §8.7 The canonical URL of the returned representation

Nothing in HTTP requires plural collection names — it is convention, not specification. What is specified is the method semantics, which is why the singleton’s method table matters more than its spelling.

The two shapes also deserve different caching policies, because their representations change for different reasons:

Cacheability of a singleton versus a collection A singleton has a stable URL and changes rarely, so strong validators pay off. A collection representation changes whenever any member changes and varies by query parameters. singleton collection stable URL, no query variance changes only when its own state changes strong ETag revalidates cheaply 304 on nearly every poll representation varies by query changes when ANY member changes validators are short-lived conditional requests miss often applying one policy to both wastes the singleton and frustrates the collection

Caching implications

A singleton’s URL is stable and its representation changes rarely, which makes it an excellent candidate for a strong ETag and conditional requests: a client polling /accounts/{id}/settings should get 304 almost every time. A collection URL is the opposite — its representation changes whenever any member changes, and it usually varies by query parameters — so validators there are weaker and shorter-lived. Applying the same caching policy to both wastes an opportunity on one and causes staleness complaints on the other. The mechanics are covered in ETag and If-None-Match Conditional Requests.

SDK / codegen downstream effect

  // Collection: id-bearing methods, list returns an envelope
  await client.orders.listOrders({ limit: 50 });
  await client.orders.getOrder({ orderId: "ord_1" });

- // Singleton modelled as a collection: an id nobody has
- await client.settings.getSettings({ settingsId: "?" });
+ // Singleton modelled correctly: no id parameter at all
+ await client.accounts.getAccountSettings({ accountId: "acc_1" });

Modelling a singleton as a collection produces a generated method demanding an identifier the caller cannot supply, which is usually worked around by passing a literal "default" or "current" — a string that then appears in logs, caches and support tickets forever. The naming rules that keep these method signatures readable are in Naming Operation IDs for Readable SDK Methods.

Common mistakes

Mistake Correct approach
Singular collection paths (/order/{id}) Plural for collections and members
Modelling a plausible-plural as a singleton Model the collection; it can hold one member
POST on a singleton Return 405 with Allow; there is nothing to create
PUT on a singleton returning 201 Return 200; no resource was created
/me as a separate resource type Alias the canonical member; send Content-Location
DELETE on a singleton that still returns 200 afterwards Either make absence a real state or use PUT/PATCH
Verbs in resource paths Nouns for resources; a command sub-resource for actions

FAQ

Should resource paths be plural or singular?

Plural for collections and their members — /orders and /orders/{orderId} — and singular only for genuine singletons such as /accounts/{id}/settings or an alias like /me. The reason is that /orders/{orderId} reads as “the order with this id within the orders collection”, which is exactly the relationship the URL expresses, and it leaves the collection URL free for listing and creation. The stronger argument, though, is consistency: whichever rule you adopt, apply it everywhere and enforce it in lint, because the real cost of mixed conventions is that every client author must check the reference for every endpoint.

Is a /me endpoint a good idea?

Yes, provided it is an alias rather than a separate resource type. Clients frequently need the authenticated principal before they know its identifier, and forcing a token-introspection round trip first is unhelpful. Return the same schema as GET /users/{userId}, include the canonical identifier in the body, and set Content-Location: /users/{id} so the client can construct links and cache entries against the stable URL. What to avoid is a /me representation that differs from the canonical one, because then clients must handle two shapes for the same entity and inevitably drift apart.

Can a singleton support DELETE?

Only if “absent” is a state the resource can genuinely be in, and you should decide that before implementing rather than after. For a settings document that always exists, DELETE usually means “reset to defaults”, which is expressed more honestly as a PUT of the default representation or an explicit POST /settings/reset command. The failure mode to avoid is a DELETE that returns 204 while a subsequent GET still returns 200 — the client has been told the resource is gone and can immediately observe that it is not, which undermines every other guarantee in the API.