URI Design Principles for Resource Naming in REST APIs



URI Design Principles for Resource Naming in REST APIs

Quick summary

A URI (Uniform Resource Identifier) is the “address” of a resource in your REST API. It should clearly describe what the resource is, not what action you are doing. URIs are nouns; HTTP methods (GET, POST, PUT, DELETE) are the verbs.

Good URIs follow a few simple rules: use plural nouns for collections (for example, /orders), shallow hierarchy (two or three levels deep), and query parameters for filtering, sorting, and pagination (for example, /orders?status=paid&page=2&size=20).

Bad URIs usually show patterns like verbs in the path (for example, /createOrder, /getPostList) or very deep nesting (for example, /shops/1/users/2/orders/3/items/4/shipments/5). These make your API hard to read, hard to maintain, and hard to evolve.

In this post we will apply these rules to three common domains: a board (posts and comments), an order, and a shipment. By the end, you will have a practical URI design that you can copy and adapt in your own REST API.

Table of contents


Key takeaways

  • URIs should describe resources using nouns. Use HTTP methods to express actions.
  • Use plural nouns for collections, such as /posts, /orders, /shipments.
  • Keep the URI hierarchy shallow, typically two or three levels deep at most.
  • Use query parameters for filtering, sorting, and pagination, not extra path segments.
  • Avoid verb-based URIs like /createOrder and excessive nesting like /shops/1/users/2/orders/3/items/4.
  • Design a clean set of URIs for board, order, and shipment that is consistent and predictable so frontend and backend teams can move faster.

1. What a URI does in a REST API

In a REST API, a URI identifies a resource. A resource can be a user, a post, an order, a shipment, and so on. The URI should answer the question “What are you talking about?” and not “What are you doing?”.

For example:

GET /orders/1001

This means: “Get the order whose identifier is 1001.” The action “get” comes from the HTTP method GET, not from the path. To update that order, you can call:

PUT /orders/1001

The URI is the same, but the HTTP method is different, so the action is different. Because of this, we do not need verbs like “get”, “create”, or “delete” in the URI itself.

2. Good URI rules: plural nouns, hierarchy, filters/sort/page

2.1 Use plural nouns for collections

In most APIs you will have two levels of URIs for each type of resource:

  • Collection URI: list of resources, for example /orders, /posts, /shipments.
  • Item URI: a single resource, for example /orders/1001, /posts/42, /shipments/555.

Using plural nouns for collections makes it easy to guess the URI even if you have never read the documentation. If your API uses plural forms for users (/users), do the same for every other resource. Consistency is more important than the exact word choice.

2.2 Keep a shallow hierarchy

It is natural to express relationships by nesting URIs:

  • /boards/10/posts — posts that belong to board 10
  • /posts/42/comments — comments that belong to post 42

This is fine when you have a strong ownership relationship. A post always belongs to one board, and a comment always belongs to one post. However, try not to go too deep. A good rule of thumb is to keep the depth at two or three levels, for example:

  • /boards/{boardId}/posts
  • /posts/{postId}/comments

If you see something like /shops/1/users/2/orders/3/items/4/shipments/5, this is a sign that your URI is carrying too much of your internal data model. It becomes hard to read, hard to debug, and hard to change later when relationships evolve.

2.3 Use query parameters for filtering, sorting, and pagination

When you want to list resources with conditions, use query parameters instead of extra path segments. Common patterns:

  • Filtering: /orders?status=paid&customerId=123
  • Sorting: /orders?sort=createdAt,desc
  • Pagination: /orders?page=1&size=20 or /orders?limit=20&offset=40

Using query parameters keeps the main URI simple and stable. You can add more filters later without changing the base path. It is also easier to generate from UI components such as dropdowns and search forms.

3. Anti-patterns: verb URIs and deep nesting

3.1 Verb-based URIs

Verb-based URIs look like this:

  • /createOrder
  • /getOrderList
  • /deleteUser

At first, they feel familiar because they look like function names in code. However, they break REST principles. Actions should come from HTTP methods. A better design is:

  • POST /orders — create a new order
  • GET /orders — get a list of orders
  • DELETE /users/{userId} — delete a user

With this style, you have fewer endpoints to manage, and you can reuse the same URI for different actions simply by changing the method.

3.2 Excessive nesting

Deeply nested URIs look like this:

/shops/1/users/2/orders/3/items/4/shipments/5

This is hard for humans to read and easy to break when your domain model changes. Maybe later an order belongs to a business account instead of a user, or shipments are handled by a separate logistics service. If your URI encodes all these relationships, you will have to redesign many endpoints.

Instead, try to focus each URI on one main resource and use IDs or query parameters to express relationships:

  • /orders/3 — the main view of order 3
  • /shipments?orderId=3 — shipments related to order 3
  • /users/2/orders — orders placed by user 2 (two-level nesting is fine)

4. Practice: URI design for board, order, and shipment

4.1 Board, post, and comment

Imagine a simple discussion board with boards, posts, and comments. A practical URI design could be:

  • List boards: GET /boards
  • Get a board: GET /boards/{boardId}
  • List posts in a board: GET /boards/{boardId}/posts
  • Get a single post: GET /posts/{postId}
  • Create a post in a board: POST /boards/{boardId}/posts
  • List comments on a post: GET /posts/{postId}/comments
  • Create a comment: POST /posts/{postId}/comments

Here, the hierarchy expresses a clear ownership relation: a post belongs to a board, and a comment belongs to a post. However, for updating or deleting a comment you do not need the parent path, so you can simplify:

  • Update a comment: PUT /comments/{commentId}
  • Delete a comment: DELETE /comments/{commentId}

For searching posts, use query parameters instead of creating many different paths:

  • GET /posts?boardId=10&authorId=123&keyword=uri
  • GET /posts?boardId=10&sort=createdAt,desc&page=1&size=20

4.2 Order

Next, consider an order system for an online shop. A simple set of URIs might be:

  • List orders: GET /orders
  • Get an order: GET /orders/{orderId}
  • Create an order: POST /orders
  • Update an order (for example, status or address): PATCH /orders/{orderId}

Filtering and sorting can be handled through query parameters:

  • Orders for a customer: GET /orders?customerId=123
  • Orders by status: GET /orders?status=paid
  • Orders by status and date range: GET /orders?status=shipped&from=2024-01-01&to=2024-01-31
  • Pagination and sort: GET /orders?sort=createdAt,desc&page=1&size=50

4.3 Shipment (delivery)

Shipments are closely related to orders but are still their own resource. You may want to search shipments on their own, regardless of the order or customer. A flexible design could be:

  • List shipments: GET /shipments
  • Get a shipment: GET /shipments/{shipmentId}
  • List shipments for an order: GET /orders/{orderId}/shipments
  • Filter shipments by order: GET /shipments?orderId={orderId}
  • Filter shipments by status: GET /shipments?status=in_transit

This pattern gives both order-centric and shipment-centric views of your data without building very deep hierarchies. Internally, you still keep a strong relation between orders and shipments through database keys and business logic.


Code examples

# Get the latest 20 posts from board 10, sorted by creation time (newest first)
curl -X GET "https://api.example.com/boards/10/posts?sort=createdAt,desc&page=1&size=20" \
  -H "Accept: application/json"

# Get paid orders for customer 123, with basic pagination

curl -X GET "[https://api.example.com/orders?customerId=123&status=paid&page=1&size=50](https://api.example.com/orders?customerId=123&status=paid&page=1&size=50)" 
-H "Accept: application/json"

# Get shipments related to order 1001

curl -X GET "[https://api.example.com/shipments?orderId=1001](https://api.example.com/shipments?orderId=1001)" 
-H "Accept: application/json"

These examples show how to combine noun-based URIs with HTTP methods and query parameters. All three use GET because they only read data and do not modify anything.

Comparison table: good vs bad URIs

The table below compares typical good and bad URI patterns. The HTML is adjusted so long URIs wrap inside the cell instead of overflowing to the right.

Category Good URI example Bad URI example Reason
Collection /orders /getOrders Use nouns for resources. The HTTP method already describes the action.
Item /orders/1001 /order?id=1001 Put the identifier in the path so URIs are more readable and bookmarkable.
Filtering /orders?status=paid&customerId=123 /orders/status/paid/customer/123 Use query parameters for search conditions instead of extra path segments.
Hierarchy /boards/{boardId}/posts /shops/{shopId}/users/{userId}/orders/{orderId}/items/{itemId}/shipments/{shipmentId} Keep hierarchy shallow. Deep nesting leaks internal structure and is hard to maintain.
Actions POST /orders /createOrder Use HTTP methods to express create, update, and delete operations.

Step-by-step guide to apply this design

  1. List your business resources
    Start by listing all core concepts in your system such as boards, posts, comments, customers, orders, and shipments. Group them by type, and decide which ones deserve their own top-level collections.
  2. Define collection and item URIs
    For each resource, define a plural collection URI (for example, /orders) and an item URI (for example, /orders/{orderId}). Use a consistent naming style such as kebab-case or snake_case for multi-word names.
  3. Add limited hierarchy where ownership is clear
    Use nested URIs only when there is a strong ownership relation. For example, /boards/{boardId}/posts and /posts/{postId}/comments are reasonable. Avoid going beyond two or three levels of depth.
  4. Design filtering, sorting, and pagination
    Decide on common query parameters (status, customerId, sort, page, size, and so on) and reuse them across resources. This makes your API easier to learn and also easier to document.
  5. Eliminate verb-based and deeply nested URIs
    Find endpoints like /createOrder or those with very long paths. Redesign them using noun-based URIs, shallow hierarchy, and HTTP methods. Plan a migration path so existing clients can move gradually.
  6. Review with both backend and frontend teams
    Share the URI design with everyone who will consume the API. Ask whether the structure feels predictable, whether common use cases are easy to express, and whether the naming is clear for English-speaking users.

Things to think about next

  • Versioning strategy — consider how you will introduce breaking changes. Options include path-based versioning (for example, /v1/orders) or header-based versioning. Decide early to avoid confusion.
  • Consistency across teams — if you have multiple microservices, agree on common conventions for URIs, query parameters, and error formats so clients see one unified API style.
  • Documentation and examples — even with a clean URI design, well-written examples in OpenAPI/Swagger or API reference pages will help new developers adopt your API quickly.
Reactions

댓글 쓰기

0 댓글