URI (Uniform Resource Identifier) are the main interface between your API and its users. Think of URIs as addresses on a map that users (with a navigator in hand) will use to reach features of your app. If your URIs are messy, inconsistent, or poorly thought-out, API users will feel the same pain as when you drive across town only to find the address goes nowhere.
Well-designed URIs:
- Improve readability and predictability of the API.
- Provide consistency and simplify maintenance.
- Help a new developer understand the API structure faster.
- Support the system's scalability in the future.
Main rules for creating URIs
We already touched on this, but it's worth repeating. Remember it like a mantra: "URIs should be intuitive and describe a resource, not an action." Here's what that means:
1. Use nouns instead of verbs
URIs should represent a resource, not actions on it. For example:
- Right:
/users(describes the "users" resource). - Wrong:
/getAllUsers(describes an action).
Example:
GET /users -> Get the list of all users.
GET /users/{id} -> Get a specific user's data.
POST /users -> Create a new user.
This approach makes the API predictable. The chance a developer will mistype and hit the wrong URI goes way down.
2. Use plural for collections
When designing routes for collections like a list of users or products, use the plural form. It's a common REST API convention.
Example:
/products -> List of all products.
/products/{id} -> Specific product by ID.
Why does this matter? Because it's immediately clear that /products is a group and /products/{id} is a single member of that group.
3. Use nesting for logical hierarchy
If your resources have a dependency or hierarchy, express that in the URI structure. For example, a user's orders can be represented like this:
GET /users/{userId}/orders -> Get all orders for a user.
/users/{userId}/orders/{orderId} -> Get a specific order for a user.
Nesting in the URI helps show the relationship between two resources. For example, an "order" without the user context might be ambiguous, but inside /users/{userId} it immediately makes sense.
4. Consistency and readability
Often in projects you see URIs like this:
/api/v1/get_all_users
Yeah... This breaks a few rules:
- Using underscores instead of hyphens.
- Actions instead of resources (
get_all_users). - No clear naming convention.
A well-formed URI:
/api/v1/users
- Use hyphens to separate words (
-), not underscores (_). - Keep it minimal: only include what's important.
Hyphens in URIs are easier to read and generally considered more legible.
Versioning your API
Versioning is an important aspect of a flexible API. It lets your API evolve without breaking old clients. Typically versions are included in the URI.
Versioning approaches:
- Version in the path:
A simple, standard approach. Convenient for REST APIs.
/api/v1/users - Version in the header: You specify the version via an HTTP header:
GET /users Content-Type: application/json API-Version: 1 - Version as a query parameter:
/users?apiVersion=1
For simplicity we'll mostly use version-in-path.
Nesting and structure
Nested URIs are used to represent relationships between resources. But keep in mind that excessive nesting makes maintenance harder. Nesting deeper than three levels is usually a sign of poor architecture.
Good example:
/users/{userId}/orders/{orderId}
Bad example:
/companies/{companyId}/departments/{departmentId}/teams/{teamId}/users/{userId}
If you end up with such a deep structure, consider revisiting your data model. For example, add a separate service to manage "users".
Where to use Query Parameters?
Query Parameters are handy when you want to filter, sort, or refine the data. For example:
GET /products?category=electronics&sort=price_asc&page=2
Division of responsibilities:
- URIs identify resources.
- Query Parameters are for searching, sorting, or limiting.
Example: putting it into practice
Let's apply everything we've learned and create a simple REST API for "Users" and "Orders" resources.
REST controller for users
@RestController
@RequestMapping("/api/v1/users")
public class UserController {
// Get all users
@GetMapping
public List<User> getAllUsers() {
return userService.getAllUsers();
}
// Get a single user
@GetMapping("/{userId}")
public User getUserById(@PathVariable Long userId) {
return userService.getUserById(userId);
}
// Create a user
@PostMapping
public User createUser(@RequestBody User newUser) {
return userService.createUser(newUser);
}
// Update a user
@PutMapping("/{userId}")
public User updateUser(@PathVariable Long userId, @RequestBody User updatedUser) {
return userService.updateUser(userId, updatedUser);
}
// Delete a user
@DeleteMapping("/{userId}")
public void deleteUser(@PathVariable Long userId) {
userService.deleteUser(userId);
}
}
Handling nesting for orders
@RestController
@RequestMapping("/api/v1/users/{userId}/orders")
public class OrderController {
// Get all orders for a user
@GetMapping
public List<Order> getAllOrdersForUser(@PathVariable Long userId) {
return orderService.getOrdersByUserId(userId);
}
// Get a specific order
@GetMapping("/{orderId}")
public Order getOrderById(@PathVariable Long userId, @PathVariable Long orderId) {
return orderService.getOrderById(userId, orderId);
}
// Create an order
@PostMapping
public Order createOrder(@PathVariable Long userId, @RequestBody Order newOrder) {
return orderService.createOrderForUser(userId, newOrder);
}
}
Example requests:
- Get all users:
GET /api/v1/users - Create a new user:
POST /api/v1/users { "name": "John Doe", "email": "john.doe@example.com" } - Get all orders for a user:
GET /api/v1/users/1/orders - Add an order for a user:
POST /api/v1/users/1/orders { "product": "Laptop", "quantity": 1 }
Now your REST API is tidy and pleasant. Thoughtful URI design is the key to a convenient and long-lasting API!
GO TO FULL VERSION