|
| 1 | +--- |
| 2 | +title: "Implement Router" |
| 3 | +date: "2025-01-27" |
| 4 | +tags: ["Hash Table", "String", "Design"] |
| 5 | +related: [] |
| 6 | +slug: "implement-router" |
| 7 | +category: "leetcode" |
| 8 | +leetcode_url: "https://leetcode.com/problems/implement-router/description/" |
| 9 | +leetcode_difficulty: "Medium" |
| 10 | +leetcode_topics: ["Hash Table", "String", "Design"] |
| 11 | +--- |
| 12 | + |
| 13 | +# Implement Router |
| 14 | + |
| 15 | +## Summary |
| 16 | +Design and implement a basic HTTP router that can register routes with different HTTP methods and handle incoming requests by matching them to the appropriate handler functions. |
| 17 | + |
| 18 | +## Problem Description |
| 19 | +Implement a Router class that supports: |
| 20 | + |
| 21 | +1. **Route Registration**: Register routes with specific HTTP methods (GET, POST, PUT, DELETE, etc.) |
| 22 | +2. **Route Matching**: Match incoming requests to registered routes |
| 23 | +3. **Handler Execution**: Execute the appropriate handler function for matched routes |
| 24 | +4. **404 Handling**: Return appropriate response for unmatched routes |
| 25 | + |
| 26 | +**Key Requirements:** |
| 27 | +- Support multiple HTTP methods |
| 28 | +- Handle exact path matching |
| 29 | +- Support path parameters (e.g., `/users/:id`) |
| 30 | +- Return appropriate HTTP status codes |
| 31 | +- Handle edge cases like duplicate routes, invalid methods |
| 32 | + |
| 33 | +**Example Usage:** |
| 34 | +```python |
| 35 | +router = Router() |
| 36 | + |
| 37 | +# Register routes |
| 38 | +router.add_route('GET', '/users', get_users_handler) |
| 39 | +router.add_route('GET', '/users/:id', get_user_handler) |
| 40 | +router.add_route('POST', '/users', create_user_handler) |
| 41 | + |
| 42 | +# Handle requests |
| 43 | +response = router.handle_request('GET', '/users') |
| 44 | +response = router.handle_request('GET', '/users/123') |
| 45 | +response = router.handle_request('POST', '/users') |
| 46 | +``` |
| 47 | + |
| 48 | +## Solution Approach |
| 49 | +This problem can be solved using a hash table-based approach with support for path parameters: |
| 50 | + |
| 51 | +### Core Data Structure |
| 52 | +- Use a dictionary where keys are HTTP methods and values are dictionaries of route patterns to handlers |
| 53 | +- Store both exact matches and parameterized routes |
| 54 | +- Use regex patterns for parameter matching |
| 55 | + |
| 56 | +### Algorithm Steps |
| 57 | +1. **Route Registration**: |
| 58 | + - Parse route pattern to identify parameters (e.g., `:id`) |
| 59 | + - Convert to regex pattern for matching |
| 60 | + - Store handler function with compiled regex |
| 61 | + |
| 62 | +2. **Route Matching**: |
| 63 | + - Look up routes for the given HTTP method |
| 64 | + - Try exact matches first |
| 65 | + - Try parameterized routes using regex matching |
| 66 | + - Extract parameter values from matched route |
| 67 | + |
| 68 | +3. **Handler Execution**: |
| 69 | + - Execute the matched handler with extracted parameters |
| 70 | + - Return appropriate response object |
| 71 | + |
| 72 | +## Time & Space Complexity |
| 73 | +- **Time Complexity:** |
| 74 | + - Route registration: O(1) average case |
| 75 | + - Route matching: O(n) where n is number of registered routes for the method |
| 76 | + - Handler execution: O(1) |
| 77 | +- **Space Complexity:** O(m × r) where m is number of HTTP methods and r is number of routes per method |
| 78 | + |
| 79 | +## Key Insights |
| 80 | +- **Exact match priority**: Check exact matches before parameterized routes |
| 81 | +- **Parameter extraction**: Use regex groups to extract parameter values |
| 82 | +- **Method separation**: Organize routes by HTTP method for efficient lookup |
| 83 | +- **Error handling**: Proper 404 and 405 (Method Not Allowed) responses |
| 84 | +- **Route conflicts**: Handle duplicate route registrations appropriately |
| 85 | + |
| 86 | +## Examples / snippets |
| 87 | + |
| 88 | +### Solution Code |
| 89 | +```python |
| 90 | +import re |
| 91 | +from typing import Dict, Callable, Optional, Tuple, Any |
| 92 | +from collections import defaultdict |
| 93 | + |
| 94 | +class Router: |
| 95 | + def __init__(self): |
| 96 | + # Dictionary: method -> {pattern: (handler, compiled_regex, param_names)} |
| 97 | + self.routes = defaultdict(dict) |
| 98 | + # Dictionary: method -> {exact_path: handler} for exact matches |
| 99 | + self.exact_routes = defaultdict(dict) |
| 100 | + |
| 101 | + def add_route(self, method: str, pattern: str, handler: Callable) -> None: |
| 102 | + """Register a route with the given method, pattern, and handler.""" |
| 103 | + method = method.upper() |
| 104 | + |
| 105 | + # Check for exact match (no parameters) |
| 106 | + if ':' not in pattern: |
| 107 | + if pattern in self.exact_routes[method]: |
| 108 | + raise ValueError(f"Route {method} {pattern} already exists") |
| 109 | + self.exact_routes[method][pattern] = handler |
| 110 | + return |
| 111 | + |
| 112 | + # Handle parameterized routes |
| 113 | + if pattern in self.routes[method]: |
| 114 | + raise ValueError(f"Route {method} {pattern} already exists") |
| 115 | + |
| 116 | + # Convert pattern to regex and extract parameter names |
| 117 | + param_names = [] |
| 118 | + regex_pattern = pattern |
| 119 | + |
| 120 | + # Find all :param patterns |
| 121 | + param_matches = re.findall(r':(\w+)', pattern) |
| 122 | + param_names = param_matches |
| 123 | + |
| 124 | + # Replace :param with regex groups |
| 125 | + regex_pattern = re.sub(r':(\w+)', r'([^/]+)', pattern) |
| 126 | + regex_pattern = f'^{regex_pattern}$' |
| 127 | + |
| 128 | + compiled_regex = re.compile(regex_pattern) |
| 129 | + self.routes[method][pattern] = (handler, compiled_regex, param_names) |
| 130 | + |
| 131 | + def handle_request(self, method: str, path: str) -> Dict[str, Any]: |
| 132 | + """Handle an incoming request and return the response.""" |
| 133 | + method = method.upper() |
| 134 | + |
| 135 | + # Try exact match first |
| 136 | + if path in self.exact_routes[method]: |
| 137 | + handler = self.exact_routes[method][path] |
| 138 | + return self._execute_handler(handler, {}) |
| 139 | + |
| 140 | + # Try parameterized routes |
| 141 | + for pattern, (handler, compiled_regex, param_names) in self.routes[method].items(): |
| 142 | + match = compiled_regex.match(path) |
| 143 | + if match: |
| 144 | + # Extract parameter values |
| 145 | + params = {} |
| 146 | + for i, param_name in enumerate(param_names): |
| 147 | + params[param_name] = match.group(i + 1) |
| 148 | + |
| 149 | + return self._execute_handler(handler, params) |
| 150 | + |
| 151 | + # Check if method exists but path doesn't (405 Method Not Allowed) |
| 152 | + if self.exact_routes[method] or self.routes[method]: |
| 153 | + return { |
| 154 | + 'status_code': 405, |
| 155 | + 'message': 'Method Not Allowed', |
| 156 | + 'data': None |
| 157 | + } |
| 158 | + |
| 159 | + # Route not found (404 Not Found) |
| 160 | + return { |
| 161 | + 'status_code': 404, |
| 162 | + 'message': 'Not Found', |
| 163 | + 'data': None |
| 164 | + } |
| 165 | + |
| 166 | + def _execute_handler(self, handler: Callable, params: Dict[str, str]) -> Dict[str, Any]: |
| 167 | + """Execute the handler function with extracted parameters.""" |
| 168 | + try: |
| 169 | + if params: |
| 170 | + result = handler(**params) |
| 171 | + else: |
| 172 | + result = handler() |
| 173 | + |
| 174 | + return { |
| 175 | + 'status_code': 200, |
| 176 | + 'message': 'OK', |
| 177 | + 'data': result |
| 178 | + } |
| 179 | + except Exception as e: |
| 180 | + return { |
| 181 | + 'status_code': 500, |
| 182 | + 'message': 'Internal Server Error', |
| 183 | + 'data': str(e) |
| 184 | + } |
| 185 | + |
| 186 | + def list_routes(self) -> Dict[str, list]: |
| 187 | + """List all registered routes for debugging.""" |
| 188 | + all_routes = {} |
| 189 | + for method in self.exact_routes: |
| 190 | + all_routes[method] = list(self.exact_routes[method].keys()) |
| 191 | + for method in self.routes: |
| 192 | + if method not in all_routes: |
| 193 | + all_routes[method] = [] |
| 194 | + all_routes[method].extend(self.routes[method].keys()) |
| 195 | + return all_routes |
| 196 | + |
| 197 | +# Example usage and test handlers |
| 198 | +def get_users(): |
| 199 | + return {"users": [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}]} |
| 200 | + |
| 201 | +def get_user(id: str): |
| 202 | + return {"user": {"id": int(id), "name": f"User {id}"}} |
| 203 | + |
| 204 | +def create_user(): |
| 205 | + return {"message": "User created successfully"} |
| 206 | + |
| 207 | +def update_user(id: str): |
| 208 | + return {"message": f"User {id} updated successfully"} |
| 209 | + |
| 210 | +def delete_user(id: str): |
| 211 | + return {"message": f"User {id} deleted successfully"} |
| 212 | + |
| 213 | +# Example usage |
| 214 | +if __name__ == "__main__": |
| 215 | + router = Router() |
| 216 | + |
| 217 | + # Register routes |
| 218 | + router.add_route('GET', '/users', get_users) |
| 219 | + router.add_route('GET', '/users/:id', get_user) |
| 220 | + router.add_route('POST', '/users', create_user) |
| 221 | + router.add_route('PUT', '/users/:id', update_user) |
| 222 | + router.add_route('DELETE', '/users/:id', delete_user) |
| 223 | + |
| 224 | + # Test requests |
| 225 | + print(router.handle_request('GET', '/users')) |
| 226 | + print(router.handle_request('GET', '/users/123')) |
| 227 | + print(router.handle_request('POST', '/users')) |
| 228 | + print(router.handle_request('PUT', '/users/456')) |
| 229 | + print(router.handle_request('DELETE', '/users/789')) |
| 230 | + print(router.handle_request('GET', '/nonexistent')) |
| 231 | + print(router.handle_request('PATCH', '/users')) |
| 232 | +``` |
| 233 | + |
| 234 | +### Example Walkthrough |
| 235 | +**Example 1: Basic Route Registration and Matching** |
| 236 | +```python |
| 237 | +router = Router() |
| 238 | + |
| 239 | +# Register routes |
| 240 | +router.add_route('GET', '/users', get_users_handler) |
| 241 | +router.add_route('GET', '/users/:id', get_user_handler) |
| 242 | + |
| 243 | +# Handle requests |
| 244 | +response1 = router.handle_request('GET', '/users') |
| 245 | +# Returns: {'status_code': 200, 'message': 'OK', 'data': {...}} |
| 246 | + |
| 247 | +response2 = router.handle_request('GET', '/users/123') |
| 248 | +# Returns: {'status_code': 200, 'message': 'OK', 'data': {'user': {'id': 123, 'name': 'User 123'}}} |
| 249 | +``` |
| 250 | + |
| 251 | +**Example 2: Error Handling** |
| 252 | +```python |
| 253 | +# 404 Not Found |
| 254 | +response = router.handle_request('GET', '/nonexistent') |
| 255 | +# Returns: {'status_code': 404, 'message': 'Not Found', 'data': None} |
| 256 | + |
| 257 | +# 405 Method Not Allowed |
| 258 | +response = router.handle_request('PATCH', '/users') |
| 259 | +# Returns: {'status_code': 405, 'message': 'Method Not Allowed', 'data': None} |
| 260 | +``` |
| 261 | + |
| 262 | +**Example 3: Multiple Parameters** |
| 263 | +```python |
| 264 | +def get_user_posts(user_id: str, post_id: str): |
| 265 | + return {"user_id": user_id, "post_id": post_id, "content": "Post content"} |
| 266 | + |
| 267 | +router.add_route('GET', '/users/:user_id/posts/:post_id', get_user_posts) |
| 268 | + |
| 269 | +response = router.handle_request('GET', '/users/123/posts/456') |
| 270 | +# Returns: {'status_code': 200, 'message': 'OK', 'data': {'user_id': '123', 'post_id': '456', 'content': 'Post content'}} |
| 271 | +``` |
| 272 | + |
| 273 | +## Edge Cases & Validation |
| 274 | +- **Duplicate routes**: Should raise ValueError when registering duplicate routes |
| 275 | +- **Invalid HTTP methods**: Should handle case-insensitive method matching |
| 276 | +- **Empty paths**: Should handle root path `/` appropriately |
| 277 | +- **Special characters**: Should handle paths with special characters correctly |
| 278 | +- **Parameter validation**: Should extract parameters correctly from complex paths |
| 279 | +- **Handler exceptions**: Should catch and return 500 errors for handler exceptions |
| 280 | +- **Method not allowed**: Should return 405 when method exists but path doesn't |
| 281 | +- **Case sensitivity**: Should handle path matching case-sensitively |
| 282 | +- **Trailing slashes**: Should handle paths with and without trailing slashes consistently |
| 283 | + |
| 284 | +## Related Problems |
| 285 | +- [208. Implement Trie (Prefix Tree)](https://leetcode.com/problems/implement-trie-prefix-tree/) - Tree-based routing |
| 286 | +- [211. Design Add and Search Words Data Structure](https://leetcode.com/problems/design-add-and-search-words-data-structure/) - Pattern matching |
| 287 | +- [676. Implement Magic Dictionary](https://leetcode.com/problems/implement-magic-dictionary/) - String matching with variations |
| 288 | +- [Design Twitter](https://leetcode.com/problems/design-twitter/) - System design with routing concepts |
0 commit comments