URI Design Principles (Resource Naming): Plurals, Hierarchy, Filtering/Sorting/Pagination, and What to Avoid
In API design, URIs are not just “addresses.” They are a contract that shapes how developers discover, understand, and use your system. A well-designed URI set feels predictable: clients can guess endpoints, errors become easier to debug, and your API remains stable even as features grow. This post explains beginner-friendly URI rules (plural nouns, clean hierarchy, filtering/sorting/pagination), common anti-patterns (verb-based URIs, excessive nesting), and a hands-on exercise designing URIs for a discussion board, orders, and shipping.
Table of Contents
- Key Points (Quick Rules)
- Core URI Principles for Beginners
- Good URI Rules: Plurals, Hierarchy, Query Options
- Anti-Patterns to Avoid: Verbs and Over-Nesting
- Hands-On Practice: Board / Orders / Shipping URI Design
- Implementation Example (Routes + Query Parameters)
- Checklist Table (Do vs Don’t)
- Step-by-Step Workflow to Design URIs
- Extra Considerations
- Blog Optimization Info
Key Points (Quick Rules)
- Use nouns for resources (e.g.,
/orders), and use HTTP methods for actions (GET/POST/PATCH/DELETE). - Prefer plural collection names:
/users,/posts,/orders. - Design a shallow hierarchy: keep nesting limited and only when it clarifies ownership (e.g.,
/orders/{orderId}/items). - Use query parameters for filtering, sorting, and pagination:
?status=paid&sort=-createdAt&page=2&size=20. - Avoid verb-based URIs like
/createOrderor/getPosts. - Avoid “tunnel” URIs with deep nesting like
/users/1/teams/2/projects/3/tasks/4/comments.
Core URI Principles for Beginners
A resource is a “thing” your API exposes: a user, a post, an order, a shipment. A URI should name that thing in a consistent way. The operation you want to perform is expressed using HTTP methods:
GETreads resources (list or detail).POSTcreates a new resource inside a collection.PUTreplaces a resource (less common for partial changes).PATCHpartially updates a resource.DELETEremoves a resource.
This separation is the reason you should avoid embedding actions into the URI itself. If you keep URIs noun-based, your API becomes easier to extend: adding new filters, sort fields, or pagination options does not require inventing new endpoints.
Good URI Rules: Plurals, Hierarchy, Query Options
1) Prefer Plural Nouns for Collections
Collections represent lists. Plural names read naturally and avoid confusion:
- Good:
/posts,/orders,/shipments - Good (single resource):
/posts/{postId} - Avoid mixing:
/postfor listing and/postsfor detail (pick one pattern and stay consistent).
2) Use Hierarchy Only When It Represents Containment
Nesting is useful when the child resource “belongs to” the parent and the relationship is part of the meaning:
- Good:
/posts/{postId}/comments(comments are related to a post) - Good:
/orders/{orderId}/items(items belong to an order) - Potentially unnecessary:
/users/{userId}/orders(useful if you want “orders for a user,” but consider also supporting/orders?userId=...)
A practical rule: keep nesting to one level deep for most APIs. If you frequently need two or three levels, it may be a sign you should introduce top-level collections plus filtering.
3) Filtering: Use Query Parameters
Filtering changes which subset of a collection you retrieve. It belongs in the query string:
- Example:
GET /orders?status=paid - Multiple filters:
GET /orders?status=paid&minTotal=50&createdFrom=2026-01-01
Guideline: query parameter names should be predictable, stable, and documented (including allowed values and types).
4) Sorting: Use sort With a Clear Convention
Sorting should also use query parameters. A common convention is:
sort=createdAtfor ascendingsort=-createdAtfor descending
Example: GET /posts?sort=-createdAt
5) Pagination: Use page and size (or Cursor)
For beginners, page + size is easiest to implement and understand:
GET /posts?page=1&size=20GET /orders?status=paid&sort=-createdAt&page=2&size=50
For very large datasets or “infinite scroll” behavior, cursor-based pagination (e.g., cursor or after) can be more stable, but page/size is a solid starting point for many internal and public APIs.
Anti-Patterns to Avoid: Verbs and Over-Nesting
1) Verb-Based URIs
If you see verbs like “get,” “create,” “update,” “delete,” “ship,” “cancel,” or “approve” in your path, pause and reconsider. In REST-style design, the method expresses the action.
- Avoid:
/getOrders,/createPost,/deleteComment - Prefer:
GET /orders,POST /posts,DELETE /comments/{commentId}
2) Excessive Nesting
Deep nesting makes URIs hard to read and harder to evolve because the “path structure” becomes too rigid. If a resource can be identified on its own (by ID), consider giving it a top-level endpoint and using filters for relationships.
- Avoid:
/boards/1/posts/2/comments/3/likes/4 - Prefer:
/comments/{commentId}or/likes?commentId={commentId}
Hands-On Practice: Board / Orders / Shipping URI Design
A) Discussion Board URIs
Assume you have boards, posts, comments, and reactions (likes). A clean, beginner-friendly URI set might look like this:
GET /boards(list boards)GET /boards/{boardId}(board detail)GET /posts?boardId={boardId}&sort=-createdAt&page=1&size=20(posts in a board, filtered via query)POST /posts(create post; includeboardIdin the request body)GET /posts/{postId}(post detail)PATCH /posts/{postId}(edit post)DELETE /posts/{postId}(delete post)GET /posts/{postId}/comments(comments for a post)POST /posts/{postId}/comments(create comment under a post)DELETE /comments/{commentId}(delete a comment by ID)POST /posts/{postId}/reactions(add a reaction to a post)DELETE /reactions/{reactionId}(remove a reaction)
Notice the pattern: collections are plural, resources use IDs, and actions are handled by methods. Also notice a design choice: listing posts in a board uses /posts?boardId=.... This helps keep the hierarchy shallow while still enabling a “board posts” view.
B) Order URIs
Orders naturally include items and payments. A clean design:
GET /orders?status=paid&sort=-createdAt&page=1&size=20POST /orders(create an order)GET /orders/{orderId}GET /orders/{orderId}/itemsPOST /orders/{orderId}/items(add item)PATCH /orders/{orderId}(update order fields like address notes)
If you need “cancel” as a domain action, consider modeling it as a state update rather than a verb endpoint: PATCH /orders/{orderId} with a request body like {"status":"canceled"}, plus validation rules on the server.
C) Shipping/Delivery URIs
Shipping is often a separate lifecycle from orders, so it is common to treat shipments as their own top-level resource:
GET /shipments?orderId={orderId}(shipments for an order)POST /shipments(create shipment; includeorderIdin body)GET /shipments/{shipmentId}PATCH /shipments/{shipmentId}(update carrier, tracking number, status)GET /shipments/{shipmentId}/events(tracking events)
This avoids locking your shipping model into /orders/{orderId}/shipments/{shipmentId} everywhere. You can still offer that nested view if needed, but the top-level collection keeps the system flexible.
Implementation Example (Routes + Query Parameters)
The following example shows how you might implement these URI patterns with Express. It includes a consistent approach to filtering, sorting, and pagination on list endpoints.
import express from "express";
const app = express();
app.use(express.json());
// Example: GET /orders?status=paid&sort=-createdAt&page=2&size=20
app.get("/orders", async (req, res) => {
const status = req.query.status; // e.g., "paid"
const sort = req.query.sort || "-createdAt"; // default descending
const page = Math.max(parseInt(req.query.page || "1", 10), 1);
const size = Math.min(Math.max(parseInt(req.query.size || "20", 10), 1), 100);
// Convert sorting convention: "-createdAt" => { createdAt: "desc" }
const sortField = String(sort).startsWith("-") ? String(sort).slice(1) : String(sort);
const sortDir = String(sort).startsWith("-") ? "desc" : "asc";
// In a real app, build a database query here.
const filter = {};
if (status) filter.status = status;
const result = {
filter,
sort: { field: sortField, direction: sortDir },
pagination: { page, size, offset: (page - 1) * size },
data: []
};
res.json(result);
});
// Nested sub-resource example: GET /orders/:orderId/items
app.get("/orders/:orderId/items", async (req, res) => {
const { orderId } = req.params;
res.json({ orderId, items: [] });
});
// Create resources with POST on collections
app.post("/orders", async (req, res) => {
const payload = req.body; // includes userId, items, address, etc.
res.status(201).json({ id: "ord_123", ...payload });
});
app.listen(3000, () => console.log("API listening on port 3000"));
Key takeaway: list endpoints (/orders, /posts, /shipments) stay consistent because filtering, sorting, and pagination are all query parameters. This avoids creating a separate endpoint for every combination of options.
Checklist Table (Do vs Don’t)
| Topic | Recommended | Avoid | Example |
|---|---|---|---|
| Resource naming | Nouns, plural collections | Verbs in paths | /orders vs /createOrder |
| Hierarchy | Shallow, 1-level nesting | Deep nesting chains | /orders/{id}/items vs /users/{u}/orders/{o}/items/{i} |
| Filtering | Query parameters | Path-based filters | /orders?status=paid vs /orders/paid |
| Sorting | Single sort convention |
Multiple ad-hoc endpoints | ?sort=-createdAt vs /orders/latest |
| Pagination | page + size (or cursor) |
Hard-coded limits | ?page=2&size=50 |
Step-by-Step Workflow to Design URIs
- List your domain nouns (resources) first: boards, posts, comments, orders, shipments, shipment events.
- Decide which are top-level collections (
/posts,/orders,/shipments) and which are sub-resources (/orders/{id}/items). - Choose a consistent naming style: plural nouns, lowercase, hyphenated words if needed (e.g.,
/shipment-eventsif you do not want nested/events). - Define list behavior once: filtering, sorting, and pagination via query parameters on every collection endpoint.
- Keep nesting shallow: only nest when the relationship clarifies meaning or simplifies authorization.
- Map actions to methods: create via
POST, update viaPATCH, delete viaDELETE. - Write examples for the most common user journeys (browse, search, detail, create, edit, delete) and confirm they feel predictable.
Extra Considerations
- Consistency beats perfection: a “good enough” consistent convention is easier to use than a “perfect” but inconsistent one.
- Case and separators: prefer lowercase paths and hyphens for multi-word segments (e.g.,
/order-items) rather than camelCase. - Trailing slash: pick a rule and keep it consistent (many APIs omit trailing slashes:
/ordersnot/orders/). - Versioning: if you must version in the URI, keep it simple (e.g.,
/v1/orders). If you use headers for versioning, document it clearly. - Errors and validation: predictable URIs help, but clear error responses (status code + message + field errors) complete the developer experience.
이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

0 댓글