Designing Resources and URLs
By the end of this lesson
Model endpoints around resources with predictable, stable addresses.
A caller learns your API mostly by pattern. They read two or three endpoints, form a guess about the rest, and are either rewarded or confused. URL design decides which.
The rule underneath every convention below is the same one: the path names a thing, and the method says what to do with it. Once a verb appears in a path, the method stops carrying meaning and the pattern is gone.
Conventions that are worth following because they are widely expected, not because they are laws:
- Plural nouns for collections: /api/employees, not /api/employee or /api/employeeList
- Lowercase, with hyphens if a name needs two words: /api/leave-requests
- The identifier in the path for one item: /api/employees/42
- The query string for narrowing, ordering and paging: /api/employees?departmentId=3&sort=surname
- No verbs in paths — the method is the verb
- No file extensions. Content type belongs in the Accept and Content-Type headers
- Names from the business, not from the database. /api/employees, even if the table is tbl_emp_master
# Predictable
GET /api/employees?departmentId=3&page=2
GET /api/employees/42
POST /api/employees
PATCH /api/employees/42
DELETE /api/employees/42
GET /api/departments/3/employees
GET /api/orders/1042/lines
# Awkward, and every line for a different reason
GET /api/getEmployee?id=42
POST /api/employee/create
POST /api/employees/42/update
GET /api/EmployeeList.json
GET /api/departments/3/employees/42/orders/1042/lines- getEmployee duplicates what GET already says, and it means a caller must learn each endpoint's name instead of a pattern.
- employee/create is singular and carries a verb. Two conventions broken in three words, and neither is guessable from any other endpoint.
- POST .../update throws away idempotency. A retried update should be harmless, which is what PATCH or PUT gives you.
- EmployeeList.json puts the format in the path. If you later add another format, the path is wrong; the Accept header was the right place all along.
- The last line nests five levels deep. Order 1042 already identifies the order, so everything before it is decoration the caller has to construct correctly — and any of those ids can contradict the others.
Nesting is worth having when it reflects real ownership and when the child has no useful meaning outside the parent. Order lines are a good case: a line belongs to exactly one order and nobody asks for line 7 on its own, so /api/orders/1042/lines reads correctly.
Employees and departments are a weaker case. An employee exists independently and can move between departments, so /api/departments/3/employees is best treated as a convenient way to list them — while /api/employees/42 stays the canonical address for one employee, and /api/employees?departmentId=3 does the same filtering work.
A practical limit: one level of nesting. Past that, the caller is assembling a path from several ids, each of which can be wrong or inconsistent with the others, and you now have to decide what to do when the employee in the path does not belong to the department in the path. Filtering on the child collection avoids the whole category of problem.
URLs are part of the contract, which is the part people learn late. A path in your code is also a path in someone's mobile app that shipped in March, in a partner's integration, in a scheduled job, and in a bookmark. Renaming /api/employees to /api/staff breaks every one of them, and no amount of care inside your codebase changes that.
Identifiers need the same stability. Use something that does not change: a database key or a generated identifier is fine, because neither is edited by a person. An employee's payroll number looks tempting and is a poor choice if HR ever reissues one, because the address of a resource would change underneath its callers.
If you want readable addresses, keep the stable id canonical and treat the readable form as an alternative. /api/departments/3 always works; /api/departments/customer-support can redirect to it. That way a rename changes a label, not an address.
Summary
- The path names a thing and the method says what to do with it
- Plural nouns, lowercase, ids in the path, filters in the query string, no verbs
- Nest only where ownership is real, and stop at one level
- Keep one canonical address per resource, and identifiers that a person never edits
- A path is part of the contract, so renaming it breaks callers regardless of how tidy it is
Practice
Attempt each one before opening the solution. Getting it wrong first is how the idea sticks.
Try it yourself
Design a small resource set
Design the paths and methods for these operations: list departments, read one department, list the employees in a department, read one employee, move an employee to a different department, and read the lines of an order.
Then check your answer against the conventions above, particularly the nesting rule.
Show solution
GET /api/departments, GET /api/departments/3, GET /api/departments/3/employees, GET /api/employees/42, and GET /api/orders/1042/lines cover five of the six directly.
Moving an employee is the interesting one. PATCH /api/employees/42 with a body containing the new departmentId treats the department as an attribute of the employee, which it is. That keeps one address for one employee and needs no new endpoint.
The design to avoid is POST /api/departments/5/employees/42/move, which invents a verb and forces a caller to hold three ids. If the move needs its own record — who moved them, when, and why — then a transfers resource becomes a genuine noun, and the earlier REST lesson describes the test for that.
Think about it
The cost of a rename
Your team renames the concept of an employee to a team member internally, and wants to rename /api/employees to /api/team-members for consistency.
What breaks, and what would you propose instead?
Show solution
Every consumer using the old path breaks the moment the new one ships: the mobile app that is already on people's phones, any partner integration, scheduled jobs, monitoring checks and anything with a stored URL.
A safer proposal is to leave the existing path in place and, if the new name genuinely matters, introduce it alongside — either as an additional path that behaves identically, or as part of a new version with a published timetable for retiring the old one.
The wider point is that internal vocabulary and the public contract are allowed to differ. Renaming classes costs an afternoon. Renaming a path costs every caller a release.
Saved in this browser only.