Resource Naming and URL Structure

Designing an intuitive and logical URL structure is a cornerstone of building well-designed REST APIs. Below is a detailed explanation of key principles and conventions-


URL Patterns and Conventions

  1. URLs should represent resources (nouns) rather than actions (verbs).

  2. Name should represent who rather than what.

  3. Use lowercase letters and hyphens (-) to separate words for readability. Avoid camelCase or underscores.

    • Correct: /books /user-profiles , /users/

    • Incorrect: /getBooks /userProfiles or /user_profiles.

  4. Use plural nouns for collections and singular nouns for individual resources.

    • /users → Represents the collection of users.

    • /users/{userId} → Represents a single user.

  5. Do not use file extensions like .json or .xml in URLs. Content negotiation should be handled via headers.

  6. Avoid deeply nested structures and long paths.

Example: /orders/{orderId}/items instead of /user/{userId}/order/{orderId}/item/{itemId}.

Resource Identification

  1. Show relationships explicitly using paths.

    • /users/{userId}/orders → Fetches all orders for a specific user.

  2. Represent a list of resources using plural nouns. e.g. /products

  3. Use a unique identifier (e.g., ID) to fetch a single resource.

    • /products/{productId} → Fetches details of a specific product.

  4. If resources have a natural compound identifier, include it in the URL.

    • /countries/{countryCode}/states/{stateCode}.

Nested Resources Handling

Nested resources should reflect a hierarchy between parent and child resources. Keep nesting manageable (1-2 levels) to avoid overly complex URLs.

  1. Use for clear parent-child relationships.

    • /authors/{authorId}/books → Books written by a specific author.

    • /departments/{deptId}/employees → Employees in a department.

  2. Deep nesting makes URLs cumbersome. Instead, flatten the structure and use query parameters or separate endpoints for complex relationships.

    • Too Deep: /users/{userId}/projects/{projectId}/tasks/{taskId}

    • Better: /tasks?userId={userId}&projectId={projectId}

  3. Ensure child resources can be accessed independently when needed.

    • /books/{bookId} should retrieve the book without requiring /authors/{authorId}/books/{bookId}.

Query Parameter Usage

Query parameters allow filtering, searching, sorting, and pagination without cluttering the URL path.

Examples:

  1. Filter and Searching:

    1. /products?category=electronics → Fetch products in the "electronics" category.

    2. /products?search=smartphone → Search for products with "smartphone"

  2. Sorting:

    1. /products?sort=price → Sort products by price in ascending order.

    2. /products?sort=-price → Sort products by price in descending order (prefix with - for descending).

  3. Pagination:

    1. /users?page=2&size=10 → Fetch the second page of 10 users.

    2. /products?limit=25&offset=50 → Fetch 25 products starting from the 50th.

    3. /products?category=electronics&price[lt]=1000&brand=apple → Multiple filters

  4. Boolean & Range queries :

    1. /orders?shipped=true → Fetch only shipped orders.

    2. /products?price[gte]=100&price[lte]=500 → Fetch products priced between 100 and 500.

  5. Custom Operations:

    1. /users/export?format=csv → Export users in CSV format.

Example

// Good Practice
@RestController
@RequestMapping("/api/v1/products")
public class ProductController {
    
    @GetMapping("/{productId}")
    public ProductDTO getProduct(@PathVariable String productId) {
        // Implementation
    }
    
    @PostMapping
    public ProductDTO createProduct(@RequestBody ProductDTO product) {
        // Implementation
    }
    
    // all with pagination
    @GetMapping
    public Page<ProductDTO> getProducts(
            @RequestParam(required = false) String category,
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "20") int size) {
        // Implementation
    }
    
    // multi
    @PostMapping("/batch")
    public List<ProductDTO> createProducts(@RequestBody List<ProductDTO> products) {
        // Implementation
    }
    
    
     // Get items for a specific order
    @GetMapping("/{orderId}/items")
    public List<OrderItemDTO> getOrderItems(@PathVariable String orderId) {
        // Implementation
    }
    
    // Get specific item from an order
    @GetMapping("/{orderId}/items/{itemId}")
    public OrderItemDTO getOrderItem(
            @PathVariable String orderId, 
            @PathVariable String itemId) {
        // Implementation
    }
    
    // filter and search
    @GetMapping("/products")
    public Page<ProductDTO> searchProducts(
        @RequestParam(required = false) String category,
        @RequestParam(required = false) BigDecimal minPrice,
        @RequestParam(required = false) BigDecimal maxPrice,
        @RequestParam(required = false) List<String> tags,
        @RequestParam(defaultValue = "0") int page,
        @RequestParam(defaultValue = "20") int size,
        @RequestParam(defaultValue = "name,asc") String[] sort) {
        // Implementation
    }
    
    
}

// Instead of (Bad Practice):
@GetMapping("/getProductById/{productId}")  // Avoid verbs
@PostMapping("/createNewProduct")           // Avoid redundant words

Service-to-Service Communication

When designing URLs for microservices, consider the service boundaries:

// Customer Service
@RestController
@RequestMapping("/api/v1/customers")
public class CustomerController {
    
    // Internal endpoint for service-to-service communication
    @GetMapping(value = "/{customerId}/verification",
                headers = "X-Internal-Call=true")
    public CustomerVerificationDTO getCustomerVerification(
            @PathVariable String customerId) {
        // Implementation
    }
}

// Order Service
@RestController
@RequestMapping("/api/v1/orders")
public class OrderController {
    
    // Public endpoint that aggregates data from multiple services
    @GetMapping("/{orderId}/summary")
    public OrderSummaryDTO getOrderSummary(@PathVariable String orderId) {
        // Calls customer service internally
        // Aggregates shipping info from shipping service
        // Combines with order details
    }
}

Aggregate Resources

When dealing with complex business entities that span multiple services:

// Product Catalog Service
@RestController
@RequestMapping("/api/v1/product-catalog")
public class ProductCatalogController {
    
    @GetMapping("/categories/{categoryId}/products")
    public Page<ProductDTO> getProductsByCategory(
            @PathVariable String categoryId,
            @RequestParam(required = false) Map<String, String> filters) {
        // Implementation
    }
    
    // Complex search across multiple domains
    @GetMapping("/search")
    public SearchResultDTO searchCatalog(
            @RequestParam String query,
            @RequestParam(required = false) List<String> facets,
            @RequestParam(required = false) Map<String, String> filters) {
        // Implementation
    }
}

Cross-Cutting Concerns

For functionalities that span multiple services:

// Audit Service
@RestController
@RequestMapping("/api/v1/audit")
public class AuditController {
    
    @GetMapping("/trails")
    public Page<AuditTrailDTO> getAuditTrails(
            @RequestParam(required = false) String resourceType,
            @RequestParam(required = false) String resourceId,
            @RequestParam(required = false) String action,
            @RequestParam(required = false) @DateTimeFormat(iso = DATE_TIME) 
                LocalDateTime fromDate,
            @RequestParam(required = false) @DateTimeFormat(iso = DATE_TIME) 
                LocalDateTime toDate) {
        // Implementation
    }
}

Resource State Transitions

For handling complex state transitions in distributed systems:

@RestController
@RequestMapping("/api/v1/orders")
public class OrderController {
    
    @PostMapping("/{orderId}/status-transitions")
    public OrderStatusDTO updateOrderStatus(
            @PathVariable String orderId,
            @RequestBody OrderStatusTransitionRequest request) {
        // Validates state transition
        // Updates order status
        // Triggers relevant events
    }
    
    @GetMapping("/{orderId}/possible-transitions")
    public List<String> getPossibleTransitions(@PathVariable String orderId) {
        // Returns allowed status transitions for current state
    }
}

API Gateway Level URLs

// API Gateway Route Configuration
@Configuration
public class GatewayConfig {
    
    @Bean
    public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
        return builder.routes()
            // Aggregate product details with inventory
            .route("product_details", r -> r
                .path("/api/v1/products/{id}/complete")
                .filters(f -> f
                    .rewritePath("/complete", "")
                    .addRequestHeader("X-Aggregate", "true"))
                .uri("lb://product-service"))
            // Aggregate order with customer details
            .route("order_details", r -> r
                .path("/api/v1/orders/{id}/full")
                .filters(f -> f
                    .rewritePath("/full", "")
                    .addRequestHeader("X-Include-Customer", "true"))
                .uri("lb://order-service"))
            .build();
    }
}

Best Practices Summary:

  1. Use plural nouns for collection resources

  2. Use nouns, not verbs in URLs

  3. Use hierarchical structure for related resources

  4. Use query parameters for filtering, sorting, and pagination

  5. Keep URLs as simple and logical as possible

  6. Maintain consistency across all services

  7. Consider service boundaries when designing URLs

  8. Use appropriate HTTP methods instead of encoding actions in URLs

  9. Handle cross-cutting concerns at appropriate levels

  10. Design with API evolution in mind

A well-designed URL structure improves API usability, readability, and maintainability. Would you like examples of these principles implemented in Spring Boot code?

Last updated