CodeGym /Courses /Module 5. Spring /How to properly craft URIs and work with resources

How to properly craft URIs and work with resources

Module 5. Spring
Level 10 , Lesson 6
Available

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:

  1. Version in the path:
    
    /api/v1/users
    
    A simple, standard approach. Convenient for REST APIs.
  2. Version in the header: You specify the version via an HTTP header:
    
    GET /users
    Content-Type: application/json
    API-Version: 1
    
  3. 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:

  1. URIs identify resources.
  2. 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:

  1. Get all users:
    
    GET /api/v1/users
    
  2. Create a new user:
    
    POST /api/v1/users
    {
        "name": "John Doe",
        "email": "john.doe@example.com"
    }
    
  3. Get all orders for a user:
    
    GET /api/v1/users/1/orders
    
  4. 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!

Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION