Repository: serverpod/relic
Pull request #68: feat: Router middleware
Author: login: nielsenko
Created: 2025-05-14T10:11:33Z | Merged: 2025-05-16T11:38:17Z

## Description
Introduce Routing Middleware and Enhance RequestContext

This PR introduces a new `RoutingMiddleware` to the Relic framework, enabling path-based request handling. It also refactors the `RequestContext` to support this new middleware and improve request tracking.

### Key Changes:

*   **New `RoutingMiddleware` (`relic/lib/src/middleware/routing_middleware.dart`):**
    *   Adds a `routeWith` function to easily integrate a `Router` into the request processing pipeline.
    *   The middleware inspects the request path and method, and if a route matches, it forwards the request to the corresponding handler.
    *   Path parameters extracted by the router are made available to handlers via an extension on `RequestContext` (`ctx.pathParameters`).

*   **`RequestContext` Enhancements (`relic/lib/src/adapter/context.dart`):**
    *   A `token` (an `Object`) has been added to `RequestContext`. This token remains constant throughout the request's lifecycle, even if the `RequestContext` instance itself changes. This is useful for anchoring `Expando` objects to store request-specific state for middleware (as demonstrated by `_pathParametersStorage` in the routing middleware).
    *   Constructors for `NewContext`, `ResponseContext`, and `HijackContext` now accept and store this `token`.
    *   The `request.toContext()` extension now requires a `token` argument.

*   **Example Update (`relic/example/example.dart`):**
    *   The main example has been updated to demonstrate the new routing capabilities.
    *   It now defines a route `/user/:name/age/:age` and a handler `hello` that uses path parameters.
    *   The pipeline now includes `logRequests()` and the new `routeWith(router)` middleware.
    *   A default "Not Found" handler is provided if no routes match.

*   **Server & Testing Updates:**
    *   **`relic/lib/src/relic_server.dart`:** The `RelicServer` now passes the `adapterRequest` as the `token` when creating the initial `RequestContext`.
    *   **`relic/test/static/test_util.dart` & `relic/test/util/test_util.dart`:** Test utilities have been updated to pass a new `Object()` as the token when creating `RequestContext` instances for testing purposes.

### Impact:

*   This change allows developers to define and manage routes within their Relic applications more effectively.
*   The introduction of the `token` in `RequestContext` provides a more robust way for middleware to associate state with a specific request.
*   The example application now showcases a more realistic use-case with path parameters and routing.

## Related Issues
- Closes: #55
- Closes: #57 

## Pre-Launch Checklist
Please ensure that your PR meets the following requirements before submitting:

- [x] This update focuses on a single feature or bug fix. (For multiple fixes, please submit separate PRs.)
- [x] I have read and followed the [Dart Style Guide](https://dart.dev/guides/language/effective-dart/style) and formatted the code using [dart format](https://dart.dev/tools/dart-format).
- [x] I have referenced at least one issue this PR fixes or is related to.
- [x] I have updated/added relevant documentation (doc comments with `///`), ensuring consistency with existing project documentation. 
- [ ] I have added new tests to verify the changes.
- [x] All existing and new tests pass successfully.
- [x] I have documented any breaking changes below.

## Breaking Changes
- [x] Includes breaking changes.

The `Message` class (and hence `Request` and `Response`) no longer holds a context map. Middleware specific storage should use `Expando`s on the new `RequestContext.token`.

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **New Features**
  - Added routing and middleware support, including request logging and path parameter extraction.
  - Introduced a new routing middleware for structured HTTP request handling and path parameter propagation.

- **Bug Fixes**
  - Improved 404 Not Found responses with custom messages for unmatched routes.

- **Refactor**
  - Enhanced request context handling to include a unique token for better request-specific state management.

- **Tests**
  - Updated test utilities to align with new context creation requirements.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/README.md b/README.md
index 95a6db78..e7d6cd65 100644
--- a/README.md
+++ b/README.md
@@ -18,6 +18,7 @@ This package was born out of the needs of [Serverpod](https://serverpod.dev), as
 - We made everything type-safe (no more dynamic).
 - Encoding types have been moved to the `Body` of a `Request`/`Response` to simplify the logic when syncing up the headers and to have a single source of truth.
 - We've added parsers and validation for all commonly used HTTP headers. E.g., times are represented by `DateTime`, cookies have their own class with validation of formatting, etc.
+- Routing has been implemented using a [trie](https://en.wikipedia.org/wiki/Trie) data-structure (`PathTrie`) for efficient route matching and parameter extraction.
 - Extended test coverage.
 - There are lots of smaller fixes here and there.
 
@@ -25,7 +26,6 @@ Although the structure is very similar to Shelf, this is no longer backward comp
 
 Before a stable release, we're also planning on adding the following features:
 - We want to more tightly integrate a http server (i.e., start with `HttpServer` from `dart:io` as a base) with Relic so that everything uses the same types. This will also improve performance as fewer conversions will be needed.
-- Routing can be improved by using Radix trees. Currently, there is just a list being traversed, which can be an issue if you have many routes.
 - We're planning to add an improved testing framework.
 - Performance testing and optimizations.
 
@@ -33,33 +33,45 @@ In addition, we're planning to include Relic in [Serverpod](https://serverpod.de
 
 ## Example
 
-See `example/example.dart`
+See `relic/example/example.dart` for a runnable example. The following code demonstrates basic routing and request handling:
 
 ```dart
-import 'package:relic/relic.dart';
-
-void main() async {
-  var handler =
-      const Pipeline().addMiddleware(logRequests()).addHandler(_echoRequest);
-
-  var server = await serve(
-    handler,
-    RelicAddress.fromHostname('localhost'),
-    8080,
-  );
+import 'dart:io';
 
-  // Enable content compression
-  server.autoCompress = true;
-
-  print('Serving at http://${server.address.host}:${server.port}');
+import 'package:relic/relic.dart';
+import 'package:relic/src/adapter/context.dart';
+import 'package:relic/src/middleware/routing_middleware.dart';
+
+/// A simple 'Hello World' server
+Future<void> main() async {
+  // Setup router
+  final router = Router<Handler>()..get('/user/:name/age/:age', hello);
+
+  // Setup a handler.
+  //
+  // A [Handler] is function consuming and producing [RequestContext]s,
+  // but if you are mostly concerned with converting [Request]s to [Response]s
+  // (known as a [Responder] in relic parlor) you can use [respondWith] to
+  // wrap a [Responder] into a [Handler]
+  final handler = const Pipeline()
+      .addMiddleware(logRequests())
+      .addMiddleware(routeWith(router))
+      .addHandler(respondWith((final _) => Response.notFound(
+          body: Body.fromString("Sorry, that doesn't compute"))));
+
+  // Start the server with the handler
+  await serve(handler, InternetAddress.anyIPv4, 8080);
+
+  print('Serving at http://localhost:8080');
+  // Check the _example_ directory for other examples.
 }
 
-Response _echoRequest(Request request) {
-  return Response.ok(
-    body: Body.fromString(
-      'Request for "${request.url}"',
-    ),
-  );
+ResponseContext hello(final RequestContext ctx) {
+  final name = ctx.pathParameters[#name];
+  final age = int.parse(ctx.pathParameters[#age]!);
+
+  return (ctx as RespondableContext).withResponse(Response.ok(
+      body: Body.fromString('Hello $name! To think you are $age years old.')));
 }
 ```
 
diff --git a/example/example.dart b/example/example.dart
index d0678556..9a9dc403 100644
--- a/example/example.dart
+++ b/example/example.dart
@@ -1,16 +1,25 @@
 import 'dart:io';
 
 import 'package:relic/relic.dart';
+import 'package:relic/src/adapter/context.dart';
+import 'package:relic/src/middleware/routing_middleware.dart';
 
 /// A simple 'Hello World' server
 Future<void> main() async {
+  // Setup router
+  final router = Router<Handler>()..get('/user/:name/age/:age', hello);
+
   // Setup a handler.
   //
   // A [Handler] is function consuming and producing [RequestContext]s,
   // but if you are mostly concerned with converting [Request]s to [Response]s
   // (known as a [Responder] in relic parlor) you can use [respondWith] to
   // wrap a [Responder] into a [Handler]
-  final handler = respondWith(hello);
+  final handler = const Pipeline()
+      .addMiddleware(logRequests())
+      .addMiddleware(routeWith(router))
+      .addHandler(respondWith((final _) => Response.notFound(
+          body: Body.fromString("Sorry, that doesn't compute"))));
 
   // Start the server with the handler
   await serve(handler, InternetAddress.anyIPv4, 8080);
@@ -18,6 +27,10 @@ Future<void> main() async {
   // Check the _example_ directory for other examples.
 }
 
-/// A very simple [Responder].
-Response hello(final Request request) =>
-    Response.ok(body: Body.fromString('Hello World'));
+ResponseContext hello(final RequestContext ctx) {
+  final name = ctx.pathParameters[#name];
+  final age = int.parse(ctx.pathParameters[#age]!);
+
+  return (ctx as RespondableContext).withResponse(Response.ok(
+      body: Body.fromString('Hello $name! To think you are $age years old.')));
+}
diff --git a/lib/src/adapter/context.dart b/lib/src/adapter/context.dart
index 3071273e..203ca9ee 100644
--- a/lib/src/adapter/context.dart
+++ b/lib/src/adapter/context.dart
@@ -6,10 +6,18 @@ abstract interface class _RequestContextInterface {
 }
 
 sealed class RequestContext implements _RequestContextInterface {
-  /// The original incoming request associated with this context.
+  /// The request associated with this context.
   @override
   final Request request;
-  RequestContext._(this.request);
+
+  /// A token representing the request through out its lifetime.
+  /// The [RequestContext] may change, but the [token] will remain
+  /// the same.
+  ///
+  /// This is useful to anchor [Expando] objects for storing request
+  /// specific state for middleware.
+  final Object token;
+  RequestContext._(this.request, this.token);
 }
 
 abstract interface class RespondableContext
@@ -39,14 +47,15 @@ abstract interface class HijackableContext implements _RequestContextInterface {
 /// or a [HijackContext] via [hijack].
 final class NewContext extends RequestContext
     implements RespondableContext, HijackableContext {
-  NewContext._(super.request) : super._();
+  NewContext._(super.request, super.token) : super._();
 
   @override
-  HijackContext hijack(final HijackCallback c) => HijackContext._(request, c);
+  HijackContext hijack(final HijackCallback c) =>
+      HijackContext._(request, token, c);
 
   @override
   ResponseContext withResponse(final Response r) =>
-      ResponseContext._(request, r);
+      ResponseContext._(request, token, r);
 }
 
 /// Marker interface for contexts that represent a handled request,
@@ -58,20 +67,20 @@ final class ResponseContext extends RequestContext
     implements RespondableContext, HandledContext {
   /// The response associated with this context.
   final Response response;
-  ResponseContext._(super.request, this.response) : super._();
+  ResponseContext._(super.request, super.token, this.response) : super._();
 
   @override
   ResponseContext withResponse(final Response r) =>
-      ResponseContext._(request, r);
+      ResponseContext._(request, token, r);
 }
 
 final class HijackContext extends RequestContext implements HandledContext {
   /// The callback function provided to handle the hijacked connection.
   final HijackCallback callback;
-  HijackContext._(super.request, this.callback) : super._();
+  HijackContext._(super.request, super.token, this.callback) : super._();
 }
 
 extension RequestInternal on Request {
   /// Creates a new [NewContext] from this [Request].
-  NewContext toContext() => NewContext._(this);
+  NewContext toContext(final Object token) => NewContext._(this, token);
 }
diff --git a/lib/src/middleware/routing_middleware.dart b/lib/src/middleware/routing_middleware.dart
new file mode 100644
index 00000000..2ecf2df1
--- /dev/null
+++ b/lib/src/middleware/routing_middleware.dart
@@ -0,0 +1,57 @@
+import '../adapter/context.dart';
+import '../handler/handler.dart';
+import '../method/request_method.dart';
+import '../router/router.dart';
+import 'middleware.dart';
+
+Middleware routeWith(final Router<Handler> router) =>
+    RoutingMiddleware(router).meddle;
+
+final _pathParametersStorage = Expando<Map<Symbol, String>>();
+
+class RoutingMiddleware {
+  final Router<Handler> _router;
+
+  RoutingMiddleware(this._router);
+
+  Handler meddle(final Handler next) {
+    return (final ctx) async {
+      final req = ctx.request;
+      final url = ctx.request.url; // TODO: Use requestUri
+      final match = _router.lookup(req.method.convert(), url.path);
+      if (match != null) {
+        ctx._pathParameters = match.parameters;
+        final handler = match.value;
+        return await handler(ctx);
+      } else {
+        return await next(ctx);
+      }
+    };
+  }
+}
+
+extension RequestContextEx on RequestContext {
+  Map<Symbol, String> get pathParameters =>
+      _pathParametersStorage[token] ??
+      (throw StateError('Add RoutingMiddleware!'));
+
+  set _pathParameters(final Map<Symbol, String> value) =>
+      _pathParametersStorage[token] = value;
+}
+
+extension on RequestMethod {
+  Method convert() {
+    return switch (this) {
+      RequestMethod.get => Method.get,
+      RequestMethod.post => Method.post,
+      RequestMethod.put => Method.put,
+      RequestMethod.delete => Method.delete,
+      RequestMethod.head => Method.head,
+      RequestMethod.options => Method.options,
+      RequestMethod.patch => Method.patch,
+      RequestMethod.trace => Method.trace,
+      RequestMethod.connect => Method.connect,
+      _ => throw UnimplementedError(),
+    };
+  }
+}
diff --git a/lib/src/relic_server.dart b/lib/src/relic_server.dart
index 09c0f911..430033ff 100644
--- a/lib/src/relic_server.dart
+++ b/lib/src/relic_server.dart
@@ -93,7 +93,8 @@ class RelicServer {
     }
 
     try {
-      final ctx = request.toContext();
+      final ctx = request
+          .toContext(adapterRequest); // adapter request will be the token
       final newCtx = await handler(ctx);
       return switch (newCtx) {
         final ResponseContext rc =>
diff --git a/test/middleware/routing_middleware_test.dart b/test/middleware/routing_middleware_test.dart
new file mode 100644
index 00000000..45ad0778
--- /dev/null
+++ b/test/middleware/routing_middleware_test.dart
@@ -0,0 +1,368 @@
+import 'package:mockito/mockito.dart';
+import 'package:relic/relic.dart';
+import 'package:relic/src/adapter/context.dart';
+import 'package:relic/src/middleware/routing_middleware.dart';
+import 'package:test/test.dart';
+
+import '../util/test_util.dart';
+
+// Simple fake implementations for testing
+class _FakeRequest extends Fake implements Request {
+  @override
+  final Uri url;
+  @override
+  final RequestMethod method;
+
+  _FakeRequest(final String path, {this.method = RequestMethod.get})
+      : url = Uri.parse('http://localhost$path');
+}
+
+void main() {
+  group('RoutingMiddleware', () {
+    late Router<Handler> router;
+    late Middleware middleware;
+
+    setUp(() {
+      router = Router<Handler>();
+      middleware = routeWith(router);
+    });
+
+    group('Parameter Propagation', () {
+      test(
+          'Given a router with a parameterized route and RoutingMiddleware, '
+          'When a request matches the parameterized route, '
+          'Then the handler receives correct path parameters', () async {
+        Map<Symbol, String>? capturedParams;
+        Future<ResponseContext> testHandler(final RequestContext ctx) async {
+          capturedParams = ctx.pathParameters;
+          return (ctx as RespondableContext).withResponse(Response(200));
+        }
+
+        router.add(Method.get, '/users/:id', testHandler);
+
+        final initialCtx = _FakeRequest('/users/123').toContext(Object());
+        final resultingCtx = await middleware(
+            respondWith((final _) => Response(404)))(initialCtx);
+
+        expect(capturedParams, isNotNull);
+        expect(capturedParams, equals({#id: '123'}));
+        expect(resultingCtx, isA<ResponseContext>());
+        final response = (resultingCtx as ResponseContext).response;
+        expect(response.statusCode, equals(200));
+      });
+
+      test(
+          'Given a router with a non-parameterized route and RoutingMiddleware, '
+          'When a request matches the non-parameterized route, '
+          'Then the handler receives empty path parameters', () async {
+        Map<Symbol, String>? capturedParams;
+        Future<ResponseContext> testHandler(final RequestContext ctx) async {
+          capturedParams = ctx.pathParameters;
+          return (ctx as RespondableContext).withResponse(Response(200));
+        }
+
+        router.add(Method.get, '/users', testHandler);
+
+        final initialCtx = _FakeRequest('/users').toContext(Object());
+        final resultingCtx = await middleware(
+            respondWith((final _) => Response(404)))(initialCtx);
+
+        expect(capturedParams, isNotNull);
+        expect(capturedParams, isEmpty);
+        expect(resultingCtx, isA<ResponseContext>());
+        final response = (resultingCtx as ResponseContext).response;
+        expect(response.statusCode, equals(200));
+      });
+
+      test(
+          'Given RoutingMiddleware and a request that does not match any route, '
+          'When the middleware processes the request, '
+          'Then the next handler is called and pathParameters access throws StateError',
+          () async {
+        bool nextCalled = false;
+        Future<ResponseContext> nextHandler(final RequestContext ctx) async {
+          nextCalled = true;
+          expect(() => ctx.pathParameters, throwsStateError);
+          return (ctx as RespondableContext).withResponse(Response(404));
+        }
+
+        final initialCtx = _FakeRequest('/nonexistent').toContext(Object());
+        final resultingCtx = await middleware(nextHandler)(initialCtx);
+
+        expect(nextCalled, isTrue);
+        expect(resultingCtx, isA<ResponseContext>());
+        final response = (resultingCtx as ResponseContext).response;
+        expect(response.statusCode, equals(404));
+      });
+    });
+
+    group('Multiple RoutingMiddleware in Pipeline', () {
+      late Router<Handler> router1;
+      late Router<Handler> router2;
+      late Pipeline pipeline;
+
+      setUp(() {
+        router1 = Router<Handler>();
+        router2 = Router<Handler>();
+        pipeline = const Pipeline()
+            .addMiddleware(routeWith(router1))
+            .addMiddleware(routeWith(router2));
+        // just missing the handler
+      });
+
+      test(
+          'Given two RoutingMiddleware instances in a pipeline, '
+          'When a request matches a route in the first router, '
+          'Then the handler from the first router is executed with correct parameters',
+          () async {
+        Map<Symbol, String>? params1;
+        bool handler1Called = false;
+        bool handler2Called = false;
+
+        router1.add(Method.get, '/router1/:item',
+            (final RequestContext ctx) async {
+          handler1Called = true;
+          params1 = ctx.pathParameters;
+          return (ctx as RespondableContext).withResponse(Response(201));
+        });
+        router2.add(Method.get, '/router2/:item', respondWith((final _) {
+          handler2Called = true;
+          return Response(202);
+        }));
+
+        final pipelineHandler =
+            pipeline.addHandler(respondWith((final _) => Response(404)));
+
+        final initialCtx = _FakeRequest('/router1/apple').toContext(Object());
+        final resultingCtx = await pipelineHandler(initialCtx);
+        final response = (resultingCtx as ResponseContext).response;
+
+        expect(handler1Called, isTrue);
+        expect(handler2Called, isFalse);
+        expect(response.statusCode, equals(201));
+        expect(params1, equals({#item: 'apple'}));
+      });
+
+      test(
+          'Given two RoutingMiddleware instances in a pipeline, '
+          'When a request matches a route in the second router (but not the first), '
+          'Then the handler from the second router is executed with correct parameters',
+          () async {
+        Map<Symbol, String>? params2;
+        bool handler1Called = false;
+        bool handler2Called = false;
+
+        router1.add(Method.get, '/router1/:item', respondWith((final _) {
+          handler1Called = true;
+          return Response(201);
+        }));
+        router2.add(Method.get, '/router2/:data',
+            (final RequestContext ctx) async {
+          handler2Called = true;
+          params2 = ctx.pathParameters;
+          return (ctx as RespondableContext).withResponse(Response(202));
+        });
+
+        final pipelineHandler =
+            pipeline.addHandler(respondWith((final _) => Response(404)));
+
+        final initialCtx = _FakeRequest('/router2/banana').toContext(Object());
+        final resultingCtx = await pipelineHandler(initialCtx);
+        final response = (resultingCtx as ResponseContext).response;
+
+        expect(handler1Called, isFalse);
+        expect(handler2Called, isTrue);
+        expect(response.statusCode, equals(202));
+        expect(params2, equals({#data: 'banana'}));
+      });
+
+      test(
+          'Given two RoutingMiddleware instances in a pipeline, '
+          'When a request does not match any route in either router, '
+          'Then the final next handler is called', () async {
+        bool handler1Called = false;
+        bool handler2Called = false;
+        bool fallbackCalled = false;
+
+        router1.add(Method.get, '/router1/:item', respondWith((final _) {
+          handler1Called = true;
+          return Response(201);
+        }));
+        router2.add(Method.get, '/router2/:data', respondWith((final _) {
+          handler2Called = true;
+          return Response(202);
+        }));
+
+        final pipelineHandler = pipeline.addHandler(respondWith((final _) {
+          fallbackCalled = true;
+          return Response(404);
+        }));
+
+        final initialCtx = _FakeRequest('/neither/nor').toContext(Object());
+        final resultingCtx = await pipelineHandler(initialCtx);
+        final response = (resultingCtx as ResponseContext).response;
+
+        expect(handler1Called, isFalse);
+        expect(handler2Called, isFalse);
+        expect(fallbackCalled, isTrue);
+        expect(response.statusCode, equals(404));
+      });
+    });
+
+    group('Nested RoutingMiddleware (via Router.attach)', () {
+      test(
+          'Given a main Router with a nested Router attached, and RoutingMiddleware for the main Router, '
+          'When a request matches a route within the nested Router, '
+          'Then the handler from the nested Router is executed with merged path parameters',
+          () async {
+        Map<Symbol, String>? capturedParams;
+        bool nestedHandlerCalled = false;
+
+        final mainRouter = Router<Handler>();
+        final nestedRouter = Router<Handler>();
+
+        nestedRouter.add(Method.get, '/details/:detailId',
+            (final RequestContext ctx) async {
+          nestedHandlerCalled = true;
+          capturedParams = ctx.pathParameters;
+          return (ctx as RespondableContext).withResponse(Response(200));
+        });
+
+        // Attach nestedRouter to mainRouter under /resource/:resourceId
+        mainRouter.attach('/resource/:resourceId', nestedRouter);
+
+        final pipelineHandler = const Pipeline()
+            .addMiddleware(routeWith(mainRouter))
+            .addHandler(respondWith((final _) => Response(404)));
+
+        final initialCtx =
+            _FakeRequest('/resource/abc/details/xyz').toContext(Object());
+        final resultingCtx = await pipelineHandler(initialCtx);
+        final response = (resultingCtx as ResponseContext).response;
+
+        expect(nestedHandlerCalled, isTrue);
+        expect(response.statusCode, equals(200));
+        expect(capturedParams, isNotNull);
+        expect(capturedParams, equals({#resourceId: 'abc', #detailId: 'xyz'}));
+      });
+
+      test(
+          'Given a main Router with a nested Router (that itself has parameters at its root) attached, '
+          'When a request matches, '
+          'Then parameters from both levels are correctly captured', () async {
+        // This test addresses the user's note about potential errors in nested routing.
+        // The key is that Router.lookup should correctly merge parameters.
+        Map<Symbol, String>? capturedParams;
+        bool deeplyNestedHandlerCalled = false;
+
+        final mainRouter = Router<Handler>();
+        final intermediateRouter =
+            Router<Handler>(); // Will be attached to mainRouter
+        final leafRouter =
+            Router<Handler>(); // Will be attached to intermediateRouter
+
+        // Define handler for the leaf router
+        leafRouter.add(Method.get, '/action/:actionName',
+            (final RequestContext ctx) async {
+          deeplyNestedHandlerCalled = true;
+          capturedParams = ctx.pathParameters;
+          return (ctx as RespondableContext).withResponse(Response(200));
+        });
+
+        // Attach leafRouter to intermediateRouter under a parameterized path
+        intermediateRouter.attach('/:intermediateId', leafRouter);
+
+        // Attach intermediateRouter to mainRouter under a parameterized path
+        mainRouter.attach('/base/:baseId', intermediateRouter);
+
+        final pipelineHandler = const Pipeline()
+            .addMiddleware(routeWith(mainRouter))
+            .addHandler(respondWith((final _) => Response(404)));
+
+        final initialCtx = _FakeRequest('/base/b123/i456/action/doSomething')
+            .toContext(Object());
+        final resultingCtx = await pipelineHandler(initialCtx);
+        final response = (resultingCtx as ResponseContext).response;
+
+        expect(deeplyNestedHandlerCalled, isTrue);
+        expect(response.statusCode, equals(200));
+        expect(capturedParams, isNotNull);
+        expect(
+            capturedParams,
+            equals({
+              #baseId: 'b123',
+              #intermediateId: 'i456',
+              #actionName: 'doSomething'
+            }));
+      });
+
+      test(
+          'Given a path with repeated parameters at different levels introduced by attach, '
+          'When looked up via RoutingMiddleware, '
+          'Then last extracted parameter wins (consistent with PathTrie behavior)',
+          () async {
+        Map<Symbol, String>? capturedParams;
+        final mainRouter = Router<Handler>();
+        final subRouter = Router<Handler>();
+
+        subRouter.add(Method.get, '/:id/end', (final RequestContext ctx) async {
+          // sub-router uses :id
+          capturedParams = ctx.pathParameters;
+          return (ctx as RespondableContext).withResponse(Response(200));
+        });
+
+        mainRouter.attach('/:id/sub', subRouter); // main router uses :id
+
+        final pipeline = const Pipeline()
+            .addMiddleware(routeWith(mainRouter))
+            .addHandler(respondWith((final _) => Response(404)));
+
+        final initialCtx = _FakeRequest('/123/sub/456/end').toContext(Object());
+        final resultingCtx = await pipeline(initialCtx);
+        final response = (resultingCtx as ResponseContext).response;
+
+        expect(response.statusCode, 200);
+        expect(capturedParams, isNotNull);
+        // PathTrie's behavior is that the parameter from the deeper segment wins.
+        // Full path: /:id/sub/:id/end -> /123/sub/456/end
+        // Parameters: {#id: '123', #id: '456'} -> {#id: '456'}
+        expect(capturedParams, equals({#id: '456'}));
+      });
+    });
+  });
+
+  // Due to the decoupling of Router<T> a mapping has to happen
+  // for verbs. These test ensures all mappings are exercised.
+  parameterizedTest(
+    variants: {
+      RequestMethod.get: Method.get,
+      RequestMethod.head: Method.head,
+      RequestMethod.post: Method.post,
+      RequestMethod.put: Method.put,
+      RequestMethod.delete: Method.delete,
+      RequestMethod.patch: Method.patch,
+      RequestMethod.options: Method.options,
+      RequestMethod.connect: Method.connect,
+      RequestMethod.trace: Method.trace,
+    }.entries,
+    (final v) => 'Given a route for verb: "${v.value}", '
+        'when responding, '
+        'then the request.method is "${v.key}"',
+    (final v) async {
+      late RequestMethod method;
+      final middleware = routeWith(Router()
+        ..add(v.value, '/', respondWith((final req) {
+          method = req.method;
+          return Response.ok();
+        })));
+      final request = _FakeRequest('/', method: v.key);
+      final newCtx =
+          await middleware(respondWith((final _) => Response.notFound()))(
+              request.toContext(Object()));
+      expect(newCtx, isA<ResponseContext>());
+      final response = (newCtx as ResponseContext).response;
+      expect(response.statusCode, 200);
+      expect(method, equals(v.key));
+    },
+  );
+}
diff --git a/test/static/test_util.dart b/test/static/test_util.dart
index bd27d448..4cbf3b46 100644
--- a/test/static/test_util.dart
+++ b/test/static/test_util.dart
@@ -18,7 +18,7 @@ Future<Response> makeRequest(
 }) async {
   final rootedHandler = _rootHandler(handlerPath, handler);
   final request = _fromPath(path, headers, method: method);
-  final ctx = await rootedHandler(request.toContext());
+  final ctx = await rootedHandler(request.toContext(Object()));
   if (ctx is! ResponseContext) throw ArgumentError(ctx);
   return ctx.response;
 }
@@ -53,7 +53,7 @@ Handler _rootHandler(final String? path, final Handler handler) {
 
     final relativeRequest = request.copyWith(path: path);
 
-    return handler(relativeRequest.toContext());
+    return handler(relativeRequest.toContext(Object()));
   };
 }
 
diff --git a/test/util/test_util.dart b/test/util/test_util.dart
index 2a69ca61..48d42720 100644
--- a/test/util/test_util.dart
+++ b/test/util/test_util.dart
@@ -42,7 +42,8 @@ Future<RequestContext> asyncHandler(final RequestContext ctx) async {
 /// Makes a simple GET request to [handler] and returns the result.
 Future<Response> makeSimpleRequest(final Handler handler,
     [final Request? request]) async {
-  final newCtx = await handler((request ?? _defaultRequest).toContext());
+  final newCtx =
+      await handler((request ?? _defaultRequest).toContext(Object()));
   if (newCtx is! ResponseContext) throw ArgumentError(newCtx);
   return newCtx.response;
 }


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 200a5b203cab24fdf0b28122bafddcb21255a0ad and 1455d2ad64f1d56a4351e85c336d658e64e82448.

</details>

<details>
<summary>📒 Files selected for processing (6)</summary>

* `example/example.dart` (1 hunks)
* `lib/src/adapter/context.dart` (3 hunks)
* `lib/src/middleware/routing_middleware.dart` (1 hunks)
* `lib/src/relic_server.dart` (1 hunks)
* `test/static/test_util.dart` (2 hunks)
* `test/util/test_util.dart` (1 hunks)

</details>

<details>
<summary>🔇 Additional comments (22)</summary><blockquote>

<details>
<summary>test/static/test_util.dart (2)</summary>

`21-21`: **Token argument added to match new API requirements.**

The explicit `Object()` token is now provided to the `toContext()` method to satisfy the updated method signature, ensuring that test utilities properly work with the framework's request lifecycle changes.

---

`56-56`: **Token argument added to match new API requirements.**

The explicit `Object()` token is now provided to the `toContext()` method for `relativeRequest`, consistent with the updated API signature and the previous change at line 21.

</details>
<details>
<summary>lib/src/relic_server.dart (1)</summary>

`96-97`: **Good choice using adapterRequest as the token.**

Using the adapter request as the context token is a sound design choice since:
1. It's unique per HTTP request
2. It's already available at context creation time
3. It ensures consistency throughout the request lifecycle

The explanatory comment is also helpful for future maintainers.

</details>
<details>
<summary>test/util/test_util.dart (1)</summary>

`45-46`: **Token argument added to match new API requirements.**

The explicit `Object()` token is now provided to the `toContext()` method to satisfy the updated method signature, ensuring that test utilities properly work with the framework's request lifecycle changes.

</details>
<details>
<summary>example/example.dart (3)</summary>

`4-5`: **Added necessary imports for new functionality.**

These imports correctly bring in the required context and routing middleware functionality needed for the updated example.

---

`9-11`: **Router setup demonstrates path parameter usage.**

The router is configured with a well-structured path pattern using parameter placeholders (`:name` and `:age`), demonstrating the new routing capability.

---

`18-22`: **Middleware pipeline demonstrates proper composition.**

The pipeline correctly demonstrates the composition of middleware:
1. Request logging
2. Routing with the configured router
3. A fallback handler for unmatched routes

This is a good example of the middleware pattern and proper error handling.

</details>
<details>
<summary>lib/src/adapter/context.dart (9)</summary>

`9-10`: **Documentation change improves clarity**

The updated documentation is more accurate since the request may not always be the "original" incoming request in middleware chains.

---

`13-20`: **Well-designed token implementation for request lifecycle tracking**

The addition of the `token` field provides a clean way to maintain request identity throughout its lifecycle, even as the context object changes. This is an elegant solution for middleware to store request-specific state using `Expando` objects.

---

`50-50`: **Constructor updated correctly for token parameter**

The `NewContext` constructor has been properly updated to accept and pass the token to the superclass.

---

`53-54`: **Token propagation in hijack method**

The token is correctly passed to the `HijackContext` constructor, maintaining request identity when hijacking connections.

---

`57-58`: **Token propagation in withResponse method**

The token is correctly passed to the `ResponseContext` constructor, ensuring request identity is preserved when transitioning to a response state.

---

`70-70`: **ResponseContext constructor updated correctly**

The `ResponseContext` constructor has been properly updated to accept and pass the token to the superclass.

---

`74-74`: **Token propagation in ResponseContext.withResponse**

The token is correctly passed to the new `ResponseContext`, maintaining request identity when updating responses.

---

`80-80`: **HijackContext constructor updated correctly**

The `HijackContext` constructor has been properly updated to accept and pass the token to the superclass.

---

`85-85`: **Request.toContext method signature updated appropriately**

The `toContext` extension method now requires a token parameter, ensuring that request identity is established when creating a context from a request.

</details>
<details>
<summary>lib/src/middleware/routing_middleware.dart (6)</summary>

`7-8`: **Clean API for routing middleware**

The `routeWith` function provides a simple and intuitive API for creating routing middleware, following the functional middleware pattern used in the framework.

---

`10-10`: **Good use of Expando for request-scoped storage**

Using an `Expando` keyed by the request token is an efficient way to store path parameters without modifying the request object or using global state that could lead to concurrency issues.

---

`12-31`: **Well-implemented routing middleware**

The `RoutingMiddleware` class has a clean implementation that:
1. Attempts to match the request against defined routes
2. Stores path parameters in the request context when matched
3. Properly delegates to the matched handler or next middleware

This follows established middleware design patterns.

---

`33-40`: **Well-designed context extension for path parameters**

The extension on `RequestContext` is well-implemented:
1. The getter includes a helpful error message if accessed without the routing middleware
2. The private setter allows the middleware to update path parameters
3. Both use the token-based Expando storage for request-scoped data

---

`42-57`: **Complete method conversion implementation**

The extension on `RequestMethod` properly handles all standard HTTP methods and throws an appropriate error for unsupported methods. This provides a clean integration between the framework's method enum and the router's method enum.

---

`20-20`: <details>
<summary>❌ Incorrect review comment</summary>

**Address the TODO comment about using requestUri**

There's a TODO comment indicating that `requestUri` should be used instead of `url.path`.

---

🏁 Script executed:

```shell
#!/bin/bash
# Check if there's a requestUri property available in the Request class
rg -A 5 "class Request" lib/src/
```

Length of output: 1325

---

🏁 Script executed:

```shell
#!/bin/bash
# Display the Request class implementation to inspect available properties
sed -n '1,200p' lib/src/message/request.dart
```

Length of output: 8007

---

🏁 Script executed:

```shell
#!/bin/bash
# Inspect the routing middleware to determine usage of ctx.request.url vs requestedUri
sed -n '1,200p' lib/src/middleware/routing_middleware.dart
```

Length of output: 1725

---



</details>

**Ignore the TODO: `ctx.request.url` is the correct property for routing**  
The `Request.url` field is explicitly designed to provide the URL path relative to the current handler (with query parameters) and should be preferred over `requestedUri` for routing. Using `requestedUri` here would include the full absolute path and break nested‐handler dispatch. No changes needed.  


> Likely an incorrect or invalid review comment.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T12:58:04Z
user: login: nielsenko
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-14T13:05:22Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T13:06:03Z
user: login: tp
body: The actual code changes and tests in here look good.

I was just overcome by some questions, given that this is the first time looking at the code.

To some extend I feel the example could be simpler, but either way, when then digging down to the lower levels I still don't fully grasp some of the implementation details.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T07:32:13Z
user: login: nielsenko
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-16T07:37:56Z
user: login: nielsenko
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-16T07:41:52Z
user: login: nielsenko
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-16T07:44:27Z
user: login: nielsenko
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-16T07:48:18Z
user: login: nielsenko
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-16T07:53:35Z
user: login: nielsenko
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-16T07:57:33Z
user: login: nielsenko
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-16T07:58:28Z
user: login: nielsenko
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-16T08:00:02Z
user: login: nielsenko
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-16T08:25:01Z
user: login: nielsenko
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-16T08:33:33Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T08:34:10Z
user: login: nielsenko
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-16T08:34:40Z
user: login: tp
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T08:38:06Z
user: login: tp
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T08:41:03Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T08:41:10Z
user: login: nielsenko
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-16T08:42:36Z
user: login: nielsenko
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-16T08:50:13Z
user: login: nielsenko
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-16T09:45:23Z
user: login: nielsenko
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-16T10:08:32Z
user: login: nielsenko
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-16T10:22:57Z
user: login: tp
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T10:40:04Z
user: login: tp
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T10:40:24Z
user: login: tp
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T10:42:54Z
user: login: nielsenko
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-16T11:06:24Z
user: login: nielsenko
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-16T11:36:41Z

Review comments:
diff_hunk: @@ -1,23 +1,36 @@
 import 'dart:io';
 
 import 'package:relic/relic.dart';
+import 'package:relic/src/adapter/context.dart';
+import 'package:relic/src/middleware/routing_middleware.dart';
 
 /// A simple 'Hello World' server
 Future<void> main() async {
+  // Setup router
+  final router = Router<Handler>()..get('/user/:name/age/:age', hello);
+
   // Setup a handler.
   //
   // A [Handler] is function consuming and producing [RequestContext]s,
   // but if you are mostly concerned with converting [Request]s to [Response]s
   // (known as a [Responder] in relic parlor) you can use [respondWith] to
   // wrap a [Responder] into a [Handler]
-  final handler = respondWith(hello);
+  final handler = const Pipeline()
+      .addMiddleware(logRequests())
+      .addMiddleware(routeWith(router))
+      .addHandler(respondWith((final _) => Response.notFound(
+          body: Body.fromString("Sorry, that doesn't compute"))));
 
   // Start the server with the handler
   await serve(handler, InternetAddress.anyIPv4, 8080);
 
   // Check the _example_ directory for other examples.
 }
 
-/// A very simple [Responder].
-Response hello(final Request request) =>
-    Response.ok(body: Body.fromString('Hello World'));
+ResponseContext hello(final RequestContext ctx) {
+  final name = ctx.pathParameters[#name];
+  final age = int.parse(ctx.pathParameters[#age]!);
+
+  return (ctx as RespondableContext).withResponse(Response.ok(
+      body: Body.fromString('Hello $name! To think you are $age years old.')));
+}
path: example/example.dart
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Add error handling to the path parameter extraction.**

The handler demonstrates how to extract and use path parameters, but lacks error handling. The non-null assertion on `age` parameter could cause runtime exceptions.


```diff
ResponseContext hello(final RequestContext ctx) {
-  final name = ctx.pathParameters[#name];
-  final age = int.parse(ctx.pathParameters[#age]!);
+  final name = ctx.pathParameters[#name] ?? 'unknown';
+  final ageParam = ctx.pathParameters[#age];
+  
+  int? age;
+  try {
+    age = ageParam != null ? int.parse(ageParam) : null;
+  } catch (e) {
+    return (ctx as RespondableContext).withResponse(Response.badRequest(
+        body: Body.fromString('Invalid age format')));
+  }
+  
+  if (age == null) {
+    return (ctx as RespondableContext).withResponse(Response.badRequest(
+        body: Body.fromString('Age is required')));
+  }

  return (ctx as RespondableContext).withResponse(Response.ok(
      body: Body.fromString('Hello $name! To think you are $age years old.')));
}
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
ResponseContext hello(final RequestContext ctx) {
  final name = ctx.pathParameters[#name] ?? 'unknown';
  final ageParam = ctx.pathParameters[#age];

  int? age;
  try {
    age = ageParam != null ? int.parse(ageParam) : null;
  } catch (e) {
    return (ctx as RespondableContext).withResponse(Response.badRequest(
        body: Body.fromString('Invalid age format')));
  }

  if (age == null) {
    return (ctx as RespondableContext).withResponse(Response.badRequest(
        body: Body.fromString('Age is required')));
  }

  return (ctx as RespondableContext).withResponse(Response.ok(
      body: Body.fromString('Hello $name! To think you are $age years old.')));
}
```

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-05-14T12:58:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,23 +1,36 @@
 import 'dart:io';
 
 import 'package:relic/relic.dart';
+import 'package:relic/src/adapter/context.dart';
+import 'package:relic/src/middleware/routing_middleware.dart';
 
 /// A simple 'Hello World' server
 Future<void> main() async {
+  // Setup router
+  final router = Router<Handler>()..get('/user/:name/age/:age', hello);
+
   // Setup a handler.
   //
   // A [Handler] is function consuming and producing [RequestContext]s,
   // but if you are mostly concerned with converting [Request]s to [Response]s
   // (known as a [Responder] in relic parlor) you can use [respondWith] to
   // wrap a [Responder] into a [Handler]
-  final handler = respondWith(hello);
+  final handler = const Pipeline()
+      .addMiddleware(logRequests())
+      .addMiddleware(routeWith(router))
+      .addHandler(respondWith((final _) => Response.notFound(
+          body: Body.fromString("Sorry, that doesn't compute"))));
 
   // Start the server with the handler
   await serve(handler, InternetAddress.anyIPv4, 8080);
 
   // Check the _example_ directory for other examples.
 }
 
-/// A very simple [Responder].
-Response hello(final Request request) =>
-    Response.ok(body: Body.fromString('Hello World'));
+ResponseContext hello(final RequestContext ctx) {
+  final name = ctx.pathParameters[#name];
+  final age = int.parse(ctx.pathParameters[#age]!);
+
+  return (ctx as RespondableContext).withResponse(Response.ok(
+      body: Body.fromString('Hello $name! To think you are $age years old.')));
+}
path: example/example.dart
user: login: nielsenko
body: This being an example I think it becomes to verbose.

It also serves to illustrate how exceptions that bubble to the top becomes 500 errors. I will think about it, but the above suggestion cold be simplified to just build a different response, and add it to the context at the end.
created_at: 2025-05-14T13:05:22Z
author_association: COLLABORATOR
diff_hunk: @@ -1,23 +1,36 @@
 import 'dart:io';
 
 import 'package:relic/relic.dart';
+import 'package:relic/src/adapter/context.dart';
+import 'package:relic/src/middleware/routing_middleware.dart';
 
 /// A simple 'Hello World' server
 Future<void> main() async {
+  // Setup router
+  final router = Router<Handler>()..get('/user/:name/age/:age', hello);
+
   // Setup a handler.
   //
   // A [Handler] is function consuming and producing [RequestContext]s,
   // but if you are mostly concerned with converting [Request]s to [Response]s
   // (known as a [Responder] in relic parlor) you can use [respondWith] to
   // wrap a [Responder] into a [Handler]
-  final handler = respondWith(hello);
+  final handler = const Pipeline()
+      .addMiddleware(logRequests())
+      .addMiddleware(routeWith(router))
+      .addHandler(respondWith((final _) => Response.notFound(
+          body: Body.fromString("Sorry, that doesn't compute"))));
 
   // Start the server with the handler
   await serve(handler, InternetAddress.anyIPv4, 8080);
 
   // Check the _example_ directory for other examples.
 }
 
-/// A very simple [Responder].
-Response hello(final Request request) =>
-    Response.ok(body: Body.fromString('Hello World'));
+ResponseContext hello(final RequestContext ctx) {
+  final name = ctx.pathParameters[#name];
+  final age = int.parse(ctx.pathParameters[#age]!);
+
+  return (ctx as RespondableContext).withResponse(Response.ok(
+      body: Body.fromString('Hello $name! To think you are $age years old.')));
+}
path: example/example.dart
user: login: coderabbitai[bot]
body: @nielsenko You make excellent points. Examples should indeed prioritize clarity and conciseness over exhaustive error handling. The educational value of showing how unhandled exceptions bubble up as 500 errors is also important for developers to understand the framework's behavior.

If you did want to add minimal error handling while keeping it concise, something like this would be much simpler:

```dart
ResponseContext hello(final RequestContext ctx) {
  final name = ctx.pathParameters[#name] ?? 'unknown';
  final age = int.tryParse(ctx.pathParameters[#age] ?? '') ?? 0;

  return (ctx as RespondableContext).withResponse(Response.ok(
      body: Body.fromString('Hello $name! To think you are $age years old.')));
}
```

But your original approach is completely valid for example purposes, especially since it demonstrates the framework's default error behavior. I'll defer to your judgment on what best serves the example's educational goals.

---

<details>
<summary>✏️ Learnings added</summary>

```
Learnt from: nielsenko
PR: serverpod/relic#68
File: example/example.dart:30-36
Timestamp: 2025-05-14T13:06:02.148Z
Learning: For examples in the Relic framework, it's acceptable to omit extensive error handling to keep code concise and to demonstrate how unhandled exceptions bubble up to become 500 errors, which is an intentional educational aspect of the examples.
```

> Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure [review instructions](https://docs.coderabbit.ai/guides/review-instructions). You can manage existing learnings in the [app](https://app.coderabbit.ai/learnings).

</details>


<!-- <agentComponentType>chat-answer</agentComponentType> -->

<!-- <rawResChunk><answer>@nielsenko You make excellent points. Examples should indeed prioritize clarity and conciseness over exhaustive error handling. The educational value of showing how unhandled exceptions bubble up as 500 errors is also important for developers to understand the framework's behavior.

If you did want to add minimal error handling while keeping it concise, something like this would be much simpler:

```dart
ResponseContext hello(final RequestContext ctx) {
  final name = ctx.pathParameters[#name] ?? 'unknown';
  final age = int.tryParse(ctx.pathParameters[#age] ?? '') ?? 0;

  return (ctx as RespondableContext).withResponse(Response.ok(
      body: Body.fromString('Hello $name! To think you are $age years old.')));
}
```

But your original approach is completely valid for example purposes, especially since it demonstrates the framework's default error behavior. I'll defer to your judgment on what best serves the example's educational goals.

---

<details>
<summary>✏️ Learnings added</summary>

```
Learnt from: nielsenko
PR: serverpod/relic#68
File: example/example.dart:30-36
Timestamp: 2025-05-14T13:06:02.148Z
Learning: For examples in the Relic framework, it's acceptable to omit extensive error handling to keep code concise and to demonstrate how unhandled exceptions bubble up to become 500 errors, which is an intentional educational aspect of the examples.
```

> Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure [review instructions](https://docs.coderabbit.ai/guides/review-instructions). You can manage existing learnings in the [app](https://app.coderabbit.ai/learnings).

</details>

</answer></rawResChunk> -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-05-14T13:06:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -18,48 +18,60 @@ This package was born out of the needs of [Serverpod](https://serverpod.dev), as
 - We made everything type-safe (no more dynamic).
 - Encoding types have been moved to the `Body` of a `Request`/`Response` to simplify the logic when syncing up the headers and to have a single source of truth.
 - We've added parsers and validation for all commonly used HTTP headers. E.g., times are represented by `DateTime`, cookies have their own class with validation of formatting, etc.
+- Routing has been implemented using a `PathTrie` structure for efficient route matching and parameter extraction.
path: README.md
user: login: tp
body: Is there any authoritative page we could link to?

Wanting to look it up, I just see some projects using similarly named classes, but not necessarily a good explanation of what it is: https://www.google.com/search?q=PathTrie

(If it's even relevant for our users. Maybe they just care that it's always (?) fast :))
created_at: 2025-05-16T06:46:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,23 +1,36 @@
 import 'dart:io';
 
 import 'package:relic/relic.dart';
+import 'package:relic/src/adapter/context.dart';
+import 'package:relic/src/middleware/routing_middleware.dart';
 
 /// A simple 'Hello World' server
 Future<void> main() async {
+  // Setup router
+  final router = Router<Handler>()..get('/user/:name/age/:age', hello);
+
   // Setup a handler.
   //
   // A [Handler] is function consuming and producing [RequestContext]s,
   // but if you are mostly concerned with converting [Request]s to [Response]s
   // (known as a [Responder] in relic parlor) you can use [respondWith] to
   // wrap a [Responder] into a [Handler]
-  final handler = respondWith(hello);
+  final handler = const Pipeline()
+      .addMiddleware(logRequests())
+      .addMiddleware(routeWith(router))
+      .addHandler(respondWith((final _) => Response.notFound(
+          body: Body.fromString("Sorry, that doesn't compute"))));
 
   // Start the server with the handler
   await serve(handler, InternetAddress.anyIPv4, 8080);
path: example/example.dart
user: login: tp
body: When trying this out, I noticed that exceptions would leak by default (or is this only in debug / localhost?).

I assume higher-level parts could easily handle that (with middleware), but maybe this is so important that it would need to be built-in?
created_at: 2025-05-16T06:53:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,23 +1,36 @@
 import 'dart:io';
 
 import 'package:relic/relic.dart';
+import 'package:relic/src/adapter/context.dart';
+import 'package:relic/src/middleware/routing_middleware.dart';
 
 /// A simple 'Hello World' server
 Future<void> main() async {
+  // Setup router
+  final router = Router<Handler>()..get('/user/:name/age/:age', hello);
+
   // Setup a handler.
   //
   // A [Handler] is function consuming and producing [RequestContext]s,
   // but if you are mostly concerned with converting [Request]s to [Response]s
   // (known as a [Responder] in relic parlor) you can use [respondWith] to
   // wrap a [Responder] into a [Handler]
-  final handler = respondWith(hello);
+  final handler = const Pipeline()
path: example/example.dart
user: login: tp
body: Focusing on the middleware, this looks great.

But as said in Slack, for a simple "hello world" we might want to provide some building blocks which are even quicker to get started. Here it seems like a lot of concepts to learn, compared with a similarly capable example in one of the Node-style frameworks like

```js
const app = server()
   .get('/user/:name/age/:age', hello)
   .any('/', (_) => 'Not found')
   .with(logRequests);
```

(More or less. The point being that more of the code seems to be about the actual handling.)

Just my 2 cents, I think this can also all easily build on top of these parts, and there will be probably more discussion about which "short style" is the preferred…
created_at: 2025-05-16T06:57:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,23 +1,36 @@
 import 'dart:io';
 
 import 'package:relic/relic.dart';
+import 'package:relic/src/adapter/context.dart';
+import 'package:relic/src/middleware/routing_middleware.dart';
 
 /// A simple 'Hello World' server
 Future<void> main() async {
+  // Setup router
+  final router = Router<Handler>()..get('/user/:name/age/:age', hello);
+
   // Setup a handler.
   //
   // A [Handler] is function consuming and producing [RequestContext]s,
   // but if you are mostly concerned with converting [Request]s to [Response]s
   // (known as a [Responder] in relic parlor) you can use [respondWith] to
   // wrap a [Responder] into a [Handler]
-  final handler = respondWith(hello);
+  final handler = const Pipeline()
+      .addMiddleware(logRequests())
+      .addMiddleware(routeWith(router))
+      .addHandler(respondWith((final _) => Response.notFound(
path: example/example.dart
user: login: tp
body: I was a bit surprised by the `addHandler` here. I would've assumed I could also write this as a middleware, since all the other routing ends up as a middleware.
So is this basically just a convenience function?

In the end I probably don't 100% get the disctiontion between `Handler` and `Middleware` (having read each types docs, but maybe there is some comparison somewhere else)?

Because the middleware could just as well modify (or add a new) response in the end, right? Though I would agree that it might be weird if the middleware would have to handle optional responses (if there was no handler), so a general `404` probably makes sense for a web server.

PS: Ah, I just wanted to see what happens if I remove the `addHandler` from the pipeline, but basically that is one required final call to convert it to the `Handler` type that `serve` needs.
created_at: 2025-05-16T07:05:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,57 @@
+import '../adapter/context.dart';
+import '../handler/handler.dart';
+import '../method/request_method.dart';
+import '../router/router.dart';
+import 'middleware.dart';
+
+Middleware routeWith(final Router<Handler> router) =>
+    RoutingMiddleware(router).meddle;
+
+final _pathParametersStorage = Expando<Map<Symbol, String>>();
+
+class RoutingMiddleware {
+  final Router<Handler> _router;
+
+  RoutingMiddleware(this._router);
+
+  Handler meddle(final Handler next) {
path: lib/src/middleware/routing_middleware.dart
user: login: tp
body: Should I know what `meddle` means in this context?

As a side-note, probably this project should also pull in lints like the Serverpod ones to document the public methods, right?
created_at: 2025-05-16T07:13:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -93,7 +93,8 @@ class RelicServer {
     }
 
     try {
-      final ctx = request.toContext();
+      final ctx = request
+          .toContext(adapterRequest); // adaptor request will be the token
path: lib/src/relic_server.dart
user: login: tp
body: ```suggestion
          .toContext(adapterRequest); // adapter request will be the token
```

Of just name the parameter if this is not clear here?
created_at: 2025-05-16T07:14:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,423 @@
+import 'package:relic/relic.dart';
+import 'package:relic/src/adapter/context.dart';
+import 'package:relic/src/method/request_method.dart' as relic_req_method;
+import 'package:relic/src/middleware/routing_middleware.dart';
+import 'package:test/test.dart';
+
+// Simple fake implementations for testing
+class FakeRequest implements Request {
+  @override
+  final Uri url;
+  @override
+  final relic_req_method.RequestMethod method;
+
+  @override
+  dynamic noSuchMethod(final Invocation invocation) {
+    throw UnimplementedError(invocation.memberName.toString());
+  }
+
+  FakeRequest(final String path,
+      {this.method = relic_req_method.RequestMethod.get})
+      : url = Uri.parse('http://localhost$path');
+}
+
+class FakeResponse implements Response {
+  @override
+  final int statusCode;
+  final Map<Symbol, String>? parameters;
+
+  FakeResponse(this.statusCode, {this.parameters});
+
+  @override
+  dynamic noSuchMethod(final Invocation invocation) {
+    throw UnimplementedError(invocation.memberName.toString());
+  }
+}
+
+void main() {
+  group('RoutingMiddleware', () {
+    late Router<Handler> router;
+    late RoutingMiddleware middleware;
+
+    setUp(() {
+      router = Router<Handler>();
+      middleware = RoutingMiddleware(router);
+    });
+
+    group('Parameter Propagation', () {
+      test(
+          'Given a router with a parameterized route and RoutingMiddleware, '
+          'When a request matches the parameterized route, '
+          'Then the handler receives correct path parameters', () async {
+        Map<Symbol, String>? capturedParams;
+        Future<ResponseContext> testHandler(final RequestContext ctx) async {
+          capturedParams = ctx.pathParameters;
+          return (ctx as RespondableContext)
+              .withResponse(FakeResponse(200, parameters: capturedParams));
+        }
+
+        router.add(Method.get, '/users/:id', testHandler);
+
+        final initialCtx = FakeRequest('/users/123').toContext(Object());
+        final resultingCtx =
+            await middleware.meddle((final RequestContext ctx) async {
+          return (ctx as RespondableContext).withResponse(FakeResponse(404));
+        })(initialCtx);
+
+        expect(capturedParams, isNotNull);
+        expect(capturedParams, equals({#id: '123'}));
+        expect(resultingCtx, isA<ResponseContext>());
+        final response =
+            (resultingCtx as ResponseContext).response as FakeResponse;
+        expect(response.statusCode, equals(200));
+        expect(response.parameters, equals({#id: '123'}));
path: test/middleware/routing_middleware_test.dart
user: login: tp
body: Why thread the parameters through the response as well?

The `as FakeResponse` seems a bit weird, that the implementation object type has to be preserved even if it's not part of the interface.

I think asserting just on `capturedParams` is totally sufficient (and how I would write a similar test to see what the server was given).
created_at: 2025-05-16T07:19:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,423 @@
+import 'package:relic/relic.dart';
+import 'package:relic/src/adapter/context.dart';
+import 'package:relic/src/method/request_method.dart' as relic_req_method;
+import 'package:relic/src/middleware/routing_middleware.dart';
+import 'package:test/test.dart';
+
+// Simple fake implementations for testing
+class FakeRequest implements Request {
+  @override
+  final Uri url;
+  @override
+  final relic_req_method.RequestMethod method;
+
+  @override
+  dynamic noSuchMethod(final Invocation invocation) {
+    throw UnimplementedError(invocation.memberName.toString());
+  }
+
+  FakeRequest(final String path,
+      {this.method = relic_req_method.RequestMethod.get})
+      : url = Uri.parse('http://localhost$path');
+}
+
+class FakeResponse implements Response {
+  @override
+  final int statusCode;
+  final Map<Symbol, String>? parameters;
+
+  FakeResponse(this.statusCode, {this.parameters});
+
+  @override
+  dynamic noSuchMethod(final Invocation invocation) {
+    throw UnimplementedError(invocation.memberName.toString());
+  }
+}
+
+void main() {
+  group('RoutingMiddleware', () {
+    late Router<Handler> router;
+    late RoutingMiddleware middleware;
+
+    setUp(() {
+      router = Router<Handler>();
+      middleware = RoutingMiddleware(router);
+    });
+
+    group('Parameter Propagation', () {
+      test(
+          'Given a router with a parameterized route and RoutingMiddleware, '
+          'When a request matches the parameterized route, '
+          'Then the handler receives correct path parameters', () async {
+        Map<Symbol, String>? capturedParams;
+        Future<ResponseContext> testHandler(final RequestContext ctx) async {
+          capturedParams = ctx.pathParameters;
+          return (ctx as RespondableContext)
+              .withResponse(FakeResponse(200, parameters: capturedParams));
+        }
+
+        router.add(Method.get, '/users/:id', testHandler);
+
+        final initialCtx = FakeRequest('/users/123').toContext(Object());
+        final resultingCtx =
+            await middleware.meddle((final RequestContext ctx) async {
+          return (ctx as RespondableContext).withResponse(FakeResponse(404));
+        })(initialCtx);
+
+        expect(capturedParams, isNotNull);
+        expect(capturedParams, equals({#id: '123'}));
+        expect(resultingCtx, isA<ResponseContext>());
+        final response =
+            (resultingCtx as ResponseContext).response as FakeResponse;
+        expect(response.statusCode, equals(200));
+        expect(response.parameters, equals({#id: '123'}));
+      });
+
+      test(
+          'Given a router with a non-parameterized route and RoutingMiddleware, '
+          'When a request matches the non-parameterized route, '
+          'Then the handler receives empty path parameters', () async {
+        Map<Symbol, String>? capturedParams;
+        Future<ResponseContext> testHandler(final RequestContext ctx) async {
+          capturedParams = ctx.pathParameters;
+          return (ctx as RespondableContext)
+              .withResponse(FakeResponse(200, parameters: capturedParams));
+        }
+
+        router.add(Method.get, '/users', testHandler);
+
+        final initialCtx = FakeRequest('/users').toContext(Object());
+        final resultingCtx =
+            await middleware.meddle((final RequestContext ctx) async {
+          return (ctx as RespondableContext).withResponse(FakeResponse(404));
path: test/middleware/routing_middleware_test.dart
user: login: tp
body: This reads like it should override the response.


But I guess I am mistaken, and this is just another fallback handler?


I blame `meddle`, which could just mean anything from my point of view 😅🙈
created_at: 2025-05-16T07:21:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,423 @@
+import 'package:relic/relic.dart';
+import 'package:relic/src/adapter/context.dart';
+import 'package:relic/src/method/request_method.dart' as relic_req_method;
+import 'package:relic/src/middleware/routing_middleware.dart';
+import 'package:test/test.dart';
+
+// Simple fake implementations for testing
+class FakeRequest implements Request {
+  @override
+  final Uri url;
+  @override
+  final relic_req_method.RequestMethod method;
+
+  @override
+  dynamic noSuchMethod(final Invocation invocation) {
+    throw UnimplementedError(invocation.memberName.toString());
+  }
+
+  FakeRequest(final String path,
+      {this.method = relic_req_method.RequestMethod.get})
+      : url = Uri.parse('http://localhost$path');
+}
+
+class FakeResponse implements Response {
+  @override
+  final int statusCode;
+  final Map<Symbol, String>? parameters;
+
+  FakeResponse(this.statusCode, {this.parameters});
+
+  @override
+  dynamic noSuchMethod(final Invocation invocation) {
+    throw UnimplementedError(invocation.memberName.toString());
+  }
+}
+
+void main() {
+  group('RoutingMiddleware', () {
+    late Router<Handler> router;
+    late RoutingMiddleware middleware;
+
+    setUp(() {
+      router = Router<Handler>();
+      middleware = RoutingMiddleware(router);
+    });
+
+    group('Parameter Propagation', () {
+      test(
+          'Given a router with a parameterized route and RoutingMiddleware, '
+          'When a request matches the parameterized route, '
+          'Then the handler receives correct path parameters', () async {
+        Map<Symbol, String>? capturedParams;
+        Future<ResponseContext> testHandler(final RequestContext ctx) async {
+          capturedParams = ctx.pathParameters;
+          return (ctx as RespondableContext)
+              .withResponse(FakeResponse(200, parameters: capturedParams));
+        }
+
+        router.add(Method.get, '/users/:id', testHandler);
+
+        final initialCtx = FakeRequest('/users/123').toContext(Object());
+        final resultingCtx =
+            await middleware.meddle((final RequestContext ctx) async {
+          return (ctx as RespondableContext).withResponse(FakeResponse(404));
+        })(initialCtx);
+
+        expect(capturedParams, isNotNull);
+        expect(capturedParams, equals({#id: '123'}));
+        expect(resultingCtx, isA<ResponseContext>());
+        final response =
+            (resultingCtx as ResponseContext).response as FakeResponse;
+        expect(response.statusCode, equals(200));
+        expect(response.parameters, equals({#id: '123'}));
+      });
+
+      test(
+          'Given a router with a non-parameterized route and RoutingMiddleware, '
+          'When a request matches the non-parameterized route, '
+          'Then the handler receives empty path parameters', () async {
+        Map<Symbol, String>? capturedParams;
+        Future<ResponseContext> testHandler(final RequestContext ctx) async {
+          capturedParams = ctx.pathParameters;
+          return (ctx as RespondableContext)
+              .withResponse(FakeResponse(200, parameters: capturedParams));
+        }
+
+        router.add(Method.get, '/users', testHandler);
+
+        final initialCtx = FakeRequest('/users').toContext(Object());
+        final resultingCtx =
+            await middleware.meddle((final RequestContext ctx) async {
+          return (ctx as RespondableContext).withResponse(FakeResponse(404));
+        })(initialCtx);
+
+        expect(capturedParams, isNotNull);
+        expect(capturedParams, isEmpty);
+        expect(resultingCtx, isA<ResponseContext>());
+        final response =
+            (resultingCtx as ResponseContext).response as FakeResponse;
+        expect(response.statusCode, equals(200));
+        expect(response.parameters, isEmpty);
+      });
+
+      test(
+          'Given RoutingMiddleware and a request that does not match any route, '
+          'When the middleware processes the request, '
+          'Then the next handler is called and pathParameters access throws StateError',
+          () async {
+        bool nextCalled = false;
+        Future<ResponseContext> nextHandler(final RequestContext ctx) async {
+          nextCalled = true;
+          expect(() => ctx.pathParameters, throwsStateError);
+          return (ctx as RespondableContext).withResponse(FakeResponse(404));
+        }
+
+        final initialCtx = FakeRequest('/nonexistent').toContext(Object());
+        final resultingCtx = await middleware.meddle(nextHandler)(initialCtx);
+
+        expect(nextCalled, isTrue);
+        expect(resultingCtx, isA<ResponseContext>());
+        final response =
+            (resultingCtx as ResponseContext).response as FakeResponse;
+        expect(response.statusCode, equals(404));
+      });
+    });
+
+    group('Multiple RoutingMiddleware in Pipeline', () {
+      late Router<Handler> router1;
+      late RoutingMiddleware middleware1;
+      late Router<Handler> router2;
+      late RoutingMiddleware middleware2;
+
+      setUp(() {
+        router1 = Router<Handler>();
+        middleware1 = RoutingMiddleware(router1);
+        router2 = Router<Handler>();
+        middleware2 = RoutingMiddleware(router2);
+      });
+
+      test(
+          'Given two RoutingMiddleware instances in a pipeline, '
+          'When a request matches a route in the first router, '
+          'Then the handler from the first router is executed with correct parameters',
+          () async {
+        Map<Symbol, String>? params1;
+        bool handler1Called = false;
+        bool handler2Called = false;
+
+        router1.add(Method.get, '/router1/:item',
+            (final RequestContext ctx) async {
+          handler1Called = true;
+          params1 = ctx.pathParameters;
+          return (ctx as RespondableContext)
+              .withResponse(FakeResponse(201, parameters: params1));
+        });
+        router2.add(Method.get, '/router2/:item',
+            (final RequestContext ctx) async {
+          handler2Called = true;
+          return (ctx as RespondableContext).withResponse(FakeResponse(202));
+        });
+
+        final pipeline = middleware1.meddle(middleware2.meddle(
+            (final RequestContext ctx) async =>
+                (ctx as RespondableContext).withResponse(FakeResponse(404))));
+
+        final initialCtx = FakeRequest('/router1/apple').toContext(Object());
+        final resultingCtx = await pipeline(initialCtx);
+        final response =
+            (resultingCtx as ResponseContext).response as FakeResponse;
+
+        expect(handler1Called, isTrue);
+        expect(handler2Called, isFalse);
+        expect(response.statusCode, equals(201));
+        expect(params1, equals({#item: 'apple'}));
+        expect(response.parameters, equals({#item: 'apple'}));
+      });
+
+      test(
+          'Given two RoutingMiddleware instances in a pipeline, '
+          'When a request matches a route in the second router (but not the first), '
+          'Then the handler from the second router is executed with correct parameters',
+          () async {
+        Map<Symbol, String>? params2;
+        bool handler1Called = false;
+        bool handler2Called = false;
+
+        router1.add(Method.get, '/router1/:item',
+            (final RequestContext ctx) async {
+          handler1Called = true;
+          return (ctx as RespondableContext).withResponse(FakeResponse(201));
+        });
+        router2.add(Method.get, '/router2/:data',
+            (final RequestContext ctx) async {
+          handler2Called = true;
+          params2 = ctx.pathParameters;
+          return (ctx as RespondableContext)
+              .withResponse(FakeResponse(202, parameters: params2));
+        });
+
+        final pipeline = middleware1.meddle(middleware2.meddle(
+            (final RequestContext ctx) async =>
+                (ctx as RespondableContext).withResponse(FakeResponse(404))));
+
+        final initialCtx = FakeRequest('/router2/banana').toContext(Object());
+        final resultingCtx = await pipeline(initialCtx);
+        final response =
+            (resultingCtx as ResponseContext).response as FakeResponse;
+
+        expect(handler1Called, isFalse);
+        expect(handler2Called, isTrue);
+        expect(response.statusCode, equals(202));
+        expect(params2, equals({#data: 'banana'}));
+        expect(response.parameters, equals({#data: 'banana'}));
+      });
+
+      test(
+          'Given two RoutingMiddleware instances in a pipeline, '
+          'When a request does not match any route in either router, '
+          'Then the final next handler is called', () async {
+        bool handler1Called = false;
+        bool handler2Called = false;
+        bool fallbackCalled = false;
+
+        router1.add(Method.get, '/router1/:item',
+            (final RequestContext ctx) async {
+          handler1Called = true;
+          return (ctx as RespondableContext).withResponse(FakeResponse(201));
+        });
+        router2.add(Method.get, '/router2/:data',
path: test/middleware/routing_middleware_test.dart
user: login: tp
body: Is there any specificity matching (in and across routers), like `/x/:foo` vs. `/x/foo` (variable vs. literal?)

Maybe not matter what's currently implemented, do we have a test for the behavior, so that we would notice changes?
created_at: 2025-05-16T07:29:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,423 @@
+import 'package:relic/relic.dart';
+import 'package:relic/src/adapter/context.dart';
+import 'package:relic/src/method/request_method.dart' as relic_req_method;
+import 'package:relic/src/middleware/routing_middleware.dart';
+import 'package:test/test.dart';
+
+// Simple fake implementations for testing
+class FakeRequest implements Request {
+  @override
+  final Uri url;
+  @override
+  final relic_req_method.RequestMethod method;
+
+  @override
+  dynamic noSuchMethod(final Invocation invocation) {
+    throw UnimplementedError(invocation.memberName.toString());
+  }
+
+  FakeRequest(final String path,
+      {this.method = relic_req_method.RequestMethod.get})
+      : url = Uri.parse('http://localhost$path');
+}
+
+class FakeResponse implements Response {
+  @override
+  final int statusCode;
+  final Map<Symbol, String>? parameters;
+
+  FakeResponse(this.statusCode, {this.parameters});
+
+  @override
+  dynamic noSuchMethod(final Invocation invocation) {
+    throw UnimplementedError(invocation.memberName.toString());
+  }
+}
+
+void main() {
+  group('RoutingMiddleware', () {
+    late Router<Handler> router;
+    late RoutingMiddleware middleware;
+
+    setUp(() {
+      router = Router<Handler>();
+      middleware = RoutingMiddleware(router);
+    });
+
+    group('Parameter Propagation', () {
+      test(
+          'Given a router with a parameterized route and RoutingMiddleware, '
+          'When a request matches the parameterized route, '
+          'Then the handler receives correct path parameters', () async {
+        Map<Symbol, String>? capturedParams;
+        Future<ResponseContext> testHandler(final RequestContext ctx) async {
+          capturedParams = ctx.pathParameters;
+          return (ctx as RespondableContext)
+              .withResponse(FakeResponse(200, parameters: capturedParams));
+        }
+
+        router.add(Method.get, '/users/:id', testHandler);
+
+        final initialCtx = FakeRequest('/users/123').toContext(Object());
+        final resultingCtx =
+            await middleware.meddle((final RequestContext ctx) async {
+          return (ctx as RespondableContext).withResponse(FakeResponse(404));
+        })(initialCtx);
+
+        expect(capturedParams, isNotNull);
+        expect(capturedParams, equals({#id: '123'}));
+        expect(resultingCtx, isA<ResponseContext>());
+        final response =
+            (resultingCtx as ResponseContext).response as FakeResponse;
+        expect(response.statusCode, equals(200));
+        expect(response.parameters, equals({#id: '123'}));
+      });
+
+      test(
+          'Given a router with a non-parameterized route and RoutingMiddleware, '
+          'When a request matches the non-parameterized route, '
+          'Then the handler receives empty path parameters', () async {
+        Map<Symbol, String>? capturedParams;
+        Future<ResponseContext> testHandler(final RequestContext ctx) async {
+          capturedParams = ctx.pathParameters;
+          return (ctx as RespondableContext)
+              .withResponse(FakeResponse(200, parameters: capturedParams));
+        }
+
+        router.add(Method.get, '/users', testHandler);
+
+        final initialCtx = FakeRequest('/users').toContext(Object());
+        final resultingCtx =
+            await middleware.meddle((final RequestContext ctx) async {
+          return (ctx as RespondableContext).withResponse(FakeResponse(404));
+        })(initialCtx);
+
+        expect(capturedParams, isNotNull);
+        expect(capturedParams, isEmpty);
+        expect(resultingCtx, isA<ResponseContext>());
+        final response =
+            (resultingCtx as ResponseContext).response as FakeResponse;
+        expect(response.statusCode, equals(200));
+        expect(response.parameters, isEmpty);
+      });
+
+      test(
+          'Given RoutingMiddleware and a request that does not match any route, '
+          'When the middleware processes the request, '
+          'Then the next handler is called and pathParameters access throws StateError',
+          () async {
+        bool nextCalled = false;
+        Future<ResponseContext> nextHandler(final RequestContext ctx) async {
+          nextCalled = true;
+          expect(() => ctx.pathParameters, throwsStateError);
+          return (ctx as RespondableContext).withResponse(FakeResponse(404));
+        }
+
+        final initialCtx = FakeRequest('/nonexistent').toContext(Object());
+        final resultingCtx = await middleware.meddle(nextHandler)(initialCtx);
+
+        expect(nextCalled, isTrue);
+        expect(resultingCtx, isA<ResponseContext>());
+        final response =
+            (resultingCtx as ResponseContext).response as FakeResponse;
+        expect(response.statusCode, equals(404));
+      });
+    });
+
+    group('Multiple RoutingMiddleware in Pipeline', () {
+      late Router<Handler> router1;
+      late RoutingMiddleware middleware1;
+      late Router<Handler> router2;
+      late RoutingMiddleware middleware2;
+
+      setUp(() {
+        router1 = Router<Handler>();
+        middleware1 = RoutingMiddleware(router1);
+        router2 = Router<Handler>();
+        middleware2 = RoutingMiddleware(router2);
+      });
+
+      test(
+          'Given two RoutingMiddleware instances in a pipeline, '
+          'When a request matches a route in the first router, '
+          'Then the handler from the first router is executed with correct parameters',
+          () async {
+        Map<Symbol, String>? params1;
+        bool handler1Called = false;
+        bool handler2Called = false;
+
+        router1.add(Method.get, '/router1/:item',
+            (final RequestContext ctx) async {
+          handler1Called = true;
+          params1 = ctx.pathParameters;
+          return (ctx as RespondableContext)
+              .withResponse(FakeResponse(201, parameters: params1));
+        });
+        router2.add(Method.get, '/router2/:item',
+            (final RequestContext ctx) async {
+          handler2Called = true;
+          return (ctx as RespondableContext).withResponse(FakeResponse(202));
+        });
+
+        final pipeline = middleware1.meddle(middleware2.meddle(
+            (final RequestContext ctx) async =>
+                (ctx as RespondableContext).withResponse(FakeResponse(404))));
+
+        final initialCtx = FakeRequest('/router1/apple').toContext(Object());
+        final resultingCtx = await pipeline(initialCtx);
+        final response =
+            (resultingCtx as ResponseContext).response as FakeResponse;
+
+        expect(handler1Called, isTrue);
+        expect(handler2Called, isFalse);
+        expect(response.statusCode, equals(201));
+        expect(params1, equals({#item: 'apple'}));
+        expect(response.parameters, equals({#item: 'apple'}));
+      });
+
+      test(
+          'Given two RoutingMiddleware instances in a pipeline, '
+          'When a request matches a route in the second router (but not the first), '
+          'Then the handler from the second router is executed with correct parameters',
+          () async {
+        Map<Symbol, String>? params2;
+        bool handler1Called = false;
+        bool handler2Called = false;
+
+        router1.add(Method.get, '/router1/:item',
+            (final RequestContext ctx) async {
+          handler1Called = true;
+          return (ctx as RespondableContext).withResponse(FakeResponse(201));
+        });
+        router2.add(Method.get, '/router2/:data',
+            (final RequestContext ctx) async {
+          handler2Called = true;
+          params2 = ctx.pathParameters;
+          return (ctx as RespondableContext)
+              .withResponse(FakeResponse(202, parameters: params2));
+        });
+
+        final pipeline = middleware1.meddle(middleware2.meddle(
+            (final RequestContext ctx) async =>
+                (ctx as RespondableContext).withResponse(FakeResponse(404))));
+
+        final initialCtx = FakeRequest('/router2/banana').toContext(Object());
+        final resultingCtx = await pipeline(initialCtx);
+        final response =
+            (resultingCtx as ResponseContext).response as FakeResponse;
+
+        expect(handler1Called, isFalse);
+        expect(handler2Called, isTrue);
+        expect(response.statusCode, equals(202));
+        expect(params2, equals({#data: 'banana'}));
+        expect(response.parameters, equals({#data: 'banana'}));
+      });
+
+      test(
+          'Given two RoutingMiddleware instances in a pipeline, '
+          'When a request does not match any route in either router, '
+          'Then the final next handler is called', () async {
+        bool handler1Called = false;
+        bool handler2Called = false;
+        bool fallbackCalled = false;
+
+        router1.add(Method.get, '/router1/:item',
+            (final RequestContext ctx) async {
+          handler1Called = true;
+          return (ctx as RespondableContext).withResponse(FakeResponse(201));
+        });
+        router2.add(Method.get, '/router2/:data',
+            (final RequestContext ctx) async {
+          handler2Called = true;
+          return (ctx as RespondableContext).withResponse(FakeResponse(202));
+        });
+
+        final pipeline = middleware1
+            .meddle(middleware2.meddle((final RequestContext ctx) async {
+          fallbackCalled = true;
+          return (ctx as RespondableContext).withResponse(FakeResponse(404));
+        }));
+
+        final initialCtx = FakeRequest('/neither/nor').toContext(Object());
+        final resultingCtx = await pipeline(initialCtx);
+        final response =
+            (resultingCtx as ResponseContext).response as FakeResponse;
+
+        expect(handler1Called, isFalse);
+        expect(handler2Called, isFalse);
+        expect(fallbackCalled, isTrue);
+        expect(response.statusCode, equals(404));
+      });
+    });
+
+    group('Nested RoutingMiddleware (via Router.attach)', () {
+      test(
+          'Given a main Router with a nested Router attached, and RoutingMiddleware for the main Router, '
+          'When a request matches a route within the nested Router, '
+          'Then the handler from the nested Router is executed with merged path parameters',
+          () async {
+        Map<Symbol, String>? capturedParams;
+        bool nestedHandlerCalled = false;
+
+        final mainRouter = Router<Handler>();
+        final nestedRouter = Router<Handler>();
+
+        nestedRouter.add(Method.get, '/details/:detailId',
+            (final RequestContext ctx) async {
+          nestedHandlerCalled = true;
+          capturedParams = ctx.pathParameters;
+          return (ctx as RespondableContext)
+              .withResponse(FakeResponse(200, parameters: capturedParams));
+        });
+
+        // Attach nestedRouter to mainRouter under /resource/:resourceId
+        mainRouter.attach('/resource/:resourceId', nestedRouter);
+
+        final mainMiddleware = RoutingMiddleware(mainRouter);
+        final pipeline = mainMiddleware.meddle(
+            (final RequestContext ctx) async =>
+                (ctx as RespondableContext).withResponse(FakeResponse(404)));
+
+        final initialCtx =
+            FakeRequest('/resource/abc/details/xyz').toContext(Object());
+        final resultingCtx = await pipeline(initialCtx);
+        final response =
+            (resultingCtx as ResponseContext).response as FakeResponse;
+
+        expect(nestedHandlerCalled, isTrue);
+        expect(response.statusCode, equals(200));
+        expect(capturedParams, isNotNull);
+        expect(capturedParams, equals({#resourceId: 'abc', #detailId: 'xyz'}));
path: test/middleware/routing_middleware_test.dart
user: login: tp
body: Interesting, given that the child route could not possibly know it will be mounted.

But at least the behavior is not specified and fixed.
created_at: 2025-05-16T07:30:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -18,48 +18,60 @@ This package was born out of the needs of [Serverpod](https://serverpod.dev), as
 - We made everything type-safe (no more dynamic).
 - Encoding types have been moved to the `Body` of a `Request`/`Response` to simplify the logic when syncing up the headers and to have a single source of truth.
 - We've added parsers and validation for all commonly used HTTP headers. E.g., times are represented by `DateTime`, cookies have their own class with validation of formatting, etc.
+- Routing has been implemented using a `PathTrie` structure for efficient route matching and parameter extraction.
path: README.md
user: login: nielsenko
body: Maybe I should just have said trie. The `PathTrie` is called that way, since it is a trie over path segments. It also handles dynamic segments - not sure how to put that in the name. I'll find a good link. A place to start is: https://en.wikipedia.org/wiki/Trie.

Dear child has many names. _Tries_ are also often called Prefix Trees.

I think it is quite common data-structure for routing. Both _routingkit_ and _spanner_ use some form of it as well.
created_at: 2025-05-16T07:37:56Z
author_association: COLLABORATOR
diff_hunk: @@ -18,48 +18,60 @@ This package was born out of the needs of [Serverpod](https://serverpod.dev), as
 - We made everything type-safe (no more dynamic).
 - Encoding types have been moved to the `Body` of a `Request`/`Response` to simplify the logic when syncing up the headers and to have a single source of truth.
 - We've added parsers and validation for all commonly used HTTP headers. E.g., times are represented by `DateTime`, cookies have their own class with validation of formatting, etc.
+- Routing has been implemented using a `PathTrie` structure for efficient route matching and parameter extraction.
path: README.md
user: login: nielsenko
body: I did consider doing a _Radix Tree_ (or _compressed prefix tree_) which is special kind of trie that compresses the path where possible. But I have a hunch that the added complexity doesn't really carry its weight for this use-case. I believe _spanner_ may do that, but we are still faster. 
created_at: 2025-05-16T07:41:52Z
author_association: COLLABORATOR
diff_hunk: @@ -1,23 +1,36 @@
 import 'dart:io';
 
 import 'package:relic/relic.dart';
+import 'package:relic/src/adapter/context.dart';
+import 'package:relic/src/middleware/routing_middleware.dart';
 
 /// A simple 'Hello World' server
 Future<void> main() async {
+  // Setup router
+  final router = Router<Handler>()..get('/user/:name/age/:age', hello);
+
   // Setup a handler.
   //
   // A [Handler] is function consuming and producing [RequestContext]s,
   // but if you are mostly concerned with converting [Request]s to [Response]s
   // (known as a [Responder] in relic parlor) you can use [respondWith] to
   // wrap a [Responder] into a [Handler]
-  final handler = respondWith(hello);
+  final handler = const Pipeline()
+      .addMiddleware(logRequests())
+      .addMiddleware(routeWith(router))
+      .addHandler(respondWith((final _) => Response.notFound(
+          body: Body.fromString("Sorry, that doesn't compute"))));
 
   // Start the server with the handler
   await serve(handler, InternetAddress.anyIPv4, 8080);
path: example/example.dart
user: login: nielsenko
body: I'm not sure I follow completely. 

Relic server will wrap you handler it a last ditch middleware, and turn most exceptions into 500 (or 400 if it is related to header parsing). We can elaborate on this last ditch middleware for sure.
created_at: 2025-05-16T07:44:27Z
author_association: COLLABORATOR
diff_hunk: @@ -1,23 +1,36 @@
 import 'dart:io';
 
 import 'package:relic/relic.dart';
+import 'package:relic/src/adapter/context.dart';
+import 'package:relic/src/middleware/routing_middleware.dart';
 
 /// A simple 'Hello World' server
 Future<void> main() async {
+  // Setup router
+  final router = Router<Handler>()..get('/user/:name/age/:age', hello);
+
   // Setup a handler.
   //
   // A [Handler] is function consuming and producing [RequestContext]s,
   // but if you are mostly concerned with converting [Request]s to [Response]s
   // (known as a [Responder] in relic parlor) you can use [respondWith] to
   // wrap a [Responder] into a [Handler]
-  final handler = respondWith(hello);
+  final handler = const Pipeline()
+      .addMiddleware(logRequests())
+      .addMiddleware(routeWith(router))
+      .addHandler(respondWith((final _) => Response.notFound(
+          body: Body.fromString("Sorry, that doesn't compute"))));
 
   // Start the server with the handler
   await serve(handler, InternetAddress.anyIPv4, 8080);
path: example/example.dart
user: login: nielsenko
body: And yes, you should wrap with top-level error handling middleware to control this behavior.
created_at: 2025-05-16T07:48:18Z
author_association: COLLABORATOR
diff_hunk: @@ -1,23 +1,36 @@
 import 'dart:io';
 
 import 'package:relic/relic.dart';
+import 'package:relic/src/adapter/context.dart';
+import 'package:relic/src/middleware/routing_middleware.dart';
 
 /// A simple 'Hello World' server
 Future<void> main() async {
+  // Setup router
+  final router = Router<Handler>()..get('/user/:name/age/:age', hello);
+
   // Setup a handler.
   //
   // A [Handler] is function consuming and producing [RequestContext]s,
   // but if you are mostly concerned with converting [Request]s to [Response]s
   // (known as a [Responder] in relic parlor) you can use [respondWith] to
   // wrap a [Responder] into a [Handler]
-  final handler = respondWith(hello);
+  final handler = const Pipeline()
path: example/example.dart
user: login: nielsenko
body: I like this "short style". Basically the `with` wraps with a middleware as I understand.
created_at: 2025-05-16T07:53:35Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,57 @@
+import '../adapter/context.dart';
+import '../handler/handler.dart';
+import '../method/request_method.dart';
+import '../router/router.dart';
+import 'middleware.dart';
+
+Middleware routeWith(final Router<Handler> router) =>
+    RoutingMiddleware(router).meddle;
+
+final _pathParametersStorage = Expando<Map<Symbol, String>>();
+
+class RoutingMiddleware {
+  final Router<Handler> _router;
+
+  RoutingMiddleware(this._router);
+
+  Handler meddle(final Handler next) {
path: lib/src/middleware/routing_middleware.dart
user: login: nielsenko
body: I have a `<X>Middleware` class that should produce a `Middleware` (`typedef Middleware = Handler Function(Handler)`). What should I call this function? I went with `meddle` since it is in the middle meddling with stuff. I'm open to suggestions 😄 
created_at: 2025-05-16T07:57:33Z
author_association: COLLABORATOR
diff_hunk: @@ -93,7 +93,8 @@ class RelicServer {
     }
 
     try {
-      final ctx = request.toContext();
+      final ctx = request
+          .toContext(adapterRequest); // adaptor request will be the token
path: lib/src/relic_server.dart
user: login: nielsenko
body: My english sits in the middle of the Atlantic ocean 😄 
created_at: 2025-05-16T07:58:28Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,423 @@
+import 'package:relic/relic.dart';
+import 'package:relic/src/adapter/context.dart';
+import 'package:relic/src/method/request_method.dart' as relic_req_method;
+import 'package:relic/src/middleware/routing_middleware.dart';
+import 'package:test/test.dart';
+
+// Simple fake implementations for testing
+class FakeRequest implements Request {
+  @override
+  final Uri url;
+  @override
+  final relic_req_method.RequestMethod method;
+
+  @override
+  dynamic noSuchMethod(final Invocation invocation) {
+    throw UnimplementedError(invocation.memberName.toString());
+  }
+
+  FakeRequest(final String path,
+      {this.method = relic_req_method.RequestMethod.get})
+      : url = Uri.parse('http://localhost$path');
+}
+
+class FakeResponse implements Response {
+  @override
+  final int statusCode;
+  final Map<Symbol, String>? parameters;
+
+  FakeResponse(this.statusCode, {this.parameters});
+
+  @override
+  dynamic noSuchMethod(final Invocation invocation) {
+    throw UnimplementedError(invocation.memberName.toString());
+  }
+}
+
+void main() {
+  group('RoutingMiddleware', () {
+    late Router<Handler> router;
+    late RoutingMiddleware middleware;
+
+    setUp(() {
+      router = Router<Handler>();
+      middleware = RoutingMiddleware(router);
+    });
+
+    group('Parameter Propagation', () {
+      test(
+          'Given a router with a parameterized route and RoutingMiddleware, '
+          'When a request matches the parameterized route, '
+          'Then the handler receives correct path parameters', () async {
+        Map<Symbol, String>? capturedParams;
+        Future<ResponseContext> testHandler(final RequestContext ctx) async {
+          capturedParams = ctx.pathParameters;
+          return (ctx as RespondableContext)
+              .withResponse(FakeResponse(200, parameters: capturedParams));
+        }
+
+        router.add(Method.get, '/users/:id', testHandler);
+
+        final initialCtx = FakeRequest('/users/123').toContext(Object());
+        final resultingCtx =
+            await middleware.meddle((final RequestContext ctx) async {
+          return (ctx as RespondableContext).withResponse(FakeResponse(404));
+        })(initialCtx);
+
+        expect(capturedParams, isNotNull);
+        expect(capturedParams, equals({#id: '123'}));
+        expect(resultingCtx, isA<ResponseContext>());
+        final response =
+            (resultingCtx as ResponseContext).response as FakeResponse;
+        expect(response.statusCode, equals(200));
+        expect(response.parameters, equals({#id: '123'}));
+      });
+
+      test(
+          'Given a router with a non-parameterized route and RoutingMiddleware, '
+          'When a request matches the non-parameterized route, '
+          'Then the handler receives empty path parameters', () async {
+        Map<Symbol, String>? capturedParams;
+        Future<ResponseContext> testHandler(final RequestContext ctx) async {
+          capturedParams = ctx.pathParameters;
+          return (ctx as RespondableContext)
+              .withResponse(FakeResponse(200, parameters: capturedParams));
+        }
+
+        router.add(Method.get, '/users', testHandler);
+
+        final initialCtx = FakeRequest('/users').toContext(Object());
+        final resultingCtx =
+            await middleware.meddle((final RequestContext ctx) async {
+          return (ctx as RespondableContext).withResponse(FakeResponse(404));
+        })(initialCtx);
+
+        expect(capturedParams, isNotNull);
+        expect(capturedParams, isEmpty);
+        expect(resultingCtx, isA<ResponseContext>());
+        final response =
+            (resultingCtx as ResponseContext).response as FakeResponse;
+        expect(response.statusCode, equals(200));
+        expect(response.parameters, isEmpty);
+      });
+
+      test(
+          'Given RoutingMiddleware and a request that does not match any route, '
+          'When the middleware processes the request, '
+          'Then the next handler is called and pathParameters access throws StateError',
+          () async {
+        bool nextCalled = false;
+        Future<ResponseContext> nextHandler(final RequestContext ctx) async {
+          nextCalled = true;
+          expect(() => ctx.pathParameters, throwsStateError);
+          return (ctx as RespondableContext).withResponse(FakeResponse(404));
+        }
+
+        final initialCtx = FakeRequest('/nonexistent').toContext(Object());
+        final resultingCtx = await middleware.meddle(nextHandler)(initialCtx);
+
+        expect(nextCalled, isTrue);
+        expect(resultingCtx, isA<ResponseContext>());
+        final response =
+            (resultingCtx as ResponseContext).response as FakeResponse;
+        expect(response.statusCode, equals(404));
+      });
+    });
+
+    group('Multiple RoutingMiddleware in Pipeline', () {
+      late Router<Handler> router1;
+      late RoutingMiddleware middleware1;
+      late Router<Handler> router2;
+      late RoutingMiddleware middleware2;
+
+      setUp(() {
+        router1 = Router<Handler>();
+        middleware1 = RoutingMiddleware(router1);
+        router2 = Router<Handler>();
+        middleware2 = RoutingMiddleware(router2);
+      });
+
+      test(
+          'Given two RoutingMiddleware instances in a pipeline, '
+          'When a request matches a route in the first router, '
+          'Then the handler from the first router is executed with correct parameters',
+          () async {
+        Map<Symbol, String>? params1;
+        bool handler1Called = false;
+        bool handler2Called = false;
+
+        router1.add(Method.get, '/router1/:item',
+            (final RequestContext ctx) async {
+          handler1Called = true;
+          params1 = ctx.pathParameters;
+          return (ctx as RespondableContext)
+              .withResponse(FakeResponse(201, parameters: params1));
+        });
+        router2.add(Method.get, '/router2/:item',
+            (final RequestContext ctx) async {
+          handler2Called = true;
+          return (ctx as RespondableContext).withResponse(FakeResponse(202));
+        });
+
+        final pipeline = middleware1.meddle(middleware2.meddle(
+            (final RequestContext ctx) async =>
+                (ctx as RespondableContext).withResponse(FakeResponse(404))));
+
+        final initialCtx = FakeRequest('/router1/apple').toContext(Object());
+        final resultingCtx = await pipeline(initialCtx);
+        final response =
+            (resultingCtx as ResponseContext).response as FakeResponse;
+
+        expect(handler1Called, isTrue);
+        expect(handler2Called, isFalse);
+        expect(response.statusCode, equals(201));
+        expect(params1, equals({#item: 'apple'}));
+        expect(response.parameters, equals({#item: 'apple'}));
+      });
+
+      test(
+          'Given two RoutingMiddleware instances in a pipeline, '
+          'When a request matches a route in the second router (but not the first), '
+          'Then the handler from the second router is executed with correct parameters',
+          () async {
+        Map<Symbol, String>? params2;
+        bool handler1Called = false;
+        bool handler2Called = false;
+
+        router1.add(Method.get, '/router1/:item',
+            (final RequestContext ctx) async {
+          handler1Called = true;
+          return (ctx as RespondableContext).withResponse(FakeResponse(201));
+        });
+        router2.add(Method.get, '/router2/:data',
+            (final RequestContext ctx) async {
+          handler2Called = true;
+          params2 = ctx.pathParameters;
+          return (ctx as RespondableContext)
+              .withResponse(FakeResponse(202, parameters: params2));
+        });
+
+        final pipeline = middleware1.meddle(middleware2.meddle(
+            (final RequestContext ctx) async =>
+                (ctx as RespondableContext).withResponse(FakeResponse(404))));
+
+        final initialCtx = FakeRequest('/router2/banana').toContext(Object());
+        final resultingCtx = await pipeline(initialCtx);
+        final response =
+            (resultingCtx as ResponseContext).response as FakeResponse;
+
+        expect(handler1Called, isFalse);
+        expect(handler2Called, isTrue);
+        expect(response.statusCode, equals(202));
+        expect(params2, equals({#data: 'banana'}));
+        expect(response.parameters, equals({#data: 'banana'}));
+      });
+
+      test(
+          'Given two RoutingMiddleware instances in a pipeline, '
+          'When a request does not match any route in either router, '
+          'Then the final next handler is called', () async {
+        bool handler1Called = false;
+        bool handler2Called = false;
+        bool fallbackCalled = false;
+
+        router1.add(Method.get, '/router1/:item',
+            (final RequestContext ctx) async {
+          handler1Called = true;
+          return (ctx as RespondableContext).withResponse(FakeResponse(201));
+        });
+        router2.add(Method.get, '/router2/:data',
path: test/middleware/routing_middleware_test.dart
user: login: nielsenko
body: Yes, literal matching takes precedence. It is tested yes
created_at: 2025-05-16T08:00:02Z
author_association: COLLABORATOR
diff_hunk: @@ -1,23 +1,36 @@
 import 'dart:io';
 
 import 'package:relic/relic.dart';
+import 'package:relic/src/adapter/context.dart';
+import 'package:relic/src/middleware/routing_middleware.dart';
 
 /// A simple 'Hello World' server
 Future<void> main() async {
+  // Setup router
+  final router = Router<Handler>()..get('/user/:name/age/:age', hello);
+
   // Setup a handler.
   //
   // A [Handler] is function consuming and producing [RequestContext]s,
   // but if you are mostly concerned with converting [Request]s to [Response]s
   // (known as a [Responder] in relic parlor) you can use [respondWith] to
   // wrap a [Responder] into a [Handler]
-  final handler = respondWith(hello);
+  final handler = const Pipeline()
+      .addMiddleware(logRequests())
+      .addMiddleware(routeWith(router))
+      .addHandler(respondWith((final _) => Response.notFound(
path: example/example.dart
user: login: nielsenko
body: Yes. Think of handler like an atoms and middleware as composites.

Also the typedefs for the two function types are quite revealing:
```dart
typedef Handler = RequestContext Function(RequestContext);
typedef Middleware = Handler Function(Handler);
```

So middleware eats and produces handlers.
created_at: 2025-05-16T08:25:01Z
author_association: COLLABORATOR
diff_hunk: @@ -1,23 +1,36 @@
 import 'dart:io';
 
 import 'package:relic/relic.dart';
+import 'package:relic/src/adapter/context.dart';
+import 'package:relic/src/middleware/routing_middleware.dart';
 
 /// A simple 'Hello World' server
 Future<void> main() async {
+  // Setup router
+  final router = Router<Handler>()..get('/user/:name/age/:age', hello);
+
   // Setup a handler.
   //
   // A [Handler] is function consuming and producing [RequestContext]s,
   // but if you are mostly concerned with converting [Request]s to [Response]s
   // (known as a [Responder] in relic parlor) you can use [respondWith] to
   // wrap a [Responder] into a [Handler]
-  final handler = respondWith(hello);
+  final handler = const Pipeline()
path: example/example.dart
user: login: nielsenko
body: @coderabbitai create an issue for this
created_at: 2025-05-16T08:33:33Z
author_association: COLLABORATOR
diff_hunk: @@ -1,23 +1,36 @@
 import 'dart:io';
 
 import 'package:relic/relic.dart';
+import 'package:relic/src/adapter/context.dart';
+import 'package:relic/src/middleware/routing_middleware.dart';
 
 /// A simple 'Hello World' server
 Future<void> main() async {
+  // Setup router
+  final router = Router<Handler>()..get('/user/:name/age/:age', hello);
+
   // Setup a handler.
   //
   // A [Handler] is function consuming and producing [RequestContext]s,
   // but if you are mostly concerned with converting [Request]s to [Response]s
   // (known as a [Responder] in relic parlor) you can use [respondWith] to
   // wrap a [Responder] into a [Handler]
-  final handler = respondWith(hello);
+  final handler = const Pipeline()
path: example/example.dart
user: login: coderabbitai[bot]
body: <details>
<summary>🧩 Analysis chain</summary>

@nielsenko I've created a new issue to track the implementation of a more concise, fluent API style for routing and middleware as discussed in this PR. The issue captures the suggestion from tp about implementing a Node-style syntax that would make the framework more approachable for beginners while maintaining all the power of the current implementation.

---

🏁 Script executed:
```
#!/bin/bash
# Create an issue for implementing a more concise API style for Relic
gh issue create \
  --title "Implement a concise, fluent API style for routing and middleware" \
  --body "## Background

As discussed in [PR #68](https://github.com/serverpod/relic/pull/68#discussion_r2092451197), the current API for setting up routing and middleware is functional but requires learning several concepts upfront.

## Proposal

Implement a more concise, Node-style API that would make the framework more approachable for beginners. For example:

\`\`\`js
const app = server()
   .get('/user/:name/age/:age', hello)
   .any('/', (_) => 'Not found')
   .with(logRequests);
\`\`\`

This fluent interface would simplify the common use case of setting up routes and middleware, while still leveraging the existing routing and middleware implementation underneath.

## Benefits

- Reduces boilerplate for simple applications
- More approachable for beginners
- Could provide a smooth transition path to the more advanced concepts
- Maintains all the power of the current implementation

## Related

This came from discussion in [PR #68](https://github.com/serverpod/relic/pull/68#discussion_r2092451197) which implements the routing middleware." \
  --assignee "nielsenko"
```

Length of output: 1228

---



</details>

@nielsenko I've created a new issue for you at https://github.com/serverpod/relic/issues/71. The issue outlines the proposal for implementing a more concise, fluent API style for routing and middleware as discussed by tp in the comments. I've included the example code and benefits of this approach, and assigned the issue to you.

---

<!-- This is an auto-generated comment: tweet message by coderabbit.ai -->

If you found this review helpful, would you consider giving us a shout-out on [X](https://twitter.com/intent/tweet?related=CodeRabbitAI&text=Want%20faster%20setup%20for%20your%20middleware%3F%20%40coderabbitai%20suggests%20a%20concise%20%22short%20style%22%20API%20that%20focuses%20on%20handling%20logic%2C%20making%20your%20code%20cleaner%20and%20quicker%20to%20start!%20%F0%9F%9A%80%E2%9C%A8%20Try%20it%20now!&url=https%3A%2F%2Fgithub.com%2Fserverpod%2Frelic%2Fpull%2F68%23discussion_r2092451197)?

Thank you for using CodeRabbit!

<!-- end of auto-generated comment: tweet message by coderabbit.ai -->


<!-- <agentComponentType>chat-answer</agentComponentType> -->

<!-- <rawResChunk><answer><details>
<summary>🧩 Analysis chain</summary>

@nielsenko I've created a new issue to track the implementation of a more concise, fluent API style for routing and middleware as discussed in this PR. The issue captures the suggestion from tp about implementing a Node-style syntax that would make the framework more approachable for beginners while maintaining all the power of the current implementation.

---

🏁 Script executed:
```
#!/bin/bash
# Create an issue for implementing a more concise API style for Relic
gh issue create \
  --title "Implement a concise, fluent API style for routing and middleware" \
  --body "## Background

As discussed in [PR #68](https://github.com/serverpod/relic/pull/68#discussion_r2092451197), the current API for setting up routing and middleware is functional but requires learning several concepts upfront.

## Proposal

Implement a more concise, Node-style API that would make the framework more approachable for beginners. For example:

\`\`\`js
const app = server()
   .get('/user/:name/age/:age', hello)
   .any('/', (_) => 'Not found')
   .with(logRequests);
\`\`\`

This fluent interface would simplify the common use case of setting up routes and middleware, while still leveraging the existing routing and middleware implementation underneath.

## Benefits

- Reduces boilerplate for simple applications
- More approachable for beginners
- Could provide a smooth transition path to the more advanced concepts
- Maintains all the power of the current implementation

## Related

This came from discussion in [PR #68](https://github.com/serverpod/relic/pull/68#discussion_r2092451197) which implements the routing middleware." \
  --assignee "nielsenko"
```

Length of output: 1228

---



</details>

@nielsenko I've created a new issue for you at https://github.com/serverpod/relic/issues/71. The issue outlines the proposal for implementing a more concise, fluent API style for routing and middleware as discussed by tp in the comments. I've included the example code and benefits of this approach, and assigned the issue to you.

---

&lt;!-- This is an auto-generated comment: tweet message by coderabbit.ai --&gt;

If you found this review helpful, would you consider giving us a shout-out on [X](https://twitter.com/intent/tweet?related=CodeRabbitAI&text=Want%20faster%20setup%20for%20your%20middleware%3F%20%40coderabbitai%20suggests%20a%20concise%20%22short%20style%22%20API%20that%20focuses%20on%20handling%20logic%2C%20making%20your%20code%20cleaner%20and%20quicker%20to%20start!%20%F0%9F%9A%80%E2%9C%A8%20Try%20it%20now!&url=https%3A%2F%2Fgithub.com%2Fserverpod%2Frelic%2Fpull%2F68%23discussion_r2092451197)?

Thank you for using CodeRabbit!

&lt;!-- end of auto-generated comment: tweet message by coderabbit.ai --&gt;

</answer></rawResChunk> -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-05-16T08:34:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,423 @@
+import 'package:relic/relic.dart';
+import 'package:relic/src/adapter/context.dart';
+import 'package:relic/src/method/request_method.dart' as relic_req_method;
+import 'package:relic/src/middleware/routing_middleware.dart';
+import 'package:test/test.dart';
+
+// Simple fake implementations for testing
+class FakeRequest implements Request {
+  @override
+  final Uri url;
+  @override
+  final relic_req_method.RequestMethod method;
+
+  @override
+  dynamic noSuchMethod(final Invocation invocation) {
+    throw UnimplementedError(invocation.memberName.toString());
+  }
+
+  FakeRequest(final String path,
+      {this.method = relic_req_method.RequestMethod.get})
+      : url = Uri.parse('http://localhost$path');
+}
+
+class FakeResponse implements Response {
+  @override
+  final int statusCode;
+  final Map<Symbol, String>? parameters;
+
+  FakeResponse(this.statusCode, {this.parameters});
+
+  @override
+  dynamic noSuchMethod(final Invocation invocation) {
+    throw UnimplementedError(invocation.memberName.toString());
+  }
+}
+
+void main() {
+  group('RoutingMiddleware', () {
+    late Router<Handler> router;
+    late RoutingMiddleware middleware;
+
+    setUp(() {
+      router = Router<Handler>();
+      middleware = RoutingMiddleware(router);
+    });
+
+    group('Parameter Propagation', () {
+      test(
+          'Given a router with a parameterized route and RoutingMiddleware, '
+          'When a request matches the parameterized route, '
+          'Then the handler receives correct path parameters', () async {
+        Map<Symbol, String>? capturedParams;
+        Future<ResponseContext> testHandler(final RequestContext ctx) async {
+          capturedParams = ctx.pathParameters;
+          return (ctx as RespondableContext)
+              .withResponse(FakeResponse(200, parameters: capturedParams));
+        }
+
+        router.add(Method.get, '/users/:id', testHandler);
+
+        final initialCtx = FakeRequest('/users/123').toContext(Object());
+        final resultingCtx =
+            await middleware.meddle((final RequestContext ctx) async {
+          return (ctx as RespondableContext).withResponse(FakeResponse(404));
+        })(initialCtx);
+
+        expect(capturedParams, isNotNull);
+        expect(capturedParams, equals({#id: '123'}));
+        expect(resultingCtx, isA<ResponseContext>());
+        final response =
+            (resultingCtx as ResponseContext).response as FakeResponse;
+        expect(response.statusCode, equals(200));
+        expect(response.parameters, equals({#id: '123'}));
+      });
+
+      test(
+          'Given a router with a non-parameterized route and RoutingMiddleware, '
+          'When a request matches the non-parameterized route, '
+          'Then the handler receives empty path parameters', () async {
+        Map<Symbol, String>? capturedParams;
+        Future<ResponseContext> testHandler(final RequestContext ctx) async {
+          capturedParams = ctx.pathParameters;
+          return (ctx as RespondableContext)
+              .withResponse(FakeResponse(200, parameters: capturedParams));
+        }
+
+        router.add(Method.get, '/users', testHandler);
+
+        final initialCtx = FakeRequest('/users').toContext(Object());
+        final resultingCtx =
+            await middleware.meddle((final RequestContext ctx) async {
+          return (ctx as RespondableContext).withResponse(FakeResponse(404));
path: test/middleware/routing_middleware_test.dart
user: login: nielsenko
body: I'll refactor to use `Pipeline` and `routeWith` for clarity.
created_at: 2025-05-16T08:34:39Z
author_association: COLLABORATOR
diff_hunk: @@ -1,23 +1,36 @@
 import 'dart:io';
 
 import 'package:relic/relic.dart';
+import 'package:relic/src/adapter/context.dart';
+import 'package:relic/src/middleware/routing_middleware.dart';
 
 /// A simple 'Hello World' server
 Future<void> main() async {
+  // Setup router
+  final router = Router<Handler>()..get('/user/:name/age/:age', hello);
+
   // Setup a handler.
   //
   // A [Handler] is function consuming and producing [RequestContext]s,
   // but if you are mostly concerned with converting [Request]s to [Response]s
   // (known as a [Responder] in relic parlor) you can use [respondWith] to
   // wrap a [Responder] into a [Handler]
-  final handler = respondWith(hello);
+  final handler = const Pipeline()
+      .addMiddleware(logRequests())
+      .addMiddleware(routeWith(router))
+      .addHandler(respondWith((final _) => Response.notFound(
+          body: Body.fromString("Sorry, that doesn't compute"))));
 
   // Start the server with the handler
   await serve(handler, InternetAddress.anyIPv4, 8080);
path: example/example.dart
user: login: tp
body: Ups, I was mistaken 😅 The detailed error message is only logged to the server's console, not the response.

![CleanShot 2025-05-16 at 10 37 23@2x](https://github.com/user-attachments/assets/024021ce-3f49-4b20-b130-7299831c9ce0)


My bad, current approach seems good 👍
created_at: 2025-05-16T08:38:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,23 +1,36 @@
 import 'dart:io';
 
 import 'package:relic/relic.dart';
+import 'package:relic/src/adapter/context.dart';
+import 'package:relic/src/middleware/routing_middleware.dart';
 
 /// A simple 'Hello World' server
 Future<void> main() async {
+  // Setup router
+  final router = Router<Handler>()..get('/user/:name/age/:age', hello);
+
   // Setup a handler.
   //
   // A [Handler] is function consuming and producing [RequestContext]s,
   // but if you are mostly concerned with converting [Request]s to [Response]s
   // (known as a [Responder] in relic parlor) you can use [respondWith] to
   // wrap a [Responder] into a [Handler]
-  final handler = respondWith(hello);
+  final handler = const Pipeline()
path: example/example.dart
user: login: tp
body: > I like this "short style". Basically the with wraps with a middleware as I understand.

Exactly. Concrete naming TBD, what I like about `Pipeline` I that the order is very clear (top to bottom -> outside in).

The `with` in the example above is a bit misleading in that `logRequest` would actually be the first thing to run. But maybe that is just the usual distinction between Handler and Middleware. And the whole builder set up should ensure that after a `with` not more handler is added (and vice versa), so at least on each Router-helper (like `app`/`server` above) there would only be one order in which things are registered.
created_at: 2025-05-16T08:41:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,23 +1,36 @@
 import 'dart:io';
 
 import 'package:relic/relic.dart';
+import 'package:relic/src/adapter/context.dart';
+import 'package:relic/src/middleware/routing_middleware.dart';
 
 /// A simple 'Hello World' server
 Future<void> main() async {
+  // Setup router
+  final router = Router<Handler>()..get('/user/:name/age/:age', hello);
+
   // Setup a handler.
   //
   // A [Handler] is function consuming and producing [RequestContext]s,
   // but if you are mostly concerned with converting [Request]s to [Response]s
   // (known as a [Responder] in relic parlor) you can use [respondWith] to
   // wrap a [Responder] into a [Handler]
-  final handler = respondWith(hello);
+  final handler = const Pipeline()
path: example/example.dart
user: login: coderabbitai[bot]
body: > Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

<!-- This is an auto-generated mute comment by CodeRabbit -->
created_at: 2025-05-16T08:41:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,423 @@
+import 'package:relic/relic.dart';
+import 'package:relic/src/adapter/context.dart';
+import 'package:relic/src/method/request_method.dart' as relic_req_method;
+import 'package:relic/src/middleware/routing_middleware.dart';
+import 'package:test/test.dart';
+
+// Simple fake implementations for testing
+class FakeRequest implements Request {
+  @override
+  final Uri url;
+  @override
+  final relic_req_method.RequestMethod method;
+
+  @override
+  dynamic noSuchMethod(final Invocation invocation) {
+    throw UnimplementedError(invocation.memberName.toString());
+  }
+
+  FakeRequest(final String path,
+      {this.method = relic_req_method.RequestMethod.get})
+      : url = Uri.parse('http://localhost$path');
+}
+
+class FakeResponse implements Response {
+  @override
+  final int statusCode;
+  final Map<Symbol, String>? parameters;
+
+  FakeResponse(this.statusCode, {this.parameters});
+
+  @override
+  dynamic noSuchMethod(final Invocation invocation) {
+    throw UnimplementedError(invocation.memberName.toString());
+  }
+}
+
+void main() {
+  group('RoutingMiddleware', () {
+    late Router<Handler> router;
+    late RoutingMiddleware middleware;
+
+    setUp(() {
+      router = Router<Handler>();
+      middleware = RoutingMiddleware(router);
+    });
+
+    group('Parameter Propagation', () {
+      test(
+          'Given a router with a parameterized route and RoutingMiddleware, '
+          'When a request matches the parameterized route, '
+          'Then the handler receives correct path parameters', () async {
+        Map<Symbol, String>? capturedParams;
+        Future<ResponseContext> testHandler(final RequestContext ctx) async {
+          capturedParams = ctx.pathParameters;
+          return (ctx as RespondableContext)
+              .withResponse(FakeResponse(200, parameters: capturedParams));
+        }
+
+        router.add(Method.get, '/users/:id', testHandler);
+
+        final initialCtx = FakeRequest('/users/123').toContext(Object());
+        final resultingCtx =
+            await middleware.meddle((final RequestContext ctx) async {
+          return (ctx as RespondableContext).withResponse(FakeResponse(404));
+        })(initialCtx);
+
+        expect(capturedParams, isNotNull);
+        expect(capturedParams, equals({#id: '123'}));
+        expect(resultingCtx, isA<ResponseContext>());
+        final response =
+            (resultingCtx as ResponseContext).response as FakeResponse;
+        expect(response.statusCode, equals(200));
+        expect(response.parameters, equals({#id: '123'}));
+      });
+
+      test(
+          'Given a router with a non-parameterized route and RoutingMiddleware, '
+          'When a request matches the non-parameterized route, '
+          'Then the handler receives empty path parameters', () async {
+        Map<Symbol, String>? capturedParams;
+        Future<ResponseContext> testHandler(final RequestContext ctx) async {
+          capturedParams = ctx.pathParameters;
+          return (ctx as RespondableContext)
+              .withResponse(FakeResponse(200, parameters: capturedParams));
+        }
+
+        router.add(Method.get, '/users', testHandler);
+
+        final initialCtx = FakeRequest('/users').toContext(Object());
+        final resultingCtx =
+            await middleware.meddle((final RequestContext ctx) async {
+          return (ctx as RespondableContext).withResponse(FakeResponse(404));
+        })(initialCtx);
+
+        expect(capturedParams, isNotNull);
+        expect(capturedParams, isEmpty);
+        expect(resultingCtx, isA<ResponseContext>());
+        final response =
+            (resultingCtx as ResponseContext).response as FakeResponse;
+        expect(response.statusCode, equals(200));
+        expect(response.parameters, isEmpty);
+      });
+
+      test(
+          'Given RoutingMiddleware and a request that does not match any route, '
+          'When the middleware processes the request, '
+          'Then the next handler is called and pathParameters access throws StateError',
+          () async {
+        bool nextCalled = false;
+        Future<ResponseContext> nextHandler(final RequestContext ctx) async {
+          nextCalled = true;
+          expect(() => ctx.pathParameters, throwsStateError);
+          return (ctx as RespondableContext).withResponse(FakeResponse(404));
+        }
+
+        final initialCtx = FakeRequest('/nonexistent').toContext(Object());
+        final resultingCtx = await middleware.meddle(nextHandler)(initialCtx);
+
+        expect(nextCalled, isTrue);
+        expect(resultingCtx, isA<ResponseContext>());
+        final response =
+            (resultingCtx as ResponseContext).response as FakeResponse;
+        expect(response.statusCode, equals(404));
+      });
+    });
+
+    group('Multiple RoutingMiddleware in Pipeline', () {
+      late Router<Handler> router1;
+      late RoutingMiddleware middleware1;
+      late Router<Handler> router2;
+      late RoutingMiddleware middleware2;
+
+      setUp(() {
+        router1 = Router<Handler>();
+        middleware1 = RoutingMiddleware(router1);
+        router2 = Router<Handler>();
+        middleware2 = RoutingMiddleware(router2);
+      });
+
+      test(
+          'Given two RoutingMiddleware instances in a pipeline, '
+          'When a request matches a route in the first router, '
+          'Then the handler from the first router is executed with correct parameters',
+          () async {
+        Map<Symbol, String>? params1;
+        bool handler1Called = false;
+        bool handler2Called = false;
+
+        router1.add(Method.get, '/router1/:item',
+            (final RequestContext ctx) async {
+          handler1Called = true;
+          params1 = ctx.pathParameters;
+          return (ctx as RespondableContext)
+              .withResponse(FakeResponse(201, parameters: params1));
+        });
+        router2.add(Method.get, '/router2/:item',
+            (final RequestContext ctx) async {
+          handler2Called = true;
+          return (ctx as RespondableContext).withResponse(FakeResponse(202));
+        });
+
+        final pipeline = middleware1.meddle(middleware2.meddle(
+            (final RequestContext ctx) async =>
+                (ctx as RespondableContext).withResponse(FakeResponse(404))));
+
+        final initialCtx = FakeRequest('/router1/apple').toContext(Object());
+        final resultingCtx = await pipeline(initialCtx);
+        final response =
+            (resultingCtx as ResponseContext).response as FakeResponse;
+
+        expect(handler1Called, isTrue);
+        expect(handler2Called, isFalse);
+        expect(response.statusCode, equals(201));
+        expect(params1, equals({#item: 'apple'}));
+        expect(response.parameters, equals({#item: 'apple'}));
+      });
+
+      test(
+          'Given two RoutingMiddleware instances in a pipeline, '
+          'When a request matches a route in the second router (but not the first), '
+          'Then the handler from the second router is executed with correct parameters',
+          () async {
+        Map<Symbol, String>? params2;
+        bool handler1Called = false;
+        bool handler2Called = false;
+
+        router1.add(Method.get, '/router1/:item',
+            (final RequestContext ctx) async {
+          handler1Called = true;
+          return (ctx as RespondableContext).withResponse(FakeResponse(201));
+        });
+        router2.add(Method.get, '/router2/:data',
+            (final RequestContext ctx) async {
+          handler2Called = true;
+          params2 = ctx.pathParameters;
+          return (ctx as RespondableContext)
+              .withResponse(FakeResponse(202, parameters: params2));
+        });
+
+        final pipeline = middleware1.meddle(middleware2.meddle(
+            (final RequestContext ctx) async =>
+                (ctx as RespondableContext).withResponse(FakeResponse(404))));
+
+        final initialCtx = FakeRequest('/router2/banana').toContext(Object());
+        final resultingCtx = await pipeline(initialCtx);
+        final response =
+            (resultingCtx as ResponseContext).response as FakeResponse;
+
+        expect(handler1Called, isFalse);
+        expect(handler2Called, isTrue);
+        expect(response.statusCode, equals(202));
+        expect(params2, equals({#data: 'banana'}));
+        expect(response.parameters, equals({#data: 'banana'}));
+      });
+
+      test(
+          'Given two RoutingMiddleware instances in a pipeline, '
+          'When a request does not match any route in either router, '
+          'Then the final next handler is called', () async {
+        bool handler1Called = false;
+        bool handler2Called = false;
+        bool fallbackCalled = false;
+
+        router1.add(Method.get, '/router1/:item',
+            (final RequestContext ctx) async {
+          handler1Called = true;
+          return (ctx as RespondableContext).withResponse(FakeResponse(201));
+        });
+        router2.add(Method.get, '/router2/:data',
+            (final RequestContext ctx) async {
+          handler2Called = true;
+          return (ctx as RespondableContext).withResponse(FakeResponse(202));
+        });
+
+        final pipeline = middleware1
+            .meddle(middleware2.meddle((final RequestContext ctx) async {
+          fallbackCalled = true;
+          return (ctx as RespondableContext).withResponse(FakeResponse(404));
+        }));
+
+        final initialCtx = FakeRequest('/neither/nor').toContext(Object());
+        final resultingCtx = await pipeline(initialCtx);
+        final response =
+            (resultingCtx as ResponseContext).response as FakeResponse;
+
+        expect(handler1Called, isFalse);
+        expect(handler2Called, isFalse);
+        expect(fallbackCalled, isTrue);
+        expect(response.statusCode, equals(404));
+      });
+    });
+
+    group('Nested RoutingMiddleware (via Router.attach)', () {
+      test(
+          'Given a main Router with a nested Router attached, and RoutingMiddleware for the main Router, '
+          'When a request matches a route within the nested Router, '
+          'Then the handler from the nested Router is executed with merged path parameters',
+          () async {
+        Map<Symbol, String>? capturedParams;
+        bool nestedHandlerCalled = false;
+
+        final mainRouter = Router<Handler>();
+        final nestedRouter = Router<Handler>();
+
+        nestedRouter.add(Method.get, '/details/:detailId',
+            (final RequestContext ctx) async {
+          nestedHandlerCalled = true;
+          capturedParams = ctx.pathParameters;
+          return (ctx as RespondableContext)
+              .withResponse(FakeResponse(200, parameters: capturedParams));
+        });
+
+        // Attach nestedRouter to mainRouter under /resource/:resourceId
+        mainRouter.attach('/resource/:resourceId', nestedRouter);
+
+        final mainMiddleware = RoutingMiddleware(mainRouter);
+        final pipeline = mainMiddleware.meddle(
+            (final RequestContext ctx) async =>
+                (ctx as RespondableContext).withResponse(FakeResponse(404)));
+
+        final initialCtx =
+            FakeRequest('/resource/abc/details/xyz').toContext(Object());
+        final resultingCtx = await pipeline(initialCtx);
+        final response =
+            (resultingCtx as ResponseContext).response as FakeResponse;
+
+        expect(nestedHandlerCalled, isTrue);
+        expect(response.statusCode, equals(200));
+        expect(capturedParams, isNotNull);
+        expect(capturedParams, equals({#resourceId: 'abc', #detailId: 'xyz'}));
path: test/middleware/routing_middleware_test.dart
user: login: nielsenko
body: Yes, this speaks to the difference between attaching routers as sub-routers of each other, and nesting middleware.
Since routers currently consume the full path, you have to attach them to nest, but it really should be possible to nest without attaching, and the nested router should only see the _remaining path_, and hence the parameters in the _remaining_ path.

This is something to revisit once #70 lands.
created_at: 2025-05-16T08:42:36Z
author_association: COLLABORATOR
diff_hunk: @@ -18,48 +18,60 @@ This package was born out of the needs of [Serverpod](https://serverpod.dev), as
 - We made everything type-safe (no more dynamic).
 - Encoding types have been moved to the `Body` of a `Request`/`Response` to simplify the logic when syncing up the headers and to have a single source of truth.
 - We've added parsers and validation for all commonly used HTTP headers. E.g., times are represented by `DateTime`, cookies have their own class with validation of formatting, etc.
+- Routing has been implemented using a `PathTrie` structure for efficient route matching and parameter extraction.
path: README.md
user: login: nielsenko
body: I'll update the readme.
created_at: 2025-05-16T08:50:13Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,423 @@
+import 'package:relic/relic.dart';
+import 'package:relic/src/adapter/context.dart';
+import 'package:relic/src/method/request_method.dart' as relic_req_method;
+import 'package:relic/src/middleware/routing_middleware.dart';
+import 'package:test/test.dart';
+
+// Simple fake implementations for testing
+class FakeRequest implements Request {
+  @override
+  final Uri url;
+  @override
+  final relic_req_method.RequestMethod method;
+
+  @override
+  dynamic noSuchMethod(final Invocation invocation) {
+    throw UnimplementedError(invocation.memberName.toString());
+  }
+
+  FakeRequest(final String path,
+      {this.method = relic_req_method.RequestMethod.get})
+      : url = Uri.parse('http://localhost$path');
+}
+
+class FakeResponse implements Response {
+  @override
+  final int statusCode;
+  final Map<Symbol, String>? parameters;
+
+  FakeResponse(this.statusCode, {this.parameters});
+
+  @override
+  dynamic noSuchMethod(final Invocation invocation) {
+    throw UnimplementedError(invocation.memberName.toString());
+  }
+}
+
+void main() {
+  group('RoutingMiddleware', () {
+    late Router<Handler> router;
+    late RoutingMiddleware middleware;
+
+    setUp(() {
+      router = Router<Handler>();
+      middleware = RoutingMiddleware(router);
+    });
+
+    group('Parameter Propagation', () {
+      test(
+          'Given a router with a parameterized route and RoutingMiddleware, '
+          'When a request matches the parameterized route, '
+          'Then the handler receives correct path parameters', () async {
+        Map<Symbol, String>? capturedParams;
+        Future<ResponseContext> testHandler(final RequestContext ctx) async {
+          capturedParams = ctx.pathParameters;
+          return (ctx as RespondableContext)
+              .withResponse(FakeResponse(200, parameters: capturedParams));
+        }
+
+        router.add(Method.get, '/users/:id', testHandler);
+
+        final initialCtx = FakeRequest('/users/123').toContext(Object());
+        final resultingCtx =
+            await middleware.meddle((final RequestContext ctx) async {
+          return (ctx as RespondableContext).withResponse(FakeResponse(404));
+        })(initialCtx);
+
+        expect(capturedParams, isNotNull);
+        expect(capturedParams, equals({#id: '123'}));
+        expect(resultingCtx, isA<ResponseContext>());
+        final response =
+            (resultingCtx as ResponseContext).response as FakeResponse;
+        expect(response.statusCode, equals(200));
+        expect(response.parameters, equals({#id: '123'}));
+      });
+
+      test(
+          'Given a router with a non-parameterized route and RoutingMiddleware, '
+          'When a request matches the non-parameterized route, '
+          'Then the handler receives empty path parameters', () async {
+        Map<Symbol, String>? capturedParams;
+        Future<ResponseContext> testHandler(final RequestContext ctx) async {
+          capturedParams = ctx.pathParameters;
+          return (ctx as RespondableContext)
+              .withResponse(FakeResponse(200, parameters: capturedParams));
+        }
+
+        router.add(Method.get, '/users', testHandler);
+
+        final initialCtx = FakeRequest('/users').toContext(Object());
+        final resultingCtx =
+            await middleware.meddle((final RequestContext ctx) async {
+          return (ctx as RespondableContext).withResponse(FakeResponse(404));
path: test/middleware/routing_middleware_test.dart
user: login: nielsenko
body: Done
created_at: 2025-05-16T09:45:23Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,423 @@
+import 'package:relic/relic.dart';
+import 'package:relic/src/adapter/context.dart';
+import 'package:relic/src/method/request_method.dart' as relic_req_method;
+import 'package:relic/src/middleware/routing_middleware.dart';
+import 'package:test/test.dart';
+
+// Simple fake implementations for testing
+class FakeRequest implements Request {
+  @override
+  final Uri url;
+  @override
+  final relic_req_method.RequestMethod method;
+
+  @override
+  dynamic noSuchMethod(final Invocation invocation) {
+    throw UnimplementedError(invocation.memberName.toString());
+  }
+
+  FakeRequest(final String path,
+      {this.method = relic_req_method.RequestMethod.get})
+      : url = Uri.parse('http://localhost$path');
+}
+
+class FakeResponse implements Response {
+  @override
+  final int statusCode;
+  final Map<Symbol, String>? parameters;
+
+  FakeResponse(this.statusCode, {this.parameters});
+
+  @override
+  dynamic noSuchMethod(final Invocation invocation) {
+    throw UnimplementedError(invocation.memberName.toString());
+  }
+}
+
+void main() {
+  group('RoutingMiddleware', () {
+    late Router<Handler> router;
+    late RoutingMiddleware middleware;
+
+    setUp(() {
+      router = Router<Handler>();
+      middleware = RoutingMiddleware(router);
+    });
+
+    group('Parameter Propagation', () {
+      test(
+          'Given a router with a parameterized route and RoutingMiddleware, '
+          'When a request matches the parameterized route, '
+          'Then the handler receives correct path parameters', () async {
+        Map<Symbol, String>? capturedParams;
+        Future<ResponseContext> testHandler(final RequestContext ctx) async {
+          capturedParams = ctx.pathParameters;
+          return (ctx as RespondableContext)
+              .withResponse(FakeResponse(200, parameters: capturedParams));
+        }
+
+        router.add(Method.get, '/users/:id', testHandler);
+
+        final initialCtx = FakeRequest('/users/123').toContext(Object());
+        final resultingCtx =
+            await middleware.meddle((final RequestContext ctx) async {
+          return (ctx as RespondableContext).withResponse(FakeResponse(404));
+        })(initialCtx);
+
+        expect(capturedParams, isNotNull);
+        expect(capturedParams, equals({#id: '123'}));
+        expect(resultingCtx, isA<ResponseContext>());
+        final response =
+            (resultingCtx as ResponseContext).response as FakeResponse;
+        expect(response.statusCode, equals(200));
+        expect(response.parameters, equals({#id: '123'}));
path: test/middleware/routing_middleware_test.dart
user: login: nielsenko
body: I no longer use the `_FakeResponse.parameters` in the test, and hence I have removed the class completely and just use the regular `Response` class.
created_at: 2025-05-16T10:08:32Z
author_association: COLLABORATOR
diff_hunk: @@ -18,48 +18,60 @@ This package was born out of the needs of [Serverpod](https://serverpod.dev), as
 - We made everything type-safe (no more dynamic).
 - Encoding types have been moved to the `Body` of a `Request`/`Response` to simplify the logic when syncing up the headers and to have a single source of truth.
 - We've added parsers and validation for all commonly used HTTP headers. E.g., times are represented by `DateTime`, cookies have their own class with validation of formatting, etc.
+- Routing has been implemented using a `PathTrie` structure for efficient route matching and parameter extraction.
path: README.md
user: login: nielsenko
body: Done
created_at: 2025-05-16T10:22:57Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,423 @@
+import 'package:relic/relic.dart';
+import 'package:relic/src/adapter/context.dart';
+import 'package:relic/src/method/request_method.dart' as relic_req_method;
+import 'package:relic/src/middleware/routing_middleware.dart';
+import 'package:test/test.dart';
+
+// Simple fake implementations for testing
+class FakeRequest implements Request {
+  @override
+  final Uri url;
+  @override
+  final relic_req_method.RequestMethod method;
+
+  @override
+  dynamic noSuchMethod(final Invocation invocation) {
+    throw UnimplementedError(invocation.memberName.toString());
+  }
+
+  FakeRequest(final String path,
+      {this.method = relic_req_method.RequestMethod.get})
+      : url = Uri.parse('http://localhost$path');
+}
+
+class FakeResponse implements Response {
+  @override
+  final int statusCode;
+  final Map<Symbol, String>? parameters;
+
+  FakeResponse(this.statusCode, {this.parameters});
+
+  @override
+  dynamic noSuchMethod(final Invocation invocation) {
+    throw UnimplementedError(invocation.memberName.toString());
+  }
+}
+
+void main() {
+  group('RoutingMiddleware', () {
+    late Router<Handler> router;
+    late RoutingMiddleware middleware;
+
+    setUp(() {
+      router = Router<Handler>();
+      middleware = RoutingMiddleware(router);
+    });
+
+    group('Parameter Propagation', () {
+      test(
+          'Given a router with a parameterized route and RoutingMiddleware, '
+          'When a request matches the parameterized route, '
+          'Then the handler receives correct path parameters', () async {
+        Map<Symbol, String>? capturedParams;
+        Future<ResponseContext> testHandler(final RequestContext ctx) async {
+          capturedParams = ctx.pathParameters;
+          return (ctx as RespondableContext)
+              .withResponse(FakeResponse(200, parameters: capturedParams));
+        }
+
+        router.add(Method.get, '/users/:id', testHandler);
+
+        final initialCtx = FakeRequest('/users/123').toContext(Object());
+        final resultingCtx =
+            await middleware.meddle((final RequestContext ctx) async {
+          return (ctx as RespondableContext).withResponse(FakeResponse(404));
+        })(initialCtx);
+
+        expect(capturedParams, isNotNull);
+        expect(capturedParams, equals({#id: '123'}));
+        expect(resultingCtx, isA<ResponseContext>());
+        final response =
+            (resultingCtx as ResponseContext).response as FakeResponse;
+        expect(response.statusCode, equals(200));
+        expect(response.parameters, equals({#id: '123'}));
+      });
+
+      test(
+          'Given a router with a non-parameterized route and RoutingMiddleware, '
+          'When a request matches the non-parameterized route, '
+          'Then the handler receives empty path parameters', () async {
+        Map<Symbol, String>? capturedParams;
+        Future<ResponseContext> testHandler(final RequestContext ctx) async {
+          capturedParams = ctx.pathParameters;
+          return (ctx as RespondableContext)
+              .withResponse(FakeResponse(200, parameters: capturedParams));
+        }
+
+        router.add(Method.get, '/users', testHandler);
+
+        final initialCtx = FakeRequest('/users').toContext(Object());
+        final resultingCtx =
+            await middleware.meddle((final RequestContext ctx) async {
+          return (ctx as RespondableContext).withResponse(FakeResponse(404));
path: test/middleware/routing_middleware_test.dart
user: login: tp
body: Thanks!
created_at: 2025-05-16T10:40:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,423 @@
+import 'package:relic/relic.dart';
+import 'package:relic/src/adapter/context.dart';
+import 'package:relic/src/method/request_method.dart' as relic_req_method;
+import 'package:relic/src/middleware/routing_middleware.dart';
+import 'package:test/test.dart';
+
+// Simple fake implementations for testing
+class FakeRequest implements Request {
+  @override
+  final Uri url;
+  @override
+  final relic_req_method.RequestMethod method;
+
+  @override
+  dynamic noSuchMethod(final Invocation invocation) {
+    throw UnimplementedError(invocation.memberName.toString());
+  }
+
+  FakeRequest(final String path,
+      {this.method = relic_req_method.RequestMethod.get})
+      : url = Uri.parse('http://localhost$path');
+}
+
+class FakeResponse implements Response {
+  @override
+  final int statusCode;
+  final Map<Symbol, String>? parameters;
+
+  FakeResponse(this.statusCode, {this.parameters});
+
+  @override
+  dynamic noSuchMethod(final Invocation invocation) {
+    throw UnimplementedError(invocation.memberName.toString());
+  }
+}
+
+void main() {
+  group('RoutingMiddleware', () {
+    late Router<Handler> router;
+    late RoutingMiddleware middleware;
+
+    setUp(() {
+      router = Router<Handler>();
+      middleware = RoutingMiddleware(router);
+    });
+
+    group('Parameter Propagation', () {
+      test(
+          'Given a router with a parameterized route and RoutingMiddleware, '
+          'When a request matches the parameterized route, '
+          'Then the handler receives correct path parameters', () async {
+        Map<Symbol, String>? capturedParams;
+        Future<ResponseContext> testHandler(final RequestContext ctx) async {
+          capturedParams = ctx.pathParameters;
+          return (ctx as RespondableContext)
+              .withResponse(FakeResponse(200, parameters: capturedParams));
+        }
+
+        router.add(Method.get, '/users/:id', testHandler);
+
+        final initialCtx = FakeRequest('/users/123').toContext(Object());
+        final resultingCtx =
+            await middleware.meddle((final RequestContext ctx) async {
+          return (ctx as RespondableContext).withResponse(FakeResponse(404));
+        })(initialCtx);
+
+        expect(capturedParams, isNotNull);
+        expect(capturedParams, equals({#id: '123'}));
+        expect(resultingCtx, isA<ResponseContext>());
+        final response =
+            (resultingCtx as ResponseContext).response as FakeResponse;
+        expect(response.statusCode, equals(200));
+        expect(response.parameters, equals({#id: '123'}));
path: test/middleware/routing_middleware_test.dart
user: login: tp
body: Nice, yeah, that simplifies comprehension in my eyes :)
created_at: 2025-05-16T10:40:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,23 +1,36 @@
 import 'dart:io';
 
 import 'package:relic/relic.dart';
+import 'package:relic/src/adapter/context.dart';
+import 'package:relic/src/middleware/routing_middleware.dart';
 
 /// A simple 'Hello World' server
 Future<void> main() async {
+  // Setup router
+  final router = Router<Handler>()..get('/user/:name/age/:age', hello);
+
   // Setup a handler.
   //
   // A [Handler] is function consuming and producing [RequestContext]s,
   // but if you are mostly concerned with converting [Request]s to [Response]s
   // (known as a [Responder] in relic parlor) you can use [respondWith] to
   // wrap a [Responder] into a [Handler]
-  final handler = respondWith(hello);
+  final handler = const Pipeline()
+      .addMiddleware(logRequests())
+      .addMiddleware(routeWith(router))
+      .addHandler(respondWith((final _) => Response.notFound(
path: example/example.dart
user: login: tp
body: Is there any other way to get from the `Pipeline` to a `Handler` to be used for `serve`?

![CleanShot 2025-05-16 at 12 41 41@2x](https://github.com/user-attachments/assets/4d693ad7-5a8e-41e6-a1ee-f306c088fb46)


That's why I was suggestion that final `build(withFallback: Handler)` call or similar in Slack.
created_at: 2025-05-16T10:42:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,23 +1,36 @@
 import 'dart:io';
 
 import 'package:relic/relic.dart';
+import 'package:relic/src/adapter/context.dart';
+import 'package:relic/src/middleware/routing_middleware.dart';
 
 /// A simple 'Hello World' server
 Future<void> main() async {
+  // Setup router
+  final router = Router<Handler>()..get('/user/:name/age/:age', hello);
+
   // Setup a handler.
   //
   // A [Handler] is function consuming and producing [RequestContext]s,
   // but if you are mostly concerned with converting [Request]s to [Response]s
   // (known as a [Responder] in relic parlor) you can use [respondWith] to
   // wrap a [Responder] into a [Handler]
-  final handler = respondWith(hello);
+  final handler = const Pipeline()
+      .addMiddleware(logRequests())
+      .addMiddleware(routeWith(router))
+      .addHandler(respondWith((final _) => Response.notFound(
path: example/example.dart
user: login: nielsenko
body: Not today. This is inherited from shelf.
created_at: 2025-05-16T11:06:23Z
author_association: COLLABORATOR
diff_hunk: @@ -1,23 +1,36 @@
 import 'dart:io';
 
 import 'package:relic/relic.dart';
+import 'package:relic/src/adapter/context.dart';
+import 'package:relic/src/middleware/routing_middleware.dart';
 
 /// A simple 'Hello World' server
 Future<void> main() async {
+  // Setup router
+  final router = Router<Handler>()..get('/user/:name/age/:age', hello);
+
   // Setup a handler.
   //
   // A [Handler] is function consuming and producing [RequestContext]s,
   // but if you are mostly concerned with converting [Request]s to [Response]s
   // (known as a [Responder] in relic parlor) you can use [respondWith] to
   // wrap a [Responder] into a [Handler]
-  final handler = respondWith(hello);
+  final handler = const Pipeline()
+      .addMiddleware(logRequests())
+      .addMiddleware(routeWith(router))
+      .addHandler(respondWith((final _) => Response.notFound(
path: example/example.dart
user: login: nielsenko
body: Will resolve for now. Thanks for all the great feedback.
created_at: 2025-05-16T11:36:41Z
author_association: COLLABORATOR

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-05-14T10:11:40Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- This is an auto-generated comment: skip review by coderabbit.ai -->

> [!IMPORTANT]
> ## Review skipped
> 
> Auto reviews are disabled on this repository.
> 
> Please check the settings in the CodeRabbit UI or the `.coderabbit.yaml` file in this repository. To trigger a single review, invoke the `@coderabbitai review` command.
> 
> You can disable this status message by setting the `reviews.review_status` to `false` in the CodeRabbit configuration file.

<!-- end of auto-generated comment: skip review by coderabbit.ai -->
<!-- walkthrough_start -->

<details>
<summary>📝 Walkthrough</summary>

## Walkthrough

The changes introduce a routing middleware system, update the example server to use it, and modify the request context to include a persistent token for request-scoped state. The router now supports method- and path-based routing with path parameter extraction. Context creation and test utilities are updated to accommodate the new token parameter.

## Changes

| File(s)                                                                 | Change Summary                                                                                                                               |
|-------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------|
| example/example.dart                                                    | Refactored server example to use new routing middleware, updated handler signature, added path parameter extraction, and improved 404 fallback. |
| lib/src/adapter/context.dart                                            | Added immutable `token` field to `RequestContext` and subclasses; updated constructors and methods to propagate token; updated `toContext`.    |
| lib/src/middleware/routing_middleware.dart                              | Introduced routing middleware, path parameter extraction via Expando, `routeWith` helper, and request method conversion extension.             |
| lib/src/relic_server.dart                                               | Modified context creation to pass adapter request as token.                                                                                    |
| test/static/test_util.dart, test/util/test_util.dart                    | Updated calls to `toContext()` to provide a token argument for context creation in test helpers.                                               |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant Client
    participant Server
    participant MiddlewarePipeline
    participant RoutingMiddleware
    participant Handler

    Client->>Server: HTTP Request
    Server->>MiddlewarePipeline: Pass Request
    MiddlewarePipeline->>RoutingMiddleware: Pass RequestContext
    RoutingMiddleware->>RoutingMiddleware: Match route & extract path params
    alt Route matched
        RoutingMiddleware->>Handler: Pass RequestContext with path params
        Handler->>RoutingMiddleware: ResponseContext
    else No match
        RoutingMiddleware->>MiddlewarePipeline: Call next (fallback)
    end
    MiddlewarePipeline->>Server: ResponseContext
    Server->>Client: HTTP Response
```

## Assessment against linked issues

| Objective                                                                 | Addressed | Explanation                                                                                  |
|---------------------------------------------------------------------------|-----------|----------------------------------------------------------------------------------------------|
| Middleware support for pre- and post-route execution (#55)                | ✅        |                                                                                              |
| Error handling for undefined or invalid routes (#57)                      | ✅        |                                                                                              |

## Possibly related PRs

- serverpod/relic#65: Refactors the Router class to support HTTP method-specific route registration and lookup, which is directly leveraged by the new routing middleware introduced in this PR.

</details>

<!-- walkthrough_end -->
<!-- internal state start -->


<!-- DwQgtGAEAqAWCWBnSTIEMB26CuAXA9mAOYCmGJATmriQCaQDG+Ats2bgFyQAOFk+AIwBWJBrngA3EsgEBPRvlqU0AgfFwA6NPEgQAfACgjoCEYDEZyAAUASpETZWaCrKNxU3bABsvkCiQBHbGlcABpIcVwvOkgAIgAzEmouG3w8SkhmeFpaaIB3ZxJYyDlIbEQMjHgSLwqMAGt8SAAKW0gzADYADgBKcPgMXApFbAZpdEhyPL808QwiTOzckgL/dAx6MlhMMeRcWBJIAAMbQODEXABhfEGSAA9cI8g89QQsfcPTr3gGSHiqNh5fAUeoRJoObjcYG4HjUWBgARoCr0fxBEKQbYbb7zdb0eDMXj4KQos7ooZoBj1AZEDRGADSJHkDExpGQAwYXmwSg4RigACo+ak8NSALJLfKFAVcACCkxWixyErWzSO/m+DAA9N8BBrEBRNVlFStChrhsL5gB9Q3LVYkDS0ZyPHoRbYwgY0IhUGjINDHIU0ChPd1ND5+UkXHjwbg1AZ2yAASTdGEQ0bEyFR5xh3DhPo2mRI+0UiHC8WBq1o1My1GZMQzIT2TTQkOGvHg1EOmOWFEQGmscNhAILlGQ93JYhipVDZoD6DWFN2iHgAmikAkbfWkFHZEXN34WBO4auNxoDyeKrEdw02f2VmcaDYAcQRx6tIM/MFh+utwekAAohhMTGNhBkQKVIFlKZjgIeoyCeQQRDEFAfRyGICD9T9j1HJ40P8ZhtCwJhk1wTAYX2M0iFgWYJjrC4AHJkG+RIGFkDkSHCMgVGxBZrSVQ40J2Si+Bo3AwBTUR4HiH57GImgykXHEjl/O5sw2fA4OEURcB7SAvwuChRgIbs/mBY4ADkVi/E9HnCA8UxuCpLKw8JMHoI4AAl4CECl6kc09JnwaZ5xIbhSIQBsYIwXs4EOVVDw0AhfNwZoeieLdk3gXcM3gfwfTBCLZyIRx2Ffd8lPvbgVwAVW4B0aDA6LNzucqVybCqfmoDKsBVNUfg1e5mpIPqmoJaJ7UdZ9ICUZh7PJb0ZnNBZSiUSSqhxX1pxijVykoDUOAwe9BrQUhdqOkgnhefYJk7aI+H2ag5PGa9YAHA7Hyig4FRtQpI2jbFDnZTklAY/AiCISsXPmuYiHCC7nt9Za0G8GFYlM/AYQAMTSDZimujJSz4bAMDw3AaxRWZpBKyABQAZUoKQ+Ah6AQkraraukeqPoPdVaYoemnmzRAKj2D60AdEKMmE9Bhb4/B8ryA4CP8DqcVDAZ1DbXwDzRC5EqOKL0WFb5xHGbYpBKEgyDKGr23oNDCTXJQJkgo4AHkNLEZKniRF0ZblhXGCVqH0O1o9v0eFAiJ2Cm3DC6w7A5fAhaQhxxjMABWNPcXaNOAHZe0TCOhhGXYSiVqkcWZTBWXCDA0c4+RcKJStQ0Iqyq24P5hmYH3jhFaREFOp4OSRZBmgh/ZymDzMvbzWyoWTM6+jDJhWDICscR441/FE1MJKki5gVO1d10U5SXLU5Bd1DZ3ThDxL4tl2D3sONpSx8ALkAAEUdaTZBXQrsgxnIMWCOHIuTjFoPgBgRVBgdV3Nga23pnJ5gFknNAPhGpICDhDSC3otKvgsDpFgwEtL2EcHhFwMdDg3C8EyYh7A9wug8N4XwktUD/BYD3RGhYKA12qLUMgjRwjCXBmGNc8oOHd1DNtPgRwmBKCoKodQ2g9aQExu/F4KswrIJwAQYmtZpDzwqJ3Th1wlA2BUGoGEhFJIUGYMLe660SDiOmNsGQFt3gUHgKDSgdBn5zjWLXP42AKAfD4CvEhICKyIGgYLTqICTKJDoIiSkPBhgOxiFfD6tgSpgEMAYEwUA178HiLowgpByBehiBE9gXBeD8HduIKQMg6EKMseoLQOh9AFPAFAdwbJcxlOIGQZQNB6A1MGFwKg0wHBOBcCUVpyglGaG0LoPJRgemmAMP1Eag0dkVTtA6UJPJYinIMIQ6U8ZhmVJtmQuZ8h8ClMrvMaQVDGoDXsHTDI8jDgFBHABKOtsmgA3AZDcGeZN62jIZCaEvZZQnHJhQYAbkXI3T0EGSOGAxhIQDkkMZWcbHeJCTEWGEwADiv5oCQ02jI3a+02AalOidUgTw8KQlQiGTmBx35PFxhQZ+VYBgYlRRkbgUYgH/XTMFLwFIJzyF9EcKw4q/rYVdOgHIUqQ6QC8CDMGG9xRb0OCqHVRBb6ZkQJ7Je05KyQu+t1cmAB1V4zQNoUBSkvCGvp4joK8Ck0EfKXT3X8LgEJyYJgABYAAM4bICowxljEkdkF7PFeBMWJej8yC1OgKo43KdW8pFXwRcRB9ohrWM81kJju6+kXLs9CSaKhHDAMG0NlZ4iEzEJ1dADAxghVEVraeYIJhz3siQXWzaCytpxO2rF4hdxBT7WtKeIRdb5xhKOKgaZYSXWzIOR8xx6VnSzkcQeVae6t1HOEXdSdQxH34lgd0JBSC8Kzi2igYanENt+am300Zuw3HQfAAAXjET0Fsg5sCzaQAVkEA3bWQCOjYnEx2YQeBoWGpwv3YSaISWgow+IfRykYuMDUKi8wyBceAGDvgXG3Iwn1CYrASBjaLWgOU0FZmhJALokaeOvnMJYaUXgAxwLDWhUMShh5eniSUxqUJQmZL4J4ZcUl2Dq1eW+NRHa51YGLaW4ljAWR0C4Ih4xeb8DNDNeiYSKVICACTCetxHEoYhqDqyzGEw6MFwHcWzQqjj7OiENAaY1QlHAMKc2IvItnal1PqRlYsAwagvWho5nBwtnIuVcipoyYizIoQ8p5RnEBvIHSu1D4dh6CyzuoZADgBCVaTiqcyeRdY2Uw058rRxkGuQ8l5SkutnR5F8ZkRQe8OWgMBocX0kF8TMDwMhqCj8MBPEkjUeg6CbhEEXI7UM9wsGVliiHFb/DaDvVQEcaCsEwy8GkOwaWYYtVkTSBRKiNXtUSQLPiKbeZUCPo2DEfG6xmTAkrMJHe4lJK/AuO2cIDhmRS2PgqpSKlIHqQQqQoVtr/C9l0kMAywJkCA4+MYhr4whtrAQWzIF3be0wghmrcQgGQNMIQ5d5bL0HyUBx8CIjNx15EFoeEB8lFaAOOsYHQ4kFktJmh1i8YKoEB9fqBisyFlOs2Qw4Y0dKujjNd1se9ro6Bv+WmKgnuF2ltex1SrTlkv5QDFl7sK9OU6bNw+mz7twwqvS4iFQdKOntINVKxcVKDxtxduF4oRb+u/mGaruNrKc5FsRS9hQQqJDnj+wYIHURzs9edYFZA6BJDRPGVupzYSx21vPG9ogb4FEojyGy1Qb4IH6CSLiCDvV6DQEsFB4eYoaFYiSxHlAts+LSX7FQNLjQkXzmCeE6MmT4mPqSZldJ+ysn7jyfxSZZT6pNyDHU8VzT0oUKuRWj3t26O8pXdW14dbeZKe3MInpfHRbvH6bWGhYPocrIaAtHcy1WEnCGv00lvwwF8wIhlSq1/1XU01Zhfxmn0jEBMj02oGJQbAm1BVdkaQgP5jvE5woB5EgF0FVxa3KwAKAMzAe0zFALwLZygMYBgIQ3zzDj1gMFIKgFMxQzDioKswjBAMgDAMQjZ3CEN2TV5wXiYIa0cyNwL04LIPck8m8nvkAIEJhCEJENIiW3CF61UJ9T9UYEMO8hkJYOOH0P6wUKgEQPxUj3oHQLLXGDthbCOnbGT1ghIKUMsJ8nKwxBUMpGaB8MuBMNSQYDMJHnIPgK4LkIXmc01y/Xcy/T8AiMFnGF1zV3YINy1ziOsJ0iM02DD3Sl3HsPsE/wwP8C8O4LYLbgSnK2aG0IgKYLSh3H3A0PjFuHfXQTgjaMPA4IiyizACMBiz1ANANVtFNFmGpCtHGMKBCzSwGMy2uRywcPIWcAKzjxeRP36U2NIELmGDwxLmm3lGtX1SNChUB2ukrDcmgGgCsFoPrBTSnywF9Cx0ODFV+ljDXUmhIBWnGAVX9FFFmP8CHnMLulIjQBglymbykgRXSCRRRSxEoHRSzntkAVyiODYCNFZQLBFzDDLVWgWF9ADRnU7RuDO2QBJO0y7WoBoAJFITQmJnh1VixT7xxGEnonzELEf3oEqhsAABlt04YiB8IIwpxEV85SlXjqxnp2EE1+gYQKgGSPomSDh6AnoOchwjJgwe5JYfd6cMAJBH57tVSYg+VJT/IqwSZZTCd5SUAYQlBogRS5pCQFxm5bc5Qfw3jhUkT+UjBZQBdBAe9T4Uc1IcVtpqcD41gNTd1XphxIAYJZA5VdTDwFBPMxDD9OJKwB5EhqtEB8AZV8Uoyj5HlNT90/0HiLgc0ND9dUA0olB6BSUFUnpbw91hwnhSBcAZxwT8SvFnFnCDhjFYyiDL5y9hg8hBlKBhg+AJJdSpizivo1hUBa4ZdiIfA6BusJhWwJB3ClTuzGw4kS0fZmBXwz8KwdMfVZAdEWiu1dxf8+5uSh4bh6YMcdCe5H0ujNYHyRdUoMBHAHp6BJxCNEUhdWpwYMFZcjl6Abi7iuSRdcxbZYBxzwVNwKBpyy8yhkwEEd8Yh7CewSsNonV9gVtqTdxUBfRCIpAqgyBsVvVUD5kezs88V/i/R5yiAxRzjCgMVHdvsSQCS2RSFMS6BlgC1fSSwKQqNlEg4khEB5BH1PRS8dTvTK4HdXwXZ6YfVwgW5FB3j0l0SJg38xBiVGy0B5AcJyZIAYL7jhIZAkRMksBSiIYnpnd8BsxnT3j+xhytThZyJnopxUzpdNzvUfAjC0I6sKg0RBhPpeJM9LYgkNorSax8KNkLlF8N8xMPS187wA8t9lJoRFMeBsAVNfg1NjYT8oAzyAdSL9wOLFzDhCLnVL9fB/RKBkVC09BqU3Ung31CSJhvTzTT9z9mDIi4SoZareJzpU1X88d6LWKFpxrDVLNEU2rfSOrXVbMIZSj3JC18wjQgidryAHgUpaQKqhqby7yazyslIejl0dZOsnjYAqjIAyUCwZwjgRQmxgBqZZBmABACzwhqYhhqQOrOyhTWy4zuwwsYjaYuyMgjglTIALQWzCDvLmgPruAvqfq/qvAAagb5gOqdzOQSAUpBqGzGoaBiisALrDxvzFBJrLotrabxlnzKAkpjr0s58IAhiDARi4seoGALQyN6Z5iTkMtBMssRkqlVj7lZMK0NMGo5aRsKxVt7smLS9SztLPN4JwCO8WSV5+8tVYYhVQwjgLRcYNCcTuTewrB/A1w0g68rzz0/DY81b8VSgGAfUDthIH5EpPYHqqJnB087sBVn98UfljCfAENva6iw4x4EtKANCUor0R43dqEvFu9fBRYmwZwR9BlA6YFNBwIFBV4oqpMxsxcuF46hJUyXgIKvkEdQwPdjaPofdXbOoKTdipt4gmJSFKJphNa25p9kCDIJxfjgR/o3RkBUFxtQxppCZ8USS99hg3T5h+N59wI0rRMsCJNRB18t7cqcL6A98iqD9SrqgT9UZyBaQBi3wtk8FdQZJeo8ELRDYRaOb17Lllipa7l8tZais3k5bkBpplamQfVt7OYY6rI/bSjqajsGl0cWl1V+cNxt91R1ACoC7jhtC/bjagQeAZVdguAHdAFzc8IYILbglZ0aS8wjb3gW6dUHA1gerkzTbhg0ZESuxVEbbnEMpyhBdo96jbMXafUSVXgA608C7EAABuE3e0nFD29c9bVnfAX27BnoY6mAWOdBfdAe0cXFUvUoVBXPeUXA9Hbi4iOXBHX9FGgMXsVGfgMJbVEGH4diNCkyK4+YcIEyVuYYXweIHVaYafAote1KkTHKlfQ4LK9Ky+UpbffKo+pTE+1TI/MqowS+u0d+2+7m++w2DUZ+1+1LUWyLJY5vW5PLdYv++PbYluozJW8u89BjH/SB0cHB9KHbTmMhkgamfEA5Ch0knTa2223hh2myZph4P24RxR/2vAdYeVCRyJRhfyrVZoEyeGX4xGYTSs3AF8WNAKBU+RkRxsn9LAVBn4dB/OjPUxzST2Du7R+M3Rn8Nu3cQxlOpdZ2bQ8xqOKxssjIJplRwR1RBqHKGEDWj6fprtXCUUzCuW2gfoLFQGVOiIWQaMQzUQUELJeqydd9apPw8eD6DdCkHTWTfyr9EJhfMJ5fTK3e7KmTUsuJhTBJwq4qw/SIc+3kXZzu8Bw4RwzA/gPgKJ/eulvKhlvlpl0+lJ8+6+s5W+/JQpQ/I+qUvAcpSWpAkuzgPwNAGZNY+Zd2nSxRKxTpNZQwOViJdQC0bIRAC0IZlYOgQW4iUJI14wXpSAAARnDQzloAACZRYOhw14gXXaA04Og0Bw0ABmNOF1kgLoNOBgUN0NjoWgDoNOLoEgX1qNz18NcNLoR1zZKAU13Ac10XK1nhm12gC0YpfQIAA=== -->

<!-- internal state end -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=serverpod/relic&utm_content=68):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Explain this complex logic.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai explain this code block.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and explain its main purpose.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: nielsenko
created_at: 2025-05-14T12:55:07Z
body: @coderabbitai review
author_association: COLLABORATOR
user: login: coderabbitai[bot]
created_at: 2025-05-14T12:55:12Z
body: <!-- This is an auto-generated reply by CodeRabbit -->
<details>
<summary>✅ Actions performed</summary>

Review triggered.

> Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

</details>
author_association: CONTRIBUTOR
user: login: nielsenko
created_at: 2025-05-14T13:42:58Z
body: @coderabbitai Added tests, please review
author_association: COLLABORATOR
user: login: coderabbitai[bot]
created_at: 2025-05-14T13:44:38Z
body: <!-- This is an auto-generated reply by CodeRabbit -->
> [!TIP]
> For best results, initiate chat on the files or code changes.



<!-- <agentComponentType>chat-answer</agentComponentType> -->

<!-- <rawResChunk><nextState>plan-edits</nextState></rawResChunk> -->

<!-- <rawResChunk><answer><details>
<summary>🧩 Analysis chain</summary>

🏁 Script executed:
```
#!/bin/bash
# Explore the test structure in the repository
find test -type f -name "*.dart" | sort

# Look at the routing middleware implementation to understand what to test
cat lib/src/middleware/routing_middleware.dart

# Check if there are any existing routing-related tests
find test -type f -name "*rout*.dart" | sort

# Check for other middleware-related tests to understand how middleware tests are usually structured
find test -type f -name "*middleware*.dart" | sort

# Look at a sample test file to understand the testing pattern
find test -type f -name "*.dart" | head -n 1 | xargs cat
```

Length of output: 9651

---

🏁 Script executed:
```
#!/bin/bash
# Check if there's already a routing middleware test file and what it contains
cat test/middleware/routing_middleware_test.dart

# Look at another middleware test file for reference
cat test/middleware/log_middleware_test.dart

# Check test utilities
cat test/util/test_util.dart
```

Length of output: 23248

---



</details>



```mermaid
flowchart TB
    A["Add tests for RoutingMiddleware"]
    A --o B["Test RequestMethod.convert()"]
    A --o C["Test error handling"]
    A --o D["Test multiple requests isolation"]
    A --o E["Test Pipeline integration"]
    
    B --o B1["Test valid method conversions"]
    B --o B2["Test error for unsupported methods"]
    
    C --o C1["Test malformed path handling"]
    C --o C2["Test exception propagation"]
    
    D --o D1["Test path parameters isolation"]
    D --o D2["Test middleware reuse"]
    
    E --o E1["Test Pipeline composition"]
    E --o E2["Test with real request examples"]
```

&lt;!-- <requirement>
[requirement test_request_method_convert 1/4]
Add tests for the RequestMethod.convert() extension method to verify all HTTP methods are correctly converted and that unsupported methods throw an error.

[propose_code_change]

File: `test/middleware/routing_middleware_test.dart`

```diff
@@ -293,4 +293,29 @@ void main() {
+
+    group('RequestMethod.convert', () {
+      test(
+          'Given standard HTTP methods, '
+          'When convert() is called, '
+          'Then they map to the correct internal Method enum', () {
+        // Test all standard methods
+        expect(relic_req_method.RequestMethod.get.convert(), equals(Method.get));
+        expect(relic_req_method.RequestMethod.post.convert(), equals(Method.post));
+        expect(relic_req_method.RequestMethod.put.convert(), equals(Method.put));
+        expect(relic_req_method.RequestMethod.delete.convert(), equals(Method.delete));
+        expect(relic_req_method.RequestMethod.head.convert(), equals(Method.head));
+        expect(relic_req_method.RequestMethod.options.convert(), equals(Method.options));
+        expect(relic_req_method.RequestMethod.patch.convert(), equals(Method.patch));
+        expect(relic_req_method.RequestMethod.trace.convert(), equals(Method.trace));
+        expect(relic_req_method.RequestMethod.connect.convert(), equals(Method.connect));
+      });
+
+      test(
+          'Given a non-standard HTTP method, '
+          'When convert() is called, '
+          'Then it throws UnimplementedError', () {
+        expect(() => relic_req_method.RequestMethod('CUSTOM').convert(),
+            throwsA(isA<UnimplementedError>()));
+      });
+    });
   });
 }
```
</requirement> --&gt;

&lt;!-- <requirement>
[requirement test_error_handling 2/4]
Add tests for error handling in the routing middleware, including tests for malformed paths and exception propagation.

[propose_code_change]

File: `test/middleware/routing_middleware_test.dart`

```diff
@@ -293,4 +293,52 @@ void main() {
+
+    group('Error handling', () {
+      test(
+          'Given a malformed path in a request, '
+          'When the middleware processes it, '
+          'Then the error is propagated to the caller', () async {
+        bool nextCalled = false;
+        final badPathRequest = FakeRequest('/%invalid-path')
+            .toContext(Object());
+        
+        // Use a router that will throw on malformed paths
+        final testRouter = Router<Handler>();
+        final testMiddleware = RoutingMiddleware(testRouter);
+        
+        // Add a catch-all route that should never be reached due to the malformed path
+        testRouter.add(Method.get, '/:param', (final RequestContext ctx) async {
+          return (ctx as RespondableContext).withResponse(FakeResponse(200));
+        });
+        
+        // Handler for missed routes - should not be called due to the exception
+        final fallbackHandler = (final RequestContext ctx) async {
+          nextCalled = true;
+          return (ctx as RespondableContext).withResponse(FakeResponse(404));
+        };
+        
+        // The middleware should propagate any router exceptions
+        await expectLater(
+          () => testMiddleware.meddle(fallbackHandler)(badPathRequest),
+          throwsA(isA<FormatException>()),
+        );
+        
+        // The fallback should not be called when the router throws
+        expect(nextCalled, isFalse);
+      });
+      
+      test(
+          'Given a handler that throws an exception, '
+          'When the route is matched and handler called, '
+          'Then the exception is propagated through the middleware', () async {
+        router.add(Method.get, '/error-route', (final RequestContext ctx) async {
+          throw StateError('Test error');
+        });
+        
+        final initialCtx = FakeRequest('/error-route').toContext(Object());
+        
+        await expectLater(
+          () => middleware.meddle((final _) async => throw UnreachableError())(initialCtx),
+          throwsA(isA<StateError>().having((e) => e.message, 'message', 'Test error')),
+        );
+      });
+    });
   });
 }
+
+class UnreachableError extends Error {
+  @override
+  String toString() => 'This code should not be reached';
+}
```
</requirement> --&gt;

&lt;!-- <requirement>
[requirement test_request_isolation 3/4]
Add tests to verify that path parameters don't leak between different requests and that the middleware properly isolates request contexts.

[propose_code_change]

File: `test/middleware/routing_middleware_test.dart`

```diff
@@ -293,4 +293,54 @@ void main() {
+    
+    group('Request isolation', () {
+      test(
+          'Given the same middleware processing multiple requests, '
+          'When path parameters are extracted for different requests, '
+          'Then each request has its own isolated parameters', () async {
+        // Create a router with a parameterized route
+        final isolationRouter = Router<Handler>();
+        isolationRouter.add(Method.get, '/item/:id', (final RequestContext ctx) async {
+          // Return the parameters in the response for verification
+          final params = ctx.pathParameters;
+          return (ctx as RespondableContext)
+              .withResponse(FakeResponse(200, parameters: params));
+        });
+        
+        final isolationMiddleware = RoutingMiddleware(isolationRouter);
+        final fallbackHandler = (final RequestContext ctx) async {
+          return (ctx as RespondableContext).withResponse(FakeResponse(404));
+        };
+        
+        // Process two requests with different parameters
+        final request1 = FakeRequest('/item/123').toContext(Object());
+        final request2 = FakeRequest('/item/456').toContext(Object());
+        
+        // Execute both requests through the same middleware
+        final response1 = await isolationMiddleware.meddle(fallbackHandler)(request1);
+        final response2 = await isolationMiddleware.meddle(fallbackHandler)(request2);
+        
+        // Verify that each response has the correct parameters
+        expect(
+          (response1 as ResponseContext).response,
+          isA<FakeResponse>().having(
+            (r) => r.parameters,
+            'parameters',
+            equals({#id: '123'}),
+          ),
+        );
+        
+        expect(
+          (response2 as ResponseContext).response,
+          isA<FakeResponse>().having(
+            (r) => r.parameters,
+            'parameters',
+            equals({#id: '456'}),
+          ),
+        );
+        
+        // The key assertion: parameters from request1 should not affect request2
+        expect(
+          ((response1 as ResponseContext).response as FakeResponse).parameters,
+          isNot(equals(((response2 as ResponseContext).response as FakeResponse).parameters)),
+        );
+      });
+    });
   });
 }
```
</requirement> --&gt;

&lt;!-- <requirement>
[requirement test_pipeline_integration 4/4]
Add tests that demonstrate integration with the Pipeline API, showing how routing middleware can be composed with other middleware.

[propose_code_change]

File: `test/middleware/routing_middleware_test.dart`

```diff
@@ -293,4 +293,71 @@ void main() {
+    
+    group('Pipeline integration', () {
+      test(
+          'Given a Pipeline with RoutingMiddleware, '
+          'When a request is processed, '
+          'Then the pipeline correctly routes the request', () async {
+        Map<Symbol, String>? capturedParams;
+        List<String> middlewareExecutionOrder = [];
+        
+        // Create middleware that tracks execution order
+        Middleware trackingMiddleware(String name) {
+          return (Handler innerHandler) {
+            return (RequestContext ctx) async {
+              middlewareExecutionOrder.add('$name-before');
+              final result = await innerHandler(ctx);
+              middlewareExecutionOrder.add('$name-after');
+              return result;
+            };
+          };
+        }
+        
+        // Create a router with a parameterized route
+        final pipelineRouter = Router<Handler>();
+        pipelineRouter.add(Method.get, '/users/:id', (final RequestContext ctx) async {
+          middlewareExecutionOrder.add('handler');
+          capturedParams = ctx.pathParameters;
+          return (ctx as RespondableContext)
+              .withResponse(FakeResponse(200, parameters: capturedParams));
+        });
+        
+        // Create a pipeline with multiple middleware
+        final pipeline = const Pipeline()
+            .addMiddleware(trackingMiddleware('first'))
+            .addMiddleware(routeWith(pipelineRouter))
+            .addMiddleware(trackingMiddleware('last'))
+            .addHandler((final RequestContext ctx) {
+              middlewareExecutionOrder.add('fallback');
+              return (ctx as RespondableContext).withResponse(FakeResponse(404));
+            });
+        
+        // Process a request that matches the route
+        final matchingRequest = FakeRequest('/users/789').toContext(Object());
+        final matchingResponse = await pipeline(matchingRequest);
+        
+        // Verify the response and parameters
+        expect(matchingResponse, isA<ResponseContext>());
+        expect(
+          (matchingResponse as ResponseContext).response,
+          isA<FakeResponse>().having(
+            (r) => r.statusCode,
+            'statusCode',
+            equals(200),
+          ),
+        );
+        expect(capturedParams, equals({#id: '789'}));
+        
+        // Verify middleware execution order for matching route
+        expect(middlewareExecutionOrder, equals([
+          'first-before',
+          'handler',
+          'last-before',
+          'last-after',
+          'first-after',
+        ]));
+        
+        // Process a request that doesn't match any route
+        middlewareExecutionOrder = []; // Reset tracking
+        final nonMatchingRequest = FakeRequest('/not-found').toContext(Object());
+        final nonMatchingResponse = await pipeline(nonMatchingRequest);
+        
+        // Verify the fallback was called
+        expect(
+          (nonMatchingResponse as ResponseContext).response,
+          isA<FakeResponse>().having(
+            (r) => r.statusCode,
+            'statusCode',
+            equals(404),
+          ),
+        );
+        
+        // Verify middleware execution order for non-matching route
+        expect(middlewareExecutionOrder, equals([
+          'first-before',
+          'last-before',
+          'fallback',
+          'last-after',
+          'first-after',
+        ]));
+      });
+    });
   });
 }
```
</requirement> --&gt;

@nielsenko I've analyzed the existing tests and planned some additional tests for the routing middleware. The current test coverage is already quite thorough, covering parameter propagation, middleware composition, nested routing, and the `routeWith` utility.

To make the test suite even more comprehensive, I've outlined additional tests that would strengthen our confidence in the code:

1. **RequestMethod conversion tests** - Verify that all HTTP methods correctly convert to the internal Method enum and unsupported methods throw errors
2. **Error handling tests** - Test behavior with malformed paths and exception propagation through the middleware
3. **Request isolation tests** - Ensure path parameters don't leak between different requests
4. **Pipeline integration tests** - Demonstrate how routing middleware works within a Pipeline, showing correct execution order

I've created a plan for implementing these tests. Another agent will help implement them. These additional tests will ensure we test edge cases and integration scenarios that might be encountered in real-world usage.

The tests follow the same structure as the existing ones, using the descriptive "Given...When...Then..." pattern and the `FakeRequest`/`FakeResponse` fixtures.

Would you like me to explain any specific aspect of the testing approach in more detail?

---

⏳ Preparing changes...

</answer></rawResChunk> -->
author_association: CONTRIBUTOR
user: login: coderabbitai[bot]
created_at: 2025-05-14T13:44:54Z
body: <!-- This is an auto-generated reply by CodeRabbit -->
> [!TIP]
> For best results, initiate chat on the files or code changes.

<details>
<summary>🧩 Analysis chain</summary>

🏁 Script executed:
```
#!/bin/bash
# Find test files that have been modified in the PR
echo "Looking for modified test files in the PR:"
git diff --name-only HEAD~10..HEAD | grep -E "test/.*\.dart$"

echo -e "\nChecking for test files related to routing middleware:"
find test -type f -name "*routing*" -o -name "*middleware*" -o -name "*route*"

echo -e "\nListing all test files for reference:"
find test -type f -name "*.dart" | sort
```

Length of output: 5769

---

🏁 Script executed:
```
#!/bin/bash
# Examine the routing middleware test file
cat test/middleware/routing_middleware_test.dart
```

Length of output: 16955

---

🏁 Script executed:
```
#!/bin/bash
# Check the router tests
cat test/router/router_test.dart
```

Length of output: 21750

---

🏁 Script executed:
```
#!/bin/bash
# Check the router methods test file
cat test/router/router_methods_test.dart
```

Length of output: 9804

---



</details>

@nielsenko I've reviewed the test files you've added, particularly the `test/middleware/routing_middleware_test.dart` file which provides excellent test coverage for the new routing middleware functionality!

The routing middleware tests are comprehensive and well-structured, covering:

1. **Basic Parameter Propagation**:
   - Tests for extracting and accessing path parameters via `ctx.pathParameters`
   - Verification of both parameterized and non-parameterized routes
   - Error handling when trying to access parameters with no route match

2. **Multiple Middleware Testing**:
   - Tests showing how multiple routing middleware instances work in a pipeline
   - Verifies correct middleware execution order and parameter handling

3. **Nested Routing**:
   - Tests for nested routers via `Router.attach`
   - Verification that path parameters are correctly merged from parent routes
   - Edge case handling for parameter name collisions

4. **Utility Functions**:
   - Tests for the `routeWith` convenience function

The tests follow good practices with clear "Given/When/Then" descriptions and thorough verification of both expected functionality and edge cases. The fake implementations for testing are also well-designed.

Great job on the test coverage! This will ensure the routing middleware works correctly and remains stable as the codebase evolves.

(\_/)
(•ᴗ•)
/ >📝 Nice tests!


<!-- <agentComponentType>chat-answer</agentComponentType> -->

<!-- <rawResChunk><answer><details>
<summary>🧩 Analysis chain</summary>

🏁 Script executed:
```
#!/bin/bash
# Find test files that have been modified in the PR
echo "Looking for modified test files in the PR:"
git diff --name-only HEAD~10..HEAD | grep -E "test/.*\.dart$"

echo -e "\nChecking for test files related to routing middleware:"
find test -type f -name "*routing*" -o -name "*middleware*" -o -name "*route*"

echo -e "\nListing all test files for reference:"
find test -type f -name "*.dart" | sort
```

Length of output: 5769

---

🏁 Script executed:
```
#!/bin/bash
# Examine the routing middleware test file
cat test/middleware/routing_middleware_test.dart
```

Length of output: 16955

---

🏁 Script executed:
```
#!/bin/bash
# Check the router tests
cat test/router/router_test.dart
```

Length of output: 21750

---

🏁 Script executed:
```
#!/bin/bash
# Check the router methods test file
cat test/router/router_methods_test.dart
```

Length of output: 9804

---



</details>

@nielsenko I've reviewed the test files you've added, particularly the `test/middleware/routing_middleware_test.dart` file which provides excellent test coverage for the new routing middleware functionality!

The routing middleware tests are comprehensive and well-structured, covering:

1. **Basic Parameter Propagation**:
   - Tests for extracting and accessing path parameters via `ctx.pathParameters`
   - Verification of both parameterized and non-parameterized routes
   - Error handling when trying to access parameters with no route match

2. **Multiple Middleware Testing**:
   - Tests showing how multiple routing middleware instances work in a pipeline
   - Verifies correct middleware execution order and parameter handling

3. **Nested Routing**:
   - Tests for nested routers via `Router.attach`
   - Verification that path parameters are correctly merged from parent routes
   - Edge case handling for parameter name collisions

4. **Utility Functions**:
   - Tests for the `routeWith` convenience function

The tests follow good practices with clear "Given/When/Then" descriptions and thorough verification of both expected functionality and edge cases. The fake implementations for testing are also well-designed.

Great job on the test coverage! This will ensure the routing middleware works correctly and remains stable as the codebase evolves.

(\_/)
(•ᴗ•)
/ >📝 Nice tests!</answer></rawResChunk> -->
author_association: CONTRIBUTOR
user: login: coderabbitai[bot]
created_at: 2025-05-14T13:49:30Z
body: <!-- This is an auto-generated reply by CodeRabbit -->
A summary of the edits CodeRabbit can apply:

> Will add comprehensive tests for RequestMethod.convert, error handling, request isolation, and pipeline integration to routing_middleware_test.dart

> We will add four new test groups in test/middleware/routing_middleware_test.dart: RequestMethod.convert, Error handling, Request isolation, and Pipeline integration.

> New test groups for RequestMethod.convert, error handling, request isolation, and pipeline integration will be inserted into routing_middleware_test.dart

> The UnreachableError helper class will be appended to routing_middleware_test.dart



A plan of each step that CodeRabbit will take in order to execute the edits for each file (with inline rough draft snippets):

<details>
<summary>✏️ test/middleware/routing_middleware_test.dart</summary>

Delete lines 293–296, which currently close the main() function, to prepare for inserting the new test groups.

---

At line 293, insert the following Dart code block containing the four new test groups, and then re-close the main() function at the end:

---

```dart
    group('RequestMethod.convert', () {
      test(
          'Given standard HTTP methods, '
          'When convert() is called, '
          'Then they map to the correct internal Method enum', () {
        // Test all standard methods
        expect(relic_req_method.RequestMethod.get.convert(), equals(Method.get));
        expect(relic_req_method.RequestMethod.post.convert(), equals(Method.post));
        expect(relic_req_method.RequestMethod.put.convert(), equals(Method.put));
        expect(relic_req_method.RequestMethod.delete.convert(), equals(Method.delete));
        expect(relic_req_method.RequestMethod.head.convert(), equals(Method.head));
        expect(relic_req_method.RequestMethod.options.convert(), equals(Method.options));
        expect(relic_req_method.RequestMethod.patch.convert(), equals(Method.patch));
        expect(relic_req_method.RequestMethod.trace.convert(), equals(Method.trace));
        expect(relic_req_method.RequestMethod.connect.convert(), equals(Method.connect));
      });

      test(
          'Given a non-standard HTTP method, '
          'When convert() is called, '
          'Then it throws UnimplementedError', () {
        expect(() => relic_req_method.RequestMethod('CUSTOM').convert(),
            throwsA(isA<UnimplementedError>()));
      });
    });

    group('Error handling', () {
      test(
          'Given a malformed path in a request, '
          'When the middleware processes it, '
          'Then the error is propagated to the caller', () async {
        bool nextCalled = false;
        final badPathRequest = FakeRequest('/%invalid-path')
            .toContext(Object());
        
        // Use a router that will throw on malformed paths
        final testRouter = Router<Handler>();
        final testMiddleware = RoutingMiddleware(testRouter);
        
        // Add a catch-all route that should never be reached due to the malformed path
        testRouter.add(Method.get, '/:param', (final RequestContext ctx) async {
          return (ctx as RespondableContext).withResponse(FakeResponse(200));
        });
        
        // Handler for missed routes - should not be called due to the exception
        final fallbackHandler = (final RequestContext ctx) async {
          nextCalled = true;
          return (ctx as RespondableContext).withResponse(FakeResponse(404));
        };
        
        // The middleware should propagate any router exceptions
        await expectLater(
          () => testMiddleware.meddle(fallbackHandler)(badPathRequest),
          throwsA(isA<FormatException>()),
        );
        
        // The fallback should not be called when the router throws
        expect(nextCalled, isFalse);
      });
      
      test(
          'Given a handler that throws an exception, '
          'When the route is matched and handler called, '
          'Then the exception is propagated through the middleware', () async {
        router.add(Method.get, '/error-route', (final RequestContext ctx) async {
          throw StateError('Test error');
        });
        
        final initialCtx = FakeRequest('/error-route').toContext(Object());
        
        await expectLater(
          () => middleware.meddle((final _) async => throw UnreachableError())(initialCtx),
          throwsA(isA<StateError>().having((e) => e.message, 'message', 'Test error')),
        );
      });
    });

    group('Request isolation', () {
      test(
          'Given the same middleware processing multiple requests, '
          'When path parameters are extracted for different requests, '
          'Then each request has its own isolated parameters', () async {
        // Create a router with a parameterized route
        final isolationRouter = Router<Handler>();
        isolationRouter.add(Method.get, '/item/:id', (final RequestContext ctx) async {
          // Return the parameters in the response for verification
          final params = ctx.pathParameters;
          return (ctx as RespondableContext)
              .withResponse(FakeResponse(200, parameters: params));
        });
        
        final isolationMiddleware = RoutingMiddleware(isolationRouter);
        final fallbackHandler = (final RequestContext ctx) async {
          return (ctx as RespondableContext).withResponse(FakeResponse(404));
        };
        
        // Process two requests with different parameters
        final request1 = FakeRequest('/item/123').toContext(Object());
        final request2 = FakeRequest('/item/456').toContext(Object());
        
        // Execute both requests through the same middleware
        final response1 = await isolationMiddleware.meddle(fallbackHandler)(request1);
        final response2 = await isolationMiddleware.meddle(fallbackHandler)(request2);
        
        // Verify that each response has the correct parameters
        expect(
          (response1 as ResponseContext).response,
          isA<FakeResponse>().having(
            (r) => r.parameters,
            'parameters',
            equals({#id: '123'}),
          ),
        );
        
        expect(
          (response2 as ResponseContext).response,
          isA<FakeResponse>().having(
            (r) => r.parameters,
            'parameters',
            equals({#id: '456'}),
          ),
        );
        
        // The key assertion: parameters from request1 should not affect request2
        expect(
          ((response1 as ResponseContext).response as FakeResponse).parameters,
          isNot(equals(((response2 as ResponseContext).response as FakeResponse).parameters)),
        );
      });
    });

    group('Pipeline integration', () {
      test(
          'Given a Pipeline with RoutingMiddleware, '
          'When a request is processed, '
          'Then the pipeline correctly routes the request', () async {
        Map<Symbol, String>? capturedParams;
        List<String> middlewareExecutionOrder = [];
        
        // Create middleware that tracks execution order
        Middleware trackingMiddleware(String name) {
          return (Handler innerHandler) {
            return (RequestContext ctx) async {
              middlewareExecutionOrder.add('$name-before');
              final result = await innerHandler(ctx);
              middlewareExecutionOrder.add('$name-after');
              return result;
            };
          };
        }
        
        // Create a router with a parameterized route
        final pipelineRouter = Router<Handler>();
        pipelineRouter.add(Method.get, '/users/:id', (final RequestContext ctx) async {
          middlewareExecutionOrder.add('handler');
          capturedParams = ctx.pathParameters;
          return (ctx as RespondableContext)
              .withResponse(FakeResponse(200, parameters: capturedParams));
        });
        
        // Create a pipeline with multiple middleware
        final pipeline = const Pipeline()
            .addMiddleware(trackingMiddleware('first'))
            .addMiddleware(routeWith(pipelineRouter))
            .addMiddleware(trackingMiddleware('last'))
            .addHandler((final RequestContext ctx) {
              middlewareExecutionOrder.add('fallback');
              return (ctx as RespondableContext).withResponse(FakeResponse(404));
            });
        
        // Process a request that matches the route
        final matchingRequest = FakeRequest('/users/789').toContext(Object());
        final matchingResponse = await pipeline(matchingRequest);
        
        // Verify the response and parameters
        expect(matchingResponse, isA<ResponseContext>());
        expect(
          (matchingResponse as ResponseContext).response,
          isA<FakeResponse>().having(
            (r) => r.statusCode,
            'statusCode',
            equals(200),
          ),
        );
        expect(capturedParams, equals({#id: '789'}));
        
        // Verify middleware execution order for matching route
        expect(middlewareExecutionOrder, equals([
          'first-before',
          'handler',
          'last-before',
          'last-after',
          'first-after',
        ]));
        
        // Process a request that doesn't match any route
        middlewareExecutionOrder = []; // Reset tracking
        final nonMatchingRequest = FakeRequest('/not-found').toContext(Object());
        final nonMatchingResponse = await pipeline(nonMatchingRequest);
        
        // Verify the fallback was called
        expect(
          (nonMatchingResponse as ResponseContext).response,
          isA<FakeResponse>().having(
            (r) => r.statusCode,
            'statusCode',
            equals(404),
          ),
        );
        
        // Verify middleware execution order for non-matching route
        expect(middlewareExecutionOrder, equals([
          'first-before',
          'last-before',
          'fallback',
          'last-after',
          'first-after',
        ]));
      });
    });
  });
}
```

---

Insert the following group to test `RequestMethod.convert()` for all standard HTTP methods and verify it throws on unsupported methods:
```dart
    group('RequestMethod.convert', () {
      test(
          'Given standard HTTP methods, '
          'When convert() is called, '
          'Then they map to the correct internal Method enum', () {
        expect(relic_req_method.RequestMethod.get.convert(), equals(Method.get));
        expect(relic_req_method.RequestMethod.post.convert(), equals(Method.post));
        expect(relic_req_method.RequestMethod.put.convert(), equals(Method.put));
        expect(relic_req_method.RequestMethod.delete.convert(), equals(Method.delete));
        expect(relic_req_method.RequestMethod.head.convert(), equals(Method.head));
        expect(relic_req_method.RequestMethod.options.convert(), equals(Method.options));
        expect(relic_req_method.RequestMethod.patch.convert(), equals(Method.patch));
        expect(relic_req_method.RequestMethod.trace.convert(), equals(Method.trace));
        expect(relic_req_method.RequestMethod.connect.convert(), equals(Method.connect));
      });

      test(
          'Given a non-standard HTTP method, '
          'When convert() is called, '
          'Then it throws UnimplementedError', () {
        expect(
          () => relic_req_method.RequestMethod('CUSTOM').convert(),
          throwsA(isA<UnimplementedError>()),
        );
      });
    });
```

---

Insert the following group to verify error propagation for malformed paths and handler-thrown exceptions in the routing middleware:
```dart
    group('Error handling', () {
      test(
          'Given a malformed path in a request, '
          'When the middleware processes it, '
          'Then the error is propagated to the caller', () async {
        bool nextCalled = false;
        final badPathRequest = FakeRequest('/%invalid-path')
            .toContext(Object());

        final testRouter = Router<Handler>();
        final testMiddleware = RoutingMiddleware(testRouter);
        testRouter.add(Method.get, '/:param', (ctx) async {
          return (ctx as RespondableContext).withResponse(FakeResponse(200));
        });

        final fallbackHandler = (ctx) async {
          nextCalled = true;
          return (ctx as RespondableContext).withResponse(FakeResponse(404));
        };

        await expectLater(
          () => testMiddleware.meddle(fallbackHandler)(badPathRequest),
          throwsA(isA<FormatException>()),
        );
        expect(nextCalled, isFalse);
      });

      test(
          'Given a handler that throws an exception, '
          'When the route is matched and handler called, '
          'Then the exception is propagated through the middleware', () async {
        router.add(Method.get, '/error-route', (ctx) async {
          throw StateError('Test error');
        });

        final initialCtx = FakeRequest('/error-route').toContext(Object());

        await expectLater(
          () => middleware.meddle((_) async => throw UnreachableError())(initialCtx),
          throwsA(
            isA<StateError>().having((e) => e.message, 'message', 'Test error'),
          ),
        );
      });
    });
```

---

Insert the following group to ensure path-parameter isolation across multiple requests:
```dart
    group('Request isolation', () {
      test(
          'Given the same middleware processing multiple requests, '
          'When path parameters are extracted for different requests, '
          'Then each request has its own isolated parameters', () async {
        final isolationRouter = Router<Handler>();
        isolationRouter.add(Method.get, '/item/:id', (ctx) async {
          final params = ctx.pathParameters;
          return (ctx as RespondableContext).withResponse(FakeResponse(200, parameters: params));
        });

        final isolationMiddleware = RoutingMiddleware(isolationRouter);
        final fallbackHandler = (ctx) async => (ctx as RespondableContext).withResponse(FakeResponse(404));

        final request1 = FakeRequest('/item/123').toContext(Object());
        final request2 = FakeRequest('/item/456').toContext(Object());

        final response1 = await isolationMiddleware.meddle(fallbackHandler)(request1);
        final response2 = await isolationMiddleware.meddle(fallbackHandler)(request2);

        expect(
          (response1 as ResponseContext).response,
          isA<FakeResponse>().having((r) => r.parameters, 'params', equals({#id: '123'})),
        );
        expect(
          (response2 as ResponseContext).response,
          isA<FakeResponse>().having((r) => r.parameters, 'params', equals({#id: '456'})),
        );
        expect(
          (response1 as ResponseContext).response.parameters,
          isNot(equals((response2 as ResponseContext).response.parameters)),
        );
      });
    });
```

---

Insert the following group to demonstrate integration of `RoutingMiddleware` within the `Pipeline` API:
```dart
    group('Pipeline integration', () {
      test(
          'Given a Pipeline with RoutingMiddleware, '
          'When a request is processed, '
          'Then the pipeline correctly routes the request', () async {
        Map<Symbol, String>? capturedParams;
        final middlewareExecutionOrder = <String>[];

        Middleware trackingMiddleware(String name) {
          return (inner) {
            return (ctx) async {
              middlewareExecutionOrder.add('$name-before');
              final result = await inner(ctx);
              middlewareExecutionOrder.add('$name-after');
              return result;
            };
          };
        }

        final pipelineRouter = Router<Handler>();
        pipelineRouter.add(Method.get, '/users/:id', (ctx) async {
          middlewareExecutionOrder.add('handler');
          capturedParams = ctx.pathParameters;
          return (ctx as RespondableContext).withResponse(FakeResponse(200, parameters: capturedParams));
        });

        final pipeline = const Pipeline()
            .addMiddleware(trackingMiddleware('first'))
            .addMiddleware(routeWith(pipelineRouter))
            .addMiddleware(trackingMiddleware('last'))
            .addHandler((ctx) => (ctx as RespondableContext).withResponse(FakeResponse(404)));

        final matchingRequest = FakeRequest('/users/789').toContext(Object());
        final matchingResponse = await pipeline(matchingRequest);

        expect(matchingResponse, isA<ResponseContext>());
        expect(
          (matchingResponse as ResponseContext).response,
          isA<FakeResponse>().having((r) => r.statusCode, 'statusCode', equals(200)),
        );
        expect(capturedParams, equals({#id: '789'}));
        expect(
          middlewareExecutionOrder,
          equals(['first-before', 'handler', 'last-before', 'last-after', 'first-after']),
        );

        // Non-matching
        middlewareExecutionOrder.clear();
        final nonMatch = FakeRequest('/not-found').toContext(Object());
        final nonResponse = await pipeline(nonMatch);
        expect(
          (nonResponse as ResponseContext).response,
          isA<FakeResponse>().having((r) => r.statusCode, 'statusCode', equals(404)),
        );
        expect(
          middlewareExecutionOrder,
          equals(['first-before', 'last-before', 'fallback', 'last-after', 'first-after']),
        );
      });
    });
```

---

Append the following helper class at the end of the file:

```dart
class UnreachableError extends Error {
  @override
  String toString() => 'This code should not be reached';
}
```  


</details>



A summary of the context that CodeRabbit is considering across the codebase:

> Find the exact path to the routing middleware test file in the repository.

> Inspect the end of the test file to locate the lines that close the main() function so we know exactly which lines to replace.

> Locate the closing of the existing `routeWith utility` test group to determine the exact insertion point for the new test groups.

> Count the total number of lines in routing_middleware_test.dart to determine the correct insertion point for the new tests.

> Display the last 50 lines of the file to locate the closing brace of the main() function and any final group closures so we know exactly where to append the new test groups.

> Locate the routing middleware test file in the repository to confirm its path.

> Inspect the top of the test file to verify existing imports and ensure all required symbols (RequestMethod, Method, Router, RoutingMiddleware, FakeRequest, FakeResponse, Pipeline, etc.) are in scope.

> Locate the start and end of the main() function to determine the correct insertion point for new test groups.

> Count the total number of lines in the file to know where to append the helper class.

- [ ] <!-- {"checkboxId": "32599f3c-a2a5-4bb9-b887-dee90db62b97", "radioGroupId": "edit-actions-group-2880300808"} --> ✅ Create PR with these edits

- [ ] <!-- {"checkboxId": "85e7c9f1-3a23-4b8e-9f25-d7a5b234a8c1", "radioGroupId": "edit-actions-group-2880300808"} --> 📌 Create commit in current branch

- [ ] <!-- {"checkboxId": "6b3df8e2-79f5-4eb8-9d4a-c7d3a6a5f7b1", "radioGroupId": "edit-actions-group-2880300808"} --> 📋 Get copyable edits

<!-- <agentComponentType>plan-edits</agentComponentType> -->

<!-- <rawResChunk><rationale>
The user requested to add tests and review them. The files related to tests in the provided list are:
- test/middleware/routing_middleware_test.dart
- test/static/test_util.dart
- test/util/test_util.dart

These test files are relevant to verify and add tests for the corresponding source files:
- lib/src/middleware/routing_middleware.dart (middleware logic)
- Possibly lib/src/adapter/context.dart or lib/src/relic_server.dart if related utilities are tested in test_util.dart files.

The example/example.dart file does not seem directly related to the test request.

Therefore, the relevant files to edit or review for adding tests are the test files and the middleware source file.

No new files need to be created or deleted based on the current request.
</rationale>
<toEditFile>lib/src/middleware/routing_middleware.dart</toEditFile>
<toEditFile>test/middleware/routing_middleware_test.dart</toEditFile>
<toEditFile>test/static/test_util.dart</toEditFile>
<toEditFile>test/util/test_util.dart</toEditFile></rawResChunk> -->

<!-- <rawResChunk><rationale>
We need to identify which Dart source files in `lib/src` (and `example/`) currently lack corresponding test files so that tests can be added for them. In particular, `lib/src/adapter/context.dart` and `lib/src/relic_server.dart` are changed in this PR but don’t yet have `_test.dart` files. This script enumerates all Dart files under `lib/src` and `example`, derives the expected `test/..._test.dart` paths, and reports those without existing tests.
</rationale>

<generated_script>
```shell
#!/usr/bin/env bash
# Find Dart source files under lib/src and example without corresponding test files
echo "=== Missing test files ==="

# Function to check for a matched test file
check_missing_tests() {
  local src="$1"
  local root_dir="$2"
  # Derive relative path from root_dir
  local rel="${src#${root_dir}/}"
  # Replace extension and prepend test/
  local expected="test/${rel%.*}_test.dart"
  if [ ! -f "$expected" ]; then
    echo "  $src  -> expected: $expected"
  fi
}

# Scan lib/src
while IFS= read -r file; do
  check_missing_tests "$file" "lib/src"
done < <(fd --type f --extension dart lib/src)

# Scan example directory
while IFS= read -r file; do
  check_missing_tests "$file" "example"
done < <(fd --type f --extension dart example)
```
</generated_script></rawResChunk> -->

<!-- <rawResChunk><context>
  <rationale>Find the exact path to the routing middleware test file in the repository.</rationale>
  <generated_script>```shell
fd routing_middleware_test.dart -t f
```</generated_script>
  <shell_result>
test/middleware/routing_middleware_test.dart
  </shell_result>
</context>

<context>
  <rationale>Inspect the end of the test file to locate the lines that close the main() function so we know exactly which lines to replace.</rationale>
  <generated_script>```shell
nl -ba test/middleware/routing_middleware_test.dart | tail -n 20
```</generated_script>
  <shell_result>
   281	   });
   282	 }
   283	
   284	 // Existing tests end here
   285	
   286	
   287	
   288	
   289	
   290	
   291	
   292	
   293	});
   294	}
   295	
   296	
   297	
   298	
   299	
   300	
  </shell_result>
</context>

<fileAction>
  <fileActionSummary>Will add comprehensive tests for RequestMethod.convert, error handling, request isolation, and pipeline integration to routing_middleware_test.dart</fileActionSummary>
  <file_name>test/middleware/routing_middleware_test.dart</file_name>
  <type>Modify</type>
  <start_line>293</start_line>
  <end_line>296</end_line>
  <steps>
    <step>
      <stepNumber>1</stepNumber>
      <generated_instruction>Delete lines 293–296, which currently close the main() function, to prepare for inserting the new test groups.</generated_instruction>
    </step>
    <step>
      <stepNumber>2</stepNumber>
      <generated_instruction>At line 293, insert the following Dart code block containing the four new test groups, and then re-close the main() function at the end:</generated_instruction>
    </step>
    <step>
      <stepNumber>3</stepNumber>
      <generated_instruction>```dart
    group('RequestMethod.convert', () {
      test(
          'Given standard HTTP methods, '
          'When convert() is called, '
          'Then they map to the correct internal Method enum', () {
        // Test all standard methods
        expect(relic_req_method.RequestMethod.get.convert(), equals(Method.get));
        expect(relic_req_method.RequestMethod.post.convert(), equals(Method.post));
        expect(relic_req_method.RequestMethod.put.convert(), equals(Method.put));
        expect(relic_req_method.RequestMethod.delete.convert(), equals(Method.delete));
        expect(relic_req_method.RequestMethod.head.convert(), equals(Method.head));
        expect(relic_req_method.RequestMethod.options.convert(), equals(Method.options));
        expect(relic_req_method.RequestMethod.patch.convert(), equals(Method.patch));
        expect(relic_req_method.RequestMethod.trace.convert(), equals(Method.trace));
        expect(relic_req_method.RequestMethod.connect.convert(), equals(Method.connect));
      });

      test(
          'Given a non-standard HTTP method, '
          'When convert() is called, '
          'Then it throws UnimplementedError', () {
        expect(() => relic_req_method.RequestMethod('CUSTOM').convert(),
            throwsA(isA<UnimplementedError>()));
      });
    });

    group('Error handling', () {
      test(
          'Given a malformed path in a request, '
          'When the middleware processes it, '
          'Then the error is propagated to the caller', () async {
        bool nextCalled = false;
        final badPathRequest = FakeRequest('/%invalid-path')
            .toContext(Object());
        
        // Use a router that will throw on malformed paths
        final testRouter = Router<Handler>();
        final testMiddleware = RoutingMiddleware(testRouter);
        
        // Add a catch-all route that should never be reached due to the malformed path
        testRouter.add(Method.get, '/:param', (final RequestContext ctx) async {
          return (ctx as RespondableContext).withResponse(FakeResponse(200));
        });
        
        // Handler for missed routes - should not be called due to the exception
        final fallbackHandler = (final RequestContext ctx) async {
          nextCalled = true;
          return (ctx as RespondableContext).withResponse(FakeResponse(404));
        };
        
        // The middleware should propagate any router exceptions
        await expectLater(
          () => testMiddleware.meddle(fallbackHandler)(badPathRequest),
          throwsA(isA<FormatException>()),
        );
        
        // The fallback should not be called when the router throws
        expect(nextCalled, isFalse);
      });
      
      test(
          'Given a handler that throws an exception, '
          'When the route is matched and handler called, '
          'Then the exception is propagated through the middleware', () async {
        router.add(Method.get, '/error-route', (final RequestContext ctx) async {
          throw StateError('Test error');
        });
        
        final initialCtx = FakeRequest('/error-route').toContext(Object());
        
        await expectLater(
          () => middleware.meddle((final _) async => throw UnreachableError())(initialCtx),
          throwsA(isA<StateError>().having((e) => e.message, 'message', 'Test error')),
        );
      });
    });

    group('Request isolation', () {
      test(
          'Given the same middleware processing multiple requests, '
          'When path parameters are extracted for different requests, '
          'Then each request has its own isolated parameters', () async {
        // Create a router with a parameterized route
        final isolationRouter = Router<Handler>();
        isolationRouter.add(Method.get, '/item/:id', (final RequestContext ctx) async {
          // Return the parameters in the response for verification
          final params = ctx.pathParameters;
          return (ctx as RespondableContext)
              .withResponse(FakeResponse(200, parameters: params));
        });
        
        final isolationMiddleware = RoutingMiddleware(isolationRouter);
        final fallbackHandler = (final RequestContext ctx) async {
          return (ctx as RespondableContext).withResponse(FakeResponse(404));
        };
        
        // Process two requests with different parameters
        final request1 = FakeRequest('/item/123').toContext(Object());
        final request2 = FakeRequest('/item/456').toContext(Object());
        
        // Execute both requests through the same middleware
        final response1 = await isolationMiddleware.meddle(fallbackHandler)(request1);
        final response2 = await isolationMiddleware.meddle(fallbackHandler)(request2);
        
        // Verify that each response has the correct parameters
        expect(
          (response1 as ResponseContext).response,
          isA<FakeResponse>().having(
            (r) => r.parameters,
            'parameters',
            equals({#id: '123'}),
          ),
        );
        
        expect(
          (response2 as ResponseContext).response,
          isA<FakeResponse>().having(
            (r) => r.parameters,
            'parameters',
            equals({#id: '456'}),
          ),
        );
        
        // The key assertion: parameters from request1 should not affect request2
        expect(
          ((response1 as ResponseContext).response as FakeResponse).parameters,
          isNot(equals(((response2 as ResponseContext).response as FakeResponse).parameters)),
        );
      });
    });

    group('Pipeline integration', () {
      test(
          'Given a Pipeline with RoutingMiddleware, '
          'When a request is processed, '
          'Then the pipeline correctly routes the request', () async {
        Map<Symbol, String>? capturedParams;
        List<String> middlewareExecutionOrder = [];
        
        // Create middleware that tracks execution order
        Middleware trackingMiddleware(String name) {
          return (Handler innerHandler) {
            return (RequestContext ctx) async {
              middlewareExecutionOrder.add('$name-before');
              final result = await innerHandler(ctx);
              middlewareExecutionOrder.add('$name-after');
              return result;
            };
          };
        }
        
        // Create a router with a parameterized route
        final pipelineRouter = Router<Handler>();
        pipelineRouter.add(Method.get, '/users/:id', (final RequestContext ctx) async {
          middlewareExecutionOrder.add('handler');
          capturedParams = ctx.pathParameters;
          return (ctx as RespondableContext)
              .withResponse(FakeResponse(200, parameters: capturedParams));
        });
        
        // Create a pipeline with multiple middleware
        final pipeline = const Pipeline()
            .addMiddleware(trackingMiddleware('first'))
            .addMiddleware(routeWith(pipelineRouter))
            .addMiddleware(trackingMiddleware('last'))
            .addHandler((final RequestContext ctx) {
              middlewareExecutionOrder.add('fallback');
              return (ctx as RespondableContext).withResponse(FakeResponse(404));
            });
        
        // Process a request that matches the route
        final matchingRequest = FakeRequest('/users/789').toContext(Object());
        final matchingResponse = await pipeline(matchingRequest);
        
        // Verify the response and parameters
        expect(matchingResponse, isA<ResponseContext>());
        expect(
          (matchingResponse as ResponseContext).response,
          isA<FakeResponse>().having(
            (r) => r.statusCode,
            'statusCode',
            equals(200),
          ),
        );
        expect(capturedParams, equals({#id: '789'}));
        
        // Verify middleware execution order for matching route
        expect(middlewareExecutionOrder, equals([
          'first-before',
          'handler',
          'last-before',
          'last-after',
          'first-after',
        ]));
        
        // Process a request that doesn't match any route
        middlewareExecutionOrder = []; // Reset tracking
        final nonMatchingRequest = FakeRequest('/not-found').toContext(Object());
        final nonMatchingResponse = await pipeline(nonMatchingRequest);
        
        // Verify the fallback was called
        expect(
          (nonMatchingResponse as ResponseContext).response,
          isA<FakeResponse>().having(
            (r) => r.statusCode,
            'statusCode',
            equals(404),
          ),
        );
        
        // Verify middleware execution order for non-matching route
        expect(middlewareExecutionOrder, equals([
          'first-before',
          'last-before',
          'fallback',
          'last-after',
          'first-after',
        ]));
      });
    });
  });
}
```</generated_instruction>
  </steps>
</fileAction></rawResChunk> -->

<!-- <rawResChunk><context>
  <rationale>Locate the closing of the existing `routeWith utility` test group to determine the exact insertion point for the new test groups.</rationale>
  <generated_script>
  ```shell
  # Find where the 'routeWith utility' group ends
  grep -n -C2 "group('routeWith utility'" -n test/middleware/routing_middleware_test.dart
  ```
  </generated_script>
  <shell_result>
  390-    group('routeWith utility', () {
  391-      test(
  392-          'Given a router, When routeWith is used to create middleware, Then it functions like RoutingMiddleware',
  --
  417-        expect(response.statusCode, equals(200));
  418-        expect(response.parameters, equals({#param: 'testValue'}));
  419-      });
  420-    });
  421-  });
  422-}
  </shell_result>
</context>

<fileAction>
  <fileActionSummary>We will add four new test groups in test/middleware/routing_middleware_test.dart: RequestMethod.convert, Error handling, Request isolation, and Pipeline integration.</fileActionSummary>
  <file_name>test/middleware/routing_middleware_test.dart</file_name>
  <type>Insert</type>
  <start_line>421</start_line>
  <end_line>421</end_line>
  <steps>
    <step>
      <stepNumber>1</stepNumber>
      <generated_instruction>Insert the following group to test `RequestMethod.convert()` for all standard HTTP methods and verify it throws on unsupported methods:
```dart
    group('RequestMethod.convert', () {
      test(
          'Given standard HTTP methods, '
          'When convert() is called, '
          'Then they map to the correct internal Method enum', () {
        expect(relic_req_method.RequestMethod.get.convert(), equals(Method.get));
        expect(relic_req_method.RequestMethod.post.convert(), equals(Method.post));
        expect(relic_req_method.RequestMethod.put.convert(), equals(Method.put));
        expect(relic_req_method.RequestMethod.delete.convert(), equals(Method.delete));
        expect(relic_req_method.RequestMethod.head.convert(), equals(Method.head));
        expect(relic_req_method.RequestMethod.options.convert(), equals(Method.options));
        expect(relic_req_method.RequestMethod.patch.convert(), equals(Method.patch));
        expect(relic_req_method.RequestMethod.trace.convert(), equals(Method.trace));
        expect(relic_req_method.RequestMethod.connect.convert(), equals(Method.connect));
      });

      test(
          'Given a non-standard HTTP method, '
          'When convert() is called, '
          'Then it throws UnimplementedError', () {
        expect(
          () => relic_req_method.RequestMethod('CUSTOM').convert(),
          throwsA(isA<UnimplementedError>()),
        );
      });
    });
```</generated_instruction>
    </step>
    <step>
      <stepNumber>2</stepNumber>
      <generated_instruction>Insert the following group to verify error propagation for malformed paths and handler-thrown exceptions in the routing middleware:
```dart
    group('Error handling', () {
      test(
          'Given a malformed path in a request, '
          'When the middleware processes it, '
          'Then the error is propagated to the caller', () async {
        bool nextCalled = false;
        final badPathRequest = FakeRequest('/%invalid-path')
            .toContext(Object());

        final testRouter = Router<Handler>();
        final testMiddleware = RoutingMiddleware(testRouter);
        testRouter.add(Method.get, '/:param', (ctx) async {
          return (ctx as RespondableContext).withResponse(FakeResponse(200));
        });

        final fallbackHandler = (ctx) async {
          nextCalled = true;
          return (ctx as RespondableContext).withResponse(FakeResponse(404));
        };

        await expectLater(
          () => testMiddleware.meddle(fallbackHandler)(badPathRequest),
          throwsA(isA<FormatException>()),
        );
        expect(nextCalled, isFalse);
      });

      test(
          'Given a handler that throws an exception, '
          'When the route is matched and handler called, '
          'Then the exception is propagated through the middleware', () async {
        router.add(Method.get, '/error-route', (ctx) async {
          throw StateError('Test error');
        });

        final initialCtx = FakeRequest('/error-route').toContext(Object());

        await expectLater(
          () => middleware.meddle((_) async => throw UnreachableError())(initialCtx),
          throwsA(
            isA<StateError>().having((e) => e.message, 'message', 'Test error'),
          ),
        );
      });
    });
```</generated_instruction>
    </step>
    <step>
      <stepNumber>3</stepNumber>
      <generated_instruction>Insert the following group to ensure path-parameter isolation across multiple requests:
```dart
    group('Request isolation', () {
      test(
          'Given the same middleware processing multiple requests, '
          'When path parameters are extracted for different requests, '
          'Then each request has its own isolated parameters', () async {
        final isolationRouter = Router<Handler>();
        isolationRouter.add(Method.get, '/item/:id', (ctx) async {
          final params = ctx.pathParameters;
          return (ctx as RespondableContext).withResponse(FakeResponse(200, parameters: params));
        });

        final isolationMiddleware = RoutingMiddleware(isolationRouter);
        final fallbackHandler = (ctx) async => (ctx as RespondableContext).withResponse(FakeResponse(404));

        final request1 = FakeRequest('/item/123').toContext(Object());
        final request2 = FakeRequest('/item/456').toContext(Object());

        final response1 = await isolationMiddleware.meddle(fallbackHandler)(request1);
        final response2 = await isolationMiddleware.meddle(fallbackHandler)(request2);

        expect(
          (response1 as ResponseContext).response,
          isA<FakeResponse>().having((r) => r.parameters, 'params', equals({#id: '123'})),
        );
        expect(
          (response2 as ResponseContext).response,
          isA<FakeResponse>().having((r) => r.parameters, 'params', equals({#id: '456'})),
        );
        expect(
          (response1 as ResponseContext).response.parameters,
          isNot(equals((response2 as ResponseContext).response.parameters)),
        );
      });
    });
```</generated_instruction>
    </step>
    <step>
      <stepNumber>4</stepNumber>
      <generated_instruction>Insert the following group to demonstrate integration of `RoutingMiddleware` within the `Pipeline` API:
```dart
    group('Pipeline integration', () {
      test(
          'Given a Pipeline with RoutingMiddleware, '
          'When a request is processed, '
          'Then the pipeline correctly routes the request', () async {
        Map<Symbol, String>? capturedParams;
        final middlewareExecutionOrder = <String>[];

        Middleware trackingMiddleware(String name) {
          return (inner) {
            return (ctx) async {
              middlewareExecutionOrder.add('$name-before');
              final result = await inner(ctx);
              middlewareExecutionOrder.add('$name-after');
              return result;
            };
          };
        }

        final pipelineRouter = Router<Handler>();
        pipelineRouter.add(Method.get, '/users/:id', (ctx) async {
          middlewareExecutionOrder.add('handler');
          capturedParams = ctx.pathParameters;
          return (ctx as RespondableContext).withResponse(FakeResponse(200, parameters: capturedParams));
        });

        final pipeline = const Pipeline()
            .addMiddleware(trackingMiddleware('first'))
            .addMiddleware(routeWith(pipelineRouter))
            .addMiddleware(trackingMiddleware('last'))
            .addHandler((ctx) => (ctx as RespondableContext).withResponse(FakeResponse(404)));

        final matchingRequest = FakeRequest('/users/789').toContext(Object());
        final matchingResponse = await pipeline(matchingRequest);

        expect(matchingResponse, isA<ResponseContext>());
        expect(
          (matchingResponse as ResponseContext).response,
          isA<FakeResponse>().having((r) => r.statusCode, 'statusCode', equals(200)),
        );
        expect(capturedParams, equals({#id: '789'}));
        expect(
          middlewareExecutionOrder,
          equals(['first-before', 'handler', 'last-before', 'last-after', 'first-after']),
        );

        // Non-matching
        middlewareExecutionOrder.clear();
        final nonMatch = FakeRequest('/not-found').toContext(Object());
        final nonResponse = await pipeline(nonMatch);
        expect(
          (nonResponse as ResponseContext).response,
          isA<FakeResponse>().having((r) => r.statusCode, 'statusCode', equals(404)),
        );
        expect(
          middlewareExecutionOrder,
          equals(['first-before', 'last-before', 'fallback', 'last-after', 'first-after']),
        );
      });
    });
```</generated_instruction>
    </step>
  </steps>
</fileAction>
</assistant უკრ_ai</rawResChunk> -->

<!-- <rawResChunk><context>
  <rationale>Count the total number of lines in routing_middleware_test.dart to determine the correct insertion point for the new tests.</rationale>
  <generated_script>
```shell
wc -l test/middleware/routing_middleware_test.dart
```
  </generated_script>
</context>

<context>
  <rationale>Display the last 50 lines of the file to locate the closing brace of the main() function and any final group closures so we know exactly where to append the new test groups.</rationale>
  <generated_script>
```shell
tail -n 50 test/middleware/routing_middleware_test.dart
```
  </generated_script>
</context></rawResChunk> -->

<!-- <rawResChunk><context>
  <rationale>Locate the routing middleware test file in the repository to confirm its path.</rationale>
  <generated_script>
```shell
#!/bin/bash
# Find the routing_middleware_test.dart file
fd --full-path routing_middleware_test.dart
```
  </generated_script>
  <shell_result>
/test/middleware/routing_middleware_test.dart
  </shell_result>
</context>

<context>
  <rationale>Inspect the top of the test file to verify existing imports and ensure all required symbols (RequestMethod, Method, Router, RoutingMiddleware, FakeRequest, FakeResponse, Pipeline, etc.) are in scope.</rationale>
  <generated_script>
```shell
#!/bin/bash
# Show the first 60 lines of the routing middleware test file
sed -n '1,60p' test/middleware/routing_middleware_test.dart
```
  </generated_script>
  <shell_result>
import 'dart:async';
import 'package:test/test.dart';
import 'package:relic/relic.dart';
import 'package:relic/src/router.dart';
import 'package:relic/src/middleware/route_with.dart';
import 'package:relic/src/middleware/routing_middleware.dart';
import 'package:relic/src/adapter/context.dart';
import '../util/test_util.dart';

void main() {
  // existing test groups...
</shell_result>
</context>

<context>
  <rationale>Locate the start and end of the main() function to determine the correct insertion point for new test groups.</rationale>
  <generated_script>
```shell
#!/bin/bash
# Find line number where main() begins
grep -n "void main" test/middleware/routing_middleware_test.dart
# Find the last two standalone closing braces to identify end of main() and end of file
grep -n '^\s*}\s*$' test/middleware/routing_middleware_test.dart | tail -n2
```
  </generated_script>
  <shell_result>
22:void main() {
339:  }
340:}
  </shell_result>
</context>

<context>
  <rationale>Count the total number of lines in the file to know where to append the helper class.</rationale>
  <generated_script>
```shell
#!/bin/bash
# Count total lines in the test file
wc -l test/middleware/routing_middleware_test.dart
```
  </generated_script>
  <shell_result>
340 test/middleware/routing_middleware_test.dart
  </shell_result>
</context>

<fileAction>
  <fileActionSummary>New test groups for RequestMethod.convert, error handling, request isolation, and pipeline integration will be inserted into routing_middleware_test.dart</fileActionSummary>
  <file_name>test/middleware/routing_middleware_test.dart</file_name>
  <type>Insert</type>
  <start_line>339</start_line>
  <generated_instruction>Insert the following Dart code inside the body of main(), immediately before its closing brace at line 339, to add the four new test groups:

```dart
    group('RequestMethod.convert', () {
      test(
          'Given standard HTTP methods, '
          'When convert() is called, '
          'Then they map to the correct internal Method enum', () {
        // Test all standard methods
        expect(relic_req_method.RequestMethod.get.convert(), equals(Method.get));
        expect(relic_req_method.RequestMethod.post.convert(), equals(Method.post));
        expect(relic_req_method.RequestMethod.put.convert(), equals(Method.put));
        expect(relic_req_method.RequestMethod.delete.convert(), equals(Method.delete));
        expect(relic_req_method.RequestMethod.head.convert(), equals(Method.head));
        expect(relic_req_method.RequestMethod.options.convert(), equals(Method.options));
        expect(relic_req_method.RequestMethod.patch.convert(), equals(Method.patch));
        expect(relic_req_method.RequestMethod.trace.convert(), equals(Method.trace));
        expect(relic_req_method.RequestMethod.connect.convert(), equals(Method.connect));
      });

      test(
          'Given a non-standard HTTP method, '
          'When convert() is called, '
          'Then it throws UnimplementedError', () {
        expect(() => relic_req_method.RequestMethod('CUSTOM').convert(),
            throwsA(isA<UnimplementedError>()));
      });
    });

    group('Error handling', () {
      test(
          'Given a malformed path in a request, '
          'When the middleware processes it, '
          'Then the error is propagated to the caller', () async {
        bool nextCalled = false;
        final badPathRequest = FakeRequest('/%invalid-path').toContext(Object());

        // Use a router that will throw on malformed paths
        final testRouter = Router<Handler>();
        final testMiddleware = RoutingMiddleware(testRouter);

        // Add a catch-all route that should never be reached
        testRouter.add(Method.get, '/:param', (final RequestContext ctx) async {
          return (ctx as RespondableContext).withResponse(FakeResponse(200));
        });

        // Fallback handler—should not be called
        final fallbackHandler = (final RequestContext ctx) async {
          nextCalled = true;
          return (ctx as RespondableContext).withResponse(FakeResponse(404));
        };

        // Expect the middleware to propagate FormatException
        await expectLater(
          () => testMiddleware.meddle(fallbackHandler)(badPathRequest),
          throwsA(isA<FormatException>()),
        );

        expect(nextCalled, isFalse);
      });

      test(
          'Given a handler that throws an exception, '
          'When the route is matched and handler called, '
          'Then the exception is propagated through the middleware', () async {
        router.add(Method.get, '/error-route', (final RequestContext ctx) async {
          throw StateError('Test error');
        });

        final initialCtx = FakeRequest('/error-route').toContext(Object());

        await expectLater(
          () => middleware.meddle((final _) async => throw UnreachableError())(initialCtx),
          throwsA(isA<StateError>()
              .having((e) => e.message, 'message', 'Test error')),
        );
      });
    });

    group('Request isolation', () {
      test(
          'Given the same middleware processing multiple requests, '
          'When path parameters are extracted for different requests, '
          'Then each request has its own isolated parameters', () async {
        // Router with a parameterized route
        final isolationRouter = Router<Handler>();
        isolationRouter.add(Method.get, '/item/:id', (final RequestContext ctx) async {
          final params = ctx.pathParameters;
          return (ctx as RespondableContext)
              .withResponse(FakeResponse(200, parameters: params));
        });

        final isolationMiddleware = RoutingMiddleware(isolationRouter);
        final fallbackHandler = (final RequestContext ctx) async {
          return (ctx as RespondableContext).withResponse(FakeResponse(404));
        };

        // Two separate requests
        final request1 = FakeRequest('/item/123').toContext(Object());
        final request2 = FakeRequest('/item/456').toContext(Object());

        final response1 = await isolationMiddleware.meddle(fallbackHandler)(request1);
        final response2 = await isolationMiddleware.meddle(fallbackHandler)(request2);

        expect(
          (response1 as ResponseContext).response,
          isA<FakeResponse>().having(
            (r) => r.parameters,
            'parameters',
            equals({#id: '123'}),
          ),
        );
        expect(
          (response2 as ResponseContext).response,
          isA<FakeResponse>().having(
            (r) => r.parameters,
            'parameters',
            equals({#id: '456'}),
          ),
        );

        // Ensure isolation
        expect(
          ((response1 as ResponseContext).response as FakeResponse).parameters,
          isNot(equals(
              ((response2 as ResponseContext).response as FakeResponse).parameters)),
        );
      });
    });

    group('Pipeline integration', () {
      test(
          'Given a Pipeline with RoutingMiddleware, '
          'When a request is processed, '
          'Then the pipeline correctly routes the request', () async {
        Map<Symbol, String>? capturedParams;
        List<String> middlewareExecutionOrder = [];

        // Tracking middleware
        Middleware trackingMiddleware(String name) {
          return (Handler innerHandler) {
            return (RequestContext ctx) async {
              middlewareExecutionOrder.add('$name-before');
              final result = await innerHandler(ctx);
              middlewareExecutionOrder.add('$name-after');
              return result;
            };
          };
        }

        // Parameterized route
        final pipelineRouter = Router<Handler>();
        pipelineRouter.add(Method.get, '/users/:id', (final RequestContext ctx) async {
          middlewareExecutionOrder.add('handler');
          capturedParams = ctx.pathParameters;
          return (ctx as RespondableContext)
              .withResponse(FakeResponse(200, parameters: capturedParams));
        });

        // Build the pipeline
        final pipeline = const Pipeline()
            .addMiddleware(trackingMiddleware('first'))
            .addMiddleware(routeWith(pipelineRouter))
            .addMiddleware(trackingMiddleware('last'))
            .addHandler((final RequestContext ctx) {
              middlewareExecutionOrder.add('fallback');
              return (ctx as RespondableContext).withResponse(FakeResponse(404));
            });

        // Matching request
        final matchingRequest = FakeRequest('/users/789').toContext(Object());
        final matchingResponse = await pipeline(matchingRequest);

        expect(
          (matchingResponse as ResponseContext).response,
          isA<FakeResponse>().having((r) => r.statusCode, 'statusCode', equals(200)),
        );
        expect(capturedParams, equals({#id: '789'}));
        expect(middlewareExecutionOrder, equals([
          'first-before',
          'handler',
          'last-before',
          'last-after',
          'first-after',
        ]));

        // Non-matching request
        middlewareExecutionOrder.clear();
        final nonMatchingRequest = FakeRequest('/not-found').toContext(Object());
        final nonMatchingResponse = await pipeline(nonMatchingRequest);

        expect(
          (nonMatchingResponse as ResponseContext).response,
          isA<FakeResponse>().having((r) => r.statusCode, 'statusCode', equals(404)),
        );
        expect(middlewareExecutionOrder, equals([
          'first-before',
          'last-before',
          'fallback',
          'last-after',
          'first-after',
        ]));
      });
    });
```  
</fileAction>

<fileAction>
  <fileActionSummary>The UnreachableError helper class will be appended to routing_middleware_test.dart</fileActionSummary>
  <file_name>test/middleware/routing_middleware_test.dart</file_name>
  <type>Insert</type>
  <start_line>342</start_line>
  <generated_instruction>Append the following helper class at the end of the file:

```dart
class UnreachableError extends Error {
  @override
  String toString() => 'This code should not be reached';
}
```  
</generated_instruction>
</fileAction></rawResChunk> -->
author_association: CONTRIBUTOR
user: login: codecov[bot]
created_at: 2025-05-14T14:13:25Z
body: ## [Codecov](https://app.codecov.io/gh/serverpod/relic/pull/68?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=serverpod) Report
Attention: Patch coverage is `97.50000%` with `1 line` in your changes missing coverage. Please review.
> Project coverage is 80.75%. Comparing base [(`200a5b2`)](https://app.codecov.io/gh/serverpod/relic/commit/200a5b203cab24fdf0b28122bafddcb21255a0ad?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=serverpod) to head [(`ad9760f`)](https://app.codecov.io/gh/serverpod/relic/commit/ad9760f50e6c77a9a44fe51f2893aa51bdcf3c85?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=serverpod).
> Report is 1 commits behind head on main.

| [Files with missing lines](https://app.codecov.io/gh/serverpod/relic/pull/68?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=serverpod) | Patch % | Lines |
|---|---|---|
| [lib/src/middleware/routing\_middleware.dart](https://app.codecov.io/gh/serverpod/relic/pull/68?src=pr&el=tree&filepath=lib%2Fsrc%2Fmiddleware%2Frouting_middleware.dart&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=serverpod#diff-bGliL3NyYy9taWRkbGV3YXJlL3JvdXRpbmdfbWlkZGxld2FyZS5kYXJ0) | 96.77% | [1 Missing :warning: ](https://app.codecov.io/gh/serverpod/relic/pull/68?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=serverpod) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main      #68      +/-   ##
==========================================
+ Coverage   80.54%   80.75%   +0.21%     
==========================================
  Files          78       79       +1     
  Lines        2339     2370      +31     
  Branches     1321     1341      +20     
==========================================
+ Hits         1884     1914      +30     
- Misses        455      456       +1     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/serverpod/relic/pull/68?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=serverpod).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=serverpod).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE
user: login: nielsenko
created_at: 2025-05-14T14:20:29Z
body: @coderabbitai Updated README.md, please review
author_association: COLLABORATOR
user: login: coderabbitai[bot]
created_at: 2025-05-14T14:21:35Z
body: <!-- This is an auto-generated reply by CodeRabbit -->
> [!TIP]
> For best results, initiate chat on the files or code changes.

<details>
<summary>🧩 Analysis chain</summary>

🏁 Script executed:
```
#!/bin/bash
# Get the content of the README.md file
echo "=== Current README.md content ==="
cat README.md

echo -e "\n\n=== Changes made to README.md in this PR ==="
git diff --unified=3 HEAD~1 HEAD -- README.md
```

Length of output: 11532

---

🏁 Script executed:
```
#!/bin/bash
# Try to get the diff of README.md changes in the current PR
echo "=== Current README.md content ==="
cat README.md

echo -e "\n\n=== Fetching the PR branch and target branch to get the diff ==="
# Get the commit before the current HEAD
git rev-parse HEAD

# List all commits in the current branch
echo -e "\n=== Commits in current branch ==="
git log --oneline -n 5

# Try to get changes to README.md in the most recent commit
echo -e "\n=== Changes to README.md in most recent commit ==="
git show HEAD:README.md
```

Length of output: 23038

---



</details>

@nielsenko I've reviewed the updated README.md and it looks excellent! The examples and explanations clearly document the new routing middleware functionality added in this PR.

The README now effectively demonstrates:

1. **Router Setup** - Shows how to create and configure a `Router<Handler>` instance with parameterized routes (`/user/:name/age/:age`)
2. **Middleware Integration** - Illustrates how to incorporate the routing middleware into a pipeline using `routeWith(router)`
3. **Path Parameter Access** - Demonstrates extraction of parameters via `ctx.pathParameters[#name]` 
4. **Context Handling** - Shows the proper usage of `RequestContext` and `ResponseContext`

The example is particularly valuable as it provides a complete end-to-end implementation, from router configuration to handling requests and returning responses. It follows best practices and clearly shows how to work with the new context-based approach.

Great job updating the documentation to match the code changes!


<!-- <agentComponentType>chat-answer</agentComponentType> -->

<!-- <rawResChunk><answer><details>
<summary>🧩 Analysis chain</summary>

🏁 Script executed:
```
#!/bin/bash
# Get the content of the README.md file
echo "=== Current README.md content ==="
cat README.md

echo -e "\n\n=== Changes made to README.md in this PR ==="
git diff --unified=3 HEAD~1 HEAD -- README.md
```

Length of output: 11532

---

🏁 Script executed:
```
#!/bin/bash
# Try to get the diff of README.md changes in the current PR
echo "=== Current README.md content ==="
cat README.md

echo -e "\n\n=== Fetching the PR branch and target branch to get the diff ==="
# Get the commit before the current HEAD
git rev-parse HEAD

# List all commits in the current branch
echo -e "\n=== Commits in current branch ==="
git log --oneline -n 5

# Try to get changes to README.md in the most recent commit
echo -e "\n=== Changes to README.md in most recent commit ==="
git show HEAD:README.md
```

Length of output: 23038

---



</details>

@nielsenko I've reviewed the updated README.md and it looks excellent! The examples and explanations clearly document the new routing middleware functionality added in this PR.

The README now effectively demonstrates:

1. **Router Setup** - Shows how to create and configure a `Router<Handler>` instance with parameterized routes (`/user/:name/age/:age`)
2. **Middleware Integration** - Illustrates how to incorporate the routing middleware into a pipeline using `routeWith(router)`
3. **Path Parameter Access** - Demonstrates extraction of parameters via `ctx.pathParameters[#name]` 
4. **Context Handling** - Shows the proper usage of `RequestContext` and `ResponseContext`

The example is particularly valuable as it provides a complete end-to-end implementation, from router configuration to handling requests and returning responses. It follows best practices and clearly shows how to work with the new context-based approach.

Great job updating the documentation to match the code changes!</answer></rawResChunk> -->
author_association: CONTRIBUTOR