---
title: "05 — Building RESTful APIs: Resources, CRUD, Naming, Versioning"
uid: building-restful-apis
tags: ["uri-design", "versioning", "rest", "crud", "resource-modeling", "roadmap:api-design", "naming"]
excerpt: "RESTful design stops being an HTTP checklist once you see it as modeling: your domain as resource nouns, CRUD as the operations, consistent naming, and versioning so it can evolve without breaking clients."
date: 2026-08-13T03:28:34+0000
source: https://www.aveshina.my.id/en/blog/building-restful-apis
---

"Use HTTP verbs correctly" was the extent of my REST knowledge, and it produced endpoints that technically obeyed the verbs and still felt arbitrary. The model that finally clicked: **a REST API is a model of your domain expressed as resource nouns, manipulated by the four CRUD operations, named consistently enough that a consumer can predict endpoints they've never seen, and versioned so it can change without breaking anyone.** [1] Once I saw it that way, "RESTful design" stopped being a checklist of HTTP trivia and became a small set of modeling decisions.

The thread connecting these nodes is that they're all consequences of one principle: **the URL names a resource, the verb names the operation, and the contract should be predictable enough to guess.** [2] Every naming and versioning rule I follow is in service of that predictability.

## Resource modeling: design the nouns first

Before any endpoint, the work is deciding what the **resources** are [1]. A resource is a thing in your domain — an order, a user, a document, a comment. Good resource modeling is a modeling exercise, not an HTTP exercise: list the nouns in your domain, decide which are top-level (worth their own /orders collection) versus nested (an order's items live under /orders/{id}/items), and drop the ones that are really just fields on another resource.

The mistake I made early was modeling endpoints around UI screens ("a /dashboard endpoint") or around actions ("a /resetPassword endpoint"). Both leak the wrong abstraction. Resources are domain nouns; screens and actions get assembled *from* resource calls. If the only way to describe an endpoint is a verb, it's probably a sub-resource or an RPC-shaped exception — and those should be rare in a REST design.

## CRUD: the four operations, mapped to verbs

Almost everything you do to a resource is one of **Create, Read, Update, Delete** [3]. REST maps these directly onto HTTP verbs, and the mapping is the spine of the whole style:

```figure
<svg viewBox="0 0 680 240" xmlns="http://www.w3.org/2000/svg" class="my-6 w-full max-w-2xl" role="img" aria-label="CRUD operations mapped to HTTP methods on an /orders resource. Four rows: Create=POST /orders (201), Read=GET /orders/42 (200), Update=PUT /orders/42 (200), Delete=DELETE /orders/42 (204).">
  <g font-family="ui-sans-serif, system-ui, sans-serif" text-rendering="geometricPrecision">
    <text x="340" y="24" font-size="12" font-weight="700" fill="#475569" text-anchor="middle">CRUD ↔ HTTP, on the /orders resource</text>

    <!-- rows -->
    <g font-family="ui-monospace, monospace" font-size="12">
      <!-- Create -->
      <rect x="30" y="44" width="100" height="36" rx="8" fill="#dcfce7" stroke="#16a34a" stroke-width="1.3"/>
      <text x="80" y="67" font-weight="700" fill="#052e16" text-anchor="middle">Create</text>
      <rect x="150" y="44" width="70" height="36" rx="8" fill="#16a34a"/>
      <text x="185" y="67" font-weight="700" fill="#fff" text-anchor="middle">POST</text>
      <text x="240" y="67" fill="#1e1b4b">/orders</text>
      <text x="430" y="67" fill="#64748b">→ 201 Created</text>

      <!-- Read -->
      <rect x="30" y="92" width="100" height="36" rx="8" fill="#e0e7ff" stroke="#6366f1" stroke-width="1.3"/>
      <text x="80" y="115" font-weight="700" fill="#1e1b4b" text-anchor="middle">Read</text>
      <rect x="150" y="92" width="70" height="36" rx="8" fill="#6366f1"/>
      <text x="185" y="115" font-weight="700" fill="#fff" text-anchor="middle">GET</text>
      <text x="240" y="115" fill="#1e1b4b">/orders/42</text>
      <text x="430" y="115" fill="#64748b">→ 200 OK</text>

      <!-- Update -->
      <rect x="30" y="140" width="100" height="36" rx="8" fill="#fef9c3" stroke="#ca8a04" stroke-width="1.3"/>
      <text x="80" y="163" font-weight="700" fill="#422006" text-anchor="middle">Update</text>
      <rect x="150" y="140" width="70" height="36" rx="8" fill="#ca8a04"/>
      <text x="185" y="163" font-weight="700" fill="#fff" text-anchor="middle">PUT</text>
      <text x="240" y="163" fill="#1e1b4b">/orders/42</text>
      <text x="430" y="163" fill="#64748b">→ 200 OK</text>

      <!-- Delete -->
      <rect x="30" y="188" width="100" height="36" rx="8" fill="#fee2e2" stroke="#dc2626" stroke-width="1.3"/>
      <text x="80" y="211" font-weight="700" fill="#7f1d1d" text-anchor="middle">Delete</text>
      <rect x="150" y="188" width="70" height="36" rx="8" fill="#dc2626"/>
      <text x="185" y="211" font-weight="700" fill="#fff" text-anchor="middle">DELETE</text>
      <text x="240" y="211" fill="#1e1b4b">/orders/42</text>
      <text x="430" y="211" fill="#64748b">→ 204 No Content</text>
    </g>
  </g>
</svg>
```

A few conventions that fall out of this mapping [3]:

- **Create** is POST to the *collection* (/orders); the server assigns the ID and returns 201 with a Location header pointing at the new resource.
- **Read** is GET — to the collection for a list, or to /orders/{id} for one. These are two distinct operations.
- **Update** is PUT (full replace) or PATCH (partial). The distinction matters: PUT to /orders/42 should replace the whole resource; PATCH changes only the fields sent.
- **Delete** is DELETE to the specific resource, conventionally returning 204 No Content (nothing to return) rather than 200.

The discipline is treating this mapping as the default and reaching for exceptions deliberately. An endpoint that doesn't fit CRUD (e.g., "cancel an order," "send an invoice") is sometimes better modeled as a sub-resource action (POST /orders/42/cancel) than forced into the wrong verb. The point is to notice when you're leaving the CRUD spine and decide consciously.

## URI design and naming conventions

Once resources exist, the URL shape follows rules that exist purely for predictability [2][4]:

- **Plural nouns for collections** — /users, not /user. A single user is /users/42.
- **Lowercase, kebab-case in paths** — /password-resets, not /passwordResets or /PasswordResets.
- **No verbs in paths** — the verb is the HTTP method. /users/42 not /getUser/42.
- **Hierarchy expresses nesting** — /users/42/orders means "orders belonging to user 42." Nest only when the parent is a true scope; avoid more than two levels.
- **Consistent field casing in JSON** — pick camelCase or snake_case and apply it everywhere. Mixed casing in one API is the fastest way to erode trust.

The payoff of consistency: a consumer who has seen /users and /orders can correctly guess /comments without reading docs. That predictability *is* the usability of a REST API [4]. An inconsistent API forces the consumer to memorize every endpoint; a consistent one lets them infer.

## Versioning: the contract can change

An API is a contract, and contracts that can't evolve die. **Versioning** is how you change the contract without breaking existing clients [5]. The roadmap highlights three strategies, each with tradeoffs:

- **URI versioning** — /v1/orders, /v2/orders. Blunt, obvious, cache-friendly. The downside is ugly URLs and the temptation to bump versions too often. This is the most common choice in practice.
- **Header versioning** — same URL, version in a custom header (Accept-version: v2). Cleaner URLs, but invisible in casual inspection and harder to test in a browser.
- **Media type versioning** — version baked into the Accept header (Accept: application/vnd.example.v2+json). Most "RESTful" in theory, least ergonomic in practice.

The rule I hold: **a breaking change requires a version bump; a non-breaking change (adding a field, adding an endpoint) does not.** Most evolutions should be additive. When a real breaking change is unavoidable, ship the new version, give consumers a deprecation window measured in months not days, and document the migration path. The version is a promise that the old contract still works.

## Simple JSON APIs

Underneath all of this, the wire format is almost always **JSON** [6]. A well-shaped REST response is a JSON object that consistently represents a resource — the same fields in the same casing every time, errors in a conventional shape, relationships either embedded or linked. The **JSON:API** spec formalizes this, and even if you don't adopt it wholesale, its conventions (a data envelope, a consistent errors array, sideloaded relationships) are good defaults to borrow. The discipline is treating the JSON shape as part of the contract — once clients depend on a field, renaming or removing it is a breaking change.

## How I use this

When designing a new endpoint, I work noun-first. I name the resource, decide its CRUD surface, place it in the URL hierarchy where a consumer would expect to find it, and only then write the handler. When reviewing an existing API, the first thing I check is consistency — plural nouns, casing, status codes — because inconsistency is usually the loudest signal of an API that was assembled rather than designed. And when a change is needed, I ask the additive-or-breaking question first: additive changes ship immediately, breaking changes go through a version bump and a deprecation window. The contract is the part I'm promising; the noun-first shape is what makes that promise predictable.

## References

[1] Integrate.io, "How to Make a RESTful API," 2024. [Online]. Available: [https://www.integrate.io/blog/how-to-make-a-rest-api/](https://www.integrate.io/blog/how-to-make-a-rest-api/)

[2] CSS-Tricks, "Guidelines for URI Design," 2024. [Online]. Available: [https://css-tricks.com/guidelines-for-uri-design/](https://css-tricks.com/guidelines-for-uri-design/)

[3] Palantir, "Rethinking CRUD For REST API Designs," 2023. [Online]. Available: [https://blog.palantir.com/rethinking-crud-for-rest-api-designs-a2a8287dc2af](https://blog.palantir.com/rethinking-crud-for-rest-api-designs-a2a8287dc2af)

[4] restfulapi.net, "REST API URI Naming Conventions and Best Practices," 2024. [Online]. Available: [https://restfulapi.net/resource-naming/](https://restfulapi.net/resource-naming/)

[5] Postman, "What is API Versioning?," 2024. [Online]. Available: [https://www.postman.com/api-platform/api-versioning/](https://www.postman.com/api-platform/api-versioning/)

[6] JSON:API, "Specification for Building APIs in JSON," jsonapi.org, 2024. [Online]. Available: [https://jsonapi.org/](https://jsonapi.org/)

```quiz
Q: You need to create a new order. The idiomatic REST call is…
- GET /createOrder
- POST /orders
- POST /orders/{id}
correct: 1
explain: Create maps to POST on the collection. The client POSTs to /orders; the server assigns the id and returns 201 with a Location header.

Q: What's the difference between PUT and PATCH on /orders/42?
- Nothing, they are interchangeable
- PUT replaces the whole resource; PATCH changes only the fields sent
correct: 1
explain: PUT is a full replacement; PATCH is partial. Sending the wrong one can wipe fields you didn't mean to clear.

Q: Which URL design follows REST naming conventions?
- /getUser?id=42
- /users/42
- /User/42
correct: 1
explain: Plural lowercase noun for the collection, id as a path param. No verbs — the HTTP method is the verb.

Q: A non-breaking change to your API (adding a new optional field to a response) requires…
- a new major version (/v2)
- no version bump — additive changes don't break existing clients
correct: 1
explain: Only breaking changes (removing/renaming fields, changing types) require a version bump. Additive changes ship without one.

Q: Why prefer plural nouns for collection endpoints (/users, not /user)?
- It signals the endpoint returns a collection and keeps the whole API predictable
- Plurals are faster to route
correct: 0
explain: Consistency and predictability. A consumer who sees /users and /orders can correctly guess /comments. The convention exists for humans, not routers.
```
