Added packages shelf_router and shelf_router_generator
diff --git a/pkgs/shelf_router/CHANGELOG.md b/pkgs/shelf_router/CHANGELOG.md
new file mode 100644
index 0000000..ed39b04
--- /dev/null
+++ b/pkgs/shelf_router/CHANGELOG.md
@@ -0,0 +1,5 @@
+# Changelog
+
+## 0.7.0
+
+ * Initial release
diff --git a/pkgs/shelf_router/LICENSE b/pkgs/shelf_router/LICENSE
new file mode 100644
index 0000000..7a4a3ea
--- /dev/null
+++ b/pkgs/shelf_router/LICENSE
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
\ No newline at end of file
diff --git a/pkgs/shelf_router/README.md b/pkgs/shelf_router/README.md
new file mode 100644
index 0000000..87addb8
--- /dev/null
+++ b/pkgs/shelf_router/README.md
@@ -0,0 +1,33 @@
+# Web Request Router for Shelf
+
+[Shelf](https://pub.dartlang.org/packages/shelf) makes it easy to build web
+applications in Dart by composing request handlers. This package offers a
+request router for Shelf, matching request to handlers using route patterns.
+
+**Disclaimer:** This is not an officially supported Google product.
+
+Also see the `shelf_router_generator` package for how to automatically generate
+a `Route` using the `Route` annotation in this package.
+
+## Example
+
+```dart
+import 'package:shelf_router/shelf_router.dart';
+import 'package:shelf/shelf.dart';
+import 'package:shelf/shelf_io.dart' as io;
+
+var app = Router();
+
+app.get('/hello', (Request request) {
+  return Response.ok('hello-world');
+});
+
+app.get('/user/<user>', (Request request, String user) {
+  return Response.ok('hello $user');
+});
+
+var server = await io.serve(app.handler, 'localhost', 8080);
+```
+
+See reference documentation of `Router` class for more information.
+
diff --git a/pkgs/shelf_router/analysis_options.yaml b/pkgs/shelf_router/analysis_options.yaml
new file mode 100644
index 0000000..108d105
--- /dev/null
+++ b/pkgs/shelf_router/analysis_options.yaml
@@ -0,0 +1 @@
+include: package:pedantic/analysis_options.yaml
diff --git a/pkgs/shelf_router/example/main.dart b/pkgs/shelf_router/example/main.dart
new file mode 100644
index 0000000..e95344d
--- /dev/null
+++ b/pkgs/shelf_router/example/main.dart
@@ -0,0 +1,85 @@
+// Copyright 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'dart:async' show Future;
+import 'package:shelf_router/shelf_router.dart';
+import 'package:shelf/shelf.dart';
+import 'package:shelf/shelf_io.dart' as shelf_io;
+
+class Service {
+  // The [Router] can be used to create a handler, which can be used with
+  // [shelf_io.serve].
+  Handler get handler {
+    final router = Router();
+
+    // Handlers can be added with `router.<verb>('<route>', handler)`, the
+    // '<route>' may embed URL-parameters, and these may be taken as parameters
+    // by the handler (but either all URL parameters or no URL parameters, must
+    // be taken parameters by the handler).
+    router.get('/say-hi/<name>', (Request request, String name) {
+      return Response.ok('hi $name');
+    });
+
+    // Embedded URL parameters may also be associated with a regular-expression
+    // that the pattern must match.
+    router.get('/user/<userId|[0-9]+>', (Request request, String userId) {
+      return Response.ok('User has the user-number: $userId');
+    });
+
+    // Handlers can be asynchronous (returning `FutureOr` is also allowed).
+    router.get('/wave', (Request request) async {
+      await Future.delayed(Duration(milliseconds: 100));
+      return Response.ok('_o/');
+    });
+
+    // Other routers can be mounted...
+    router.mount('/api/', Api().router);
+
+    // You can catch all verbs and use a URL-parameter with a regular expression
+    // that matches everything to catch app.
+    router.all('/<ignored|.*>', (Request request) {
+      return Response.notFound('Page not found');
+    });
+
+    return router.handler;
+  }
+}
+
+class Api {
+  Future<Response> _messages(Request request) async {
+    return Response.ok('[]');
+  }
+
+  // By exposing a [Router] for an object, it can be mounted in other routers.
+  Router get router {
+    final router = Router();
+
+    // A handler can have more that one route.
+    router.get('/messages', _messages);
+    router.get('/messages/', _messages);
+
+    // This nested catch-all, will only catch /api/.* when mounted above.
+    // Notice that ordering if annotated handlers and mounts is significant.
+    router.all('/<ignored|.*>', (Request request) => Response.notFound('null'));
+
+    return router;
+  }
+}
+
+// Run shelf server and host a [Service] instance on port 8080.
+void main() async {
+  final service = Service();
+  final server = await shelf_io.serve(service.handler, 'localhost', 8080);
+  print('Server running on localhost:${server.port}');
+}
diff --git a/pkgs/shelf_router/lib/shelf_router.dart b/pkgs/shelf_router/lib/shelf_router.dart
new file mode 100644
index 0000000..9492dda
--- /dev/null
+++ b/pkgs/shelf_router/lib/shelf_router.dart
@@ -0,0 +1,85 @@
+// Copyright 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/// A request routing library for shelf.
+///
+/// When writing a shelf web server it is often desirable to route requests to
+/// different handlers based on HTTP method and path patterns. The following
+/// example demonstrates how to do this using [Router].
+///
+/// **Example**
+/// ```dart
+/// import 'package:shelf_router/shelf_router.dart';
+/// import 'package:shelf/shelf.dart' show Request, Response;
+/// import 'package:shelf/shelf_io.dart' as io;
+///
+/// void main() async {
+///   // Create a router
+///   final router = Router();
+///
+///   // Handle GET requests with a path matching ^/say-hello/[^\]*$
+///   router.get('/say-hello/<name>', (Request request, String name) async {
+///     return Response.ok('hello $name');
+///   });
+///
+///   // Listen for requests on port localhost:8080
+///   await io.serve(router.handler, 'localhost', 8080);
+/// }
+/// ```
+///
+/// As it is often useful to organize request handlers in classes, methods can
+/// be annotated with the [Route] annotation, allowing the
+/// `shelf_router_generator` package to generated a method for creating a
+/// [Router] wrapping the class.
+///
+/// To automatically generate add the `shelf_router_generator` and
+/// `build_runner` packages to `dev_dependencies`. The follow the example
+/// below and generate code using `pub run build_runner build`.
+///
+/// **Example**, assume file name is `hello.dart`.
+/// ```dart
+/// import 'package:shelf_router/shelf_router.dart';
+/// import 'package:shelf/shelf.dart' show Request, Response;
+/// import 'package:shelf/shelf_io.dart' as io;
+///
+/// // include the generated part, assumes current file is 'hello.dart'.
+/// part 'hello.g.dart';
+///
+/// class HelloService {
+///   // Annotate a handler with the `Route` annotation.
+///   @Route.get('/say-hello/<name>')
+///   Future<Response> _sayHello(Request request, String name) async {
+///     return Response.ok('hello $name');
+///   }
+///
+///   // Use the generated function `_$<ClassName>Router(<ClassName> instance)`
+///   // to create a getter returning a `Router` for this instance of
+///   // `HelloService`
+///   Router get router => _$HelloServiceRouter(this);
+/// }
+///
+/// void main() async {
+///   // Create a `HelloService` instance
+///   final service = HelloService();
+///
+///   await io.serve(service.router.handler, 'localhost', 8080);
+/// }
+/// ```
+///
+library shelf_router;
+
+import 'src/router.dart';
+import 'src/route.dart';
+export 'src/router.dart';
+export 'src/route.dart';
diff --git a/pkgs/shelf_router/lib/src/route.dart b/pkgs/shelf_router/lib/src/route.dart
new file mode 100644
index 0000000..10a8104
--- /dev/null
+++ b/pkgs/shelf_router/lib/src/route.dart
@@ -0,0 +1,90 @@
+// Copyright 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'router.dart';
+
+/// Annotation for handler methods that requests should be routed when using
+/// package `shelf_router_generator`.
+///
+/// The `shelf_router_generator` packages makes it easy to generate a function
+/// that wraps your class and returns a [Router] that forwards requests to
+/// annotated methods. Simply add the `shelf_router_generator` and
+/// `build_runner` packages to `dev_dependencies`, write as illustrated in the
+/// following example and run `pub run build_runner build` to generate code.
+///
+/// **Example**
+/// ```dart
+/// // Always import 'shelf_router' without 'show' or 'as'.
+/// import 'package:shelf_router/shelf_router.dart';
+/// import 'package:shelf/shelf.dart' show Request, Response;
+///
+/// // Include generated code, this assumes current file is 'my_service.dart'.
+/// part 'my_service.g.dart';
+///
+/// class MyService {
+///   @Route.get('/say-hello/<name>')
+///   Future<Response> _sayHello(Request request, String name) async {
+///     return Response.ok('hello $name');
+///   }
+///
+///   /// Get a router for this service.
+///   Router get router => _$MyServiceRouter(this);
+/// }
+/// ```
+///
+/// It is also permitted to annotate public members, the only requirement is
+/// that the member has a signature accepted by [Router] as `handler`.
+class Route {
+  /// HTTP verb for requests routed to the annotated method.
+  final String verb;
+
+  /// HTTP route for request routed to the annotated method.
+  final String route;
+
+  /// Create an annotation that routes requests matching [verb] and [route] to
+  /// the annotated method.
+  const Route(this.verb, this.route);
+
+  /// Route all requests matching [route] to annotated method.
+  const Route.all(this.route) : verb = r'$all';
+
+  /// Route `GET` requests matching [route] to annotated method.
+  const Route.get(this.route) : verb = 'GET';
+
+  /// Route `HEAD` requests matching [route] to annotated method.
+  const Route.head(this.route) : verb = 'HEAD';
+
+  /// Route `POST` requests matching [route] to annotated method.
+  const Route.post(this.route) : verb = 'POST';
+
+  /// Route `PUT` requests matching [route] to annotated method.
+  const Route.put(this.route) : verb = 'PUT';
+
+  /// Route `DELETE` requests matching [route] to annotated method.
+  const Route.delete(this.route) : verb = 'DELETE';
+
+  /// Route `CONNECT` requests matching [route] to annotated method.
+  const Route.connect(this.route) : verb = 'CONNECT';
+
+  /// Route `OPTIONS` requests matching [route] to annotated method.
+  const Route.options(this.route) : verb = 'OPTIONS';
+
+  /// Route `TRACE` requests matching [route] to annotated method.
+  const Route.trace(this.route) : verb = 'TRACE';
+
+  /// Route `MOUNT` requests matching [route] to annotated method.
+  const Route.mount(String prefix)
+      : verb = r'$mount',
+        route = prefix;
+}
diff --git a/pkgs/shelf_router/lib/src/router.dart b/pkgs/shelf_router/lib/src/router.dart
new file mode 100644
index 0000000..4f054f1
--- /dev/null
+++ b/pkgs/shelf_router/lib/src/router.dart
@@ -0,0 +1,163 @@
+// Copyright 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'router_entry.dart' show RouterEntry;
+import 'package:shelf/shelf.dart';
+import 'package:http_methods/http_methods.dart';
+
+/// Get a URL parameter captured by the [Router].
+String params(Request request, String name) {
+  ArgumentError.checkNotNull(request, 'request');
+  ArgumentError.checkNotNull(name, 'name');
+
+  final p = request.context['shelf_router/params'];
+  if (!(p is Map<String, String>)) {
+    throw new Exception('no such parameter $name');
+  }
+  final value = (p as Map<String, String>)[name];
+  if (value == null) {
+    throw new Exception('no such parameter $name');
+  }
+  return value;
+}
+
+/// A shelf [Router] routes requests to handlers based on HTTP verb and route
+/// pattern.
+///
+/// ```dart
+/// import 'package:shelf_router/shelf_router.dart';
+/// import 'package:shelf/shelf.dart';
+/// import 'package:shelf/shelf_io.dart' as io;
+///
+/// var app = Router();
+///
+/// // Route pattern parameters can be specified <paramName>
+/// app.get('/users/<userName>/whoami', (Request request) async {
+///   // The matched values can be read with params(request, param)
+///   var userName = params(request, 'userName');
+///   return Response.ok('You are ${userName}');
+/// });
+///
+/// // The matched value can also be taken as parameter, if the handler given
+/// // doesn't implement Handler, it's assumed to take all parameters in the
+/// // order they appear in the route pattern.
+/// app.get('/users/<userName>/say-hello', (Request request, String userName) async {
+///   return Response.ok('Hello ${uName}');
+/// });
+///
+/// // It is possible to have multiple parameters, and if desired a custom
+/// // regular expression can be specified with <paramName|REGEXP>, where
+/// // REGEXP is a regular expression (leaving out ^ and $).
+/// // If no regular expression is specified `[^/]+` will be used.
+/// app.get('/users/<userName>/messages/<msgId|\d+>', (Request request) async {
+///   var msgId = int.parse(params(request, 'msgId'));
+///   return Response.ok(message.getById(msgId));
+/// });
+///
+/// var server = await io.serve(app.handler, 'localhost', 8080);
+/// ```
+///
+/// If multiple routes match the same request, the handler for the first
+/// route is called. If the handler returns `null` the next matching handler
+/// will be attempted.
+///
+///
+class Router {
+  final List<RouterEntry> _routes = [];
+
+  /// Add [handler] for [verb] requests to [route].
+  void add(String verb, String route, dynamic handler) {
+    ArgumentError.checkNotNull(verb, 'verb');
+    if (!isHttpMethod(verb)) {
+      throw ArgumentError.value(verb, 'verb', 'expected a valid HTTP method');
+    }
+    verb = verb.toUpperCase();
+
+    _routes.add(RouterEntry(verb, route, handler));
+  }
+
+  /// Handle all request to [route] using [handler].
+  void all(String route, dynamic handler) {
+    _routes.add(RouterEntry('ALL', route, handler));
+  }
+
+  /// Mount a router below a prefix.
+  ///
+  /// In this case prefix may not contain any parameters, nor
+  void mount(String prefix, Router router) {
+    ArgumentError.checkNotNull(prefix, 'prefix');
+    ArgumentError.checkNotNull(router, 'router');
+    if (!prefix.startsWith('/') || !prefix.endsWith('/')) {
+      throw ArgumentError.value(
+          prefix, 'prefix', 'must start and end with a slash');
+    }
+
+    final handler = router.handler;
+    // first slash is always in request.handlerPath
+    final path = prefix.substring(1);
+    all(prefix + '<path|[^]*>', (Request request) {
+      return handler(request.change(path: path));
+    });
+  }
+
+  /// Get a [Handler] that will route incoming requests to registered handlers.
+  Handler get handler {
+    // Note: this is a great place to optimize the implementation by building
+    //       a trie for faster matching... left as an exercise for the reader :)
+    return (Request request) async {
+      for (var route in _routes) {
+        if (route.verb != request.method.toUpperCase() && route.verb != 'ALL') {
+          continue;
+        }
+        var params = route.match('/' + request.url.path);
+        if (params != null) {
+          var res = await route.invoke(request, params);
+          if (res != null) {
+            return res;
+          }
+        }
+      }
+      return null;
+    };
+  }
+
+  // Handlers for all methods
+
+  /// Handle `GET` request to [route] using [handler].
+  void get(String route, dynamic handler) => add('GET', route, handler);
+
+  /// Handle `HEAD` request to [route] using [handler].
+  void head(String route, dynamic handler) => add('HEAD', route, handler);
+
+  /// Handle `POST` request to [route] using [handler].
+  void post(String route, dynamic handler) => add('POST', route, handler);
+
+  /// Handle `PUT` request to [route] using [handler].
+  void put(String route, dynamic handler) => add('PUT', route, handler);
+
+  /// Handle `DELETE` request to [route] using [handler].
+  void delete(String route, dynamic handler) => add('DELETE', route, handler);
+
+  /// Handle `CONNECT` request to [route] using [handler].
+  void connect(String route, dynamic handler) => add('CONNECT', route, handler);
+
+  /// Handle `OPTIONS` request to [route] using [handler].
+  void options(String route, dynamic handler) => add('OPTIONS', route, handler);
+
+  /// Handle `TRACE` request to [route] using [handler].
+  void trace(String route, dynamic handler) => add('TRACE', route, handler);
+
+  /// Handle `PATCH` request to [route] using [handler].
+  void patch(String route, dynamic handler) => add('PATCH', route, handler);
+}
diff --git a/pkgs/shelf_router/lib/src/router_entry.dart b/pkgs/shelf_router/lib/src/router_entry.dart
new file mode 100644
index 0000000..0225b6d
--- /dev/null
+++ b/pkgs/shelf_router/lib/src/router_entry.dart
@@ -0,0 +1,101 @@
+// Copyright 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'dart:async';
+import 'package:shelf/shelf.dart';
+
+/// Check if the [regexp] is non-capturing.
+bool _isNoCapture(String regexp) {
+  ArgumentError.checkNotNull(regexp, 'regexp');
+
+  // Construct a new regular expression matching anything containing regexp,
+  // then match with empty-string and count number of groups.
+  return RegExp('^(?:$regexp)|.*\$').firstMatch('').groupCount == 0;
+}
+
+/// Entry in the router.
+///
+/// This class implements the logic for matching the path pattern.
+class RouterEntry {
+  /// Pattern for parsing the route pattern
+  static final RegExp _parser = RegExp(r'([^<]*)(?:<([^>|]+)(?:\|([^>]*))?>)?');
+
+  final String verb, route;
+  final dynamic _handler;
+
+  /// Expression that the request path must match.
+  ///
+  /// This also captures any parameters in the route pattern.
+  RegExp _routePattern;
+
+  /// Names for the parameters in the route pattern.
+  List<String> _params = [];
+
+  /// List of parameter names in the route pattern.
+  List<String> get params => _params.toList(); // exposed for using generator.
+
+  RouterEntry(this.verb, this.route, this._handler) {
+    ArgumentError.checkNotNull(verb, 'verb');
+    ArgumentError.checkNotNull(route, 'route');
+    ArgumentError.checkNotNull(_handler, 'handler');
+    if (!route.startsWith('/')) {
+      throw ArgumentError.value(
+          route, 'route', 'expected route to start with a slash');
+    }
+    if (!(_handler is Function)) {
+      throw ArgumentError.value(_handler, 'handler', 'expected a function');
+    }
+
+    var pattern = '';
+    for (var m in _parser.allMatches(route)) {
+      pattern += RegExp.escape(m[1]);
+      if (m[2] != null) {
+        _params.add(m[2]);
+        if (m[3] != null && !_isNoCapture(m[3])) {
+          throw ArgumentError.value(
+              route, 'route', 'expression for "${m[2]}" is capturing');
+        }
+        pattern += '(${m[3] ?? r'[^/]+'})';
+      }
+    }
+    _routePattern = RegExp('^$pattern\$');
+  }
+
+  /// Returns a map from parameter name to value, if the path matches the
+  /// route pattern. Otherwise returns null.
+  Map<String, String> match(String path) {
+    // Check if path matches the route pattern
+    var m = _routePattern.firstMatch(path);
+    if (m == null) {
+      return null;
+    }
+    // Construct map from parameter name to matched value
+    var params = <String, String>{};
+    for (int i = 0; i < _params.length; i++) {
+      // first group is always the full match, we ignore this group.
+      params[_params[i]] = m[i + 1];
+    }
+    return params;
+  }
+
+  // invoke handler with given request and params
+  Future<Response> invoke(Request request, Map<String, String> params) async {
+    request = request.change(context: {'shelf_router/params': params});
+    if (_handler is Handler || _params.isEmpty) {
+      return await _handler(request);
+    }
+    return await Function.apply(
+        _handler, [request]..addAll(_params.map((n) => params[n])));
+  }
+}
diff --git a/pkgs/shelf_router/mono_pkg.yaml b/pkgs/shelf_router/mono_pkg.yaml
new file mode 100644
index 0000000..1b2bf49
--- /dev/null
+++ b/pkgs/shelf_router/mono_pkg.yaml
@@ -0,0 +1,8 @@
+dart:
+- stable
+stages:
+- analyze:
+  - dartanalyzer
+  - dartfmt
+- unit_test:
+  - test
diff --git a/pkgs/shelf_router/pubspec.yaml b/pkgs/shelf_router/pubspec.yaml
new file mode 100644
index 0000000..f866f32
--- /dev/null
+++ b/pkgs/shelf_router/pubspec.yaml
@@ -0,0 +1,21 @@
+name: shelf_router
+version: 0.7.0
+description: A router for the shelf package.
+description: |
+  A convinent request router for the shelf web-framework, with support for
+  URL-parameters, nested routers and routers generated from source annotations
+  using the shelf_router_generator package.
+homepage: https://github.com/google/dart-neats/tree/master/shelf_router
+repository: https://github.com/google/dart-neats.git
+issue_tracker: https://github.com/google/dart-neats/labels/pkg:shelf_router
+dependencies:
+  shelf: ^0.7.3
+  http_methods: ^1.0.0
+dev_dependencies:
+  test: ^1.5.1
+  pedantic: ^1.4.0
+dependency_overrides:
+  http_methods:
+    path: ../http_methods
+environment:
+  sdk: '>=2.0.0 <3.0.0'
\ No newline at end of file
diff --git a/pkgs/shelf_router/test/route_entry_test.dart b/pkgs/shelf_router/test/route_entry_test.dart
new file mode 100644
index 0000000..ca21ef3
--- /dev/null
+++ b/pkgs/shelf_router/test/route_entry_test.dart
@@ -0,0 +1,73 @@
+// Copyright 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'package:shelf_router/src/router_entry.dart' show RouterEntry;
+import 'package:test/test.dart';
+
+void main() {
+  void testPattern(
+    String pattern, {
+    Map<String, Map<String, String>> match = const {},
+    List<String> notMatch = const [],
+  }) {
+    group('RouterEntry: "$pattern"', () {
+      final r = RouterEntry('GET', pattern, () => null);
+      for (final e in match.entries) {
+        test('Matches "${e.key}"', () {
+          expect(r.match(e.key), equals(e.value));
+        });
+      }
+      for (final v in notMatch) {
+        test('NotMatch "$v"', () {
+          expect(r.match(v), isNull);
+        });
+      }
+    });
+  }
+
+  testPattern('/hello', match: {
+    '/hello': {},
+  }, notMatch: [
+    '/not-hello',
+    '/',
+  ]);
+
+  testPattern(r'/user/<user>/groups/<group|\d+>', match: {
+    '/user/jonasfj/groups/42': {
+      'user': 'jonasfj',
+      'group': '42',
+    },
+    '/user/jonasfj/groups/0': {
+      'user': 'jonasfj',
+      'group': '0',
+    },
+    '/user/123/groups/101': {
+      'user': '123',
+      'group': '101',
+    },
+  }, notMatch: [
+    '/user/',
+    '/user/jonasfj/groups/5-3',
+    '/user/jonasfj/test/groups/5',
+    '/user/jonasfjtest/groups/4/',
+    '/user/jonasfj/groups/',
+    '/not-hello',
+    '/',
+  ]);
+
+  test('non-capture regex only', () {
+    expect(() => RouterEntry('GET', '/users/<user|([^]*)>/info', null),
+        throwsA(anything));
+  });
+}
diff --git a/pkgs/shelf_router/test/router_test.dart b/pkgs/shelf_router/test/router_test.dart
new file mode 100644
index 0000000..6c09d5c
--- /dev/null
+++ b/pkgs/shelf_router/test/router_test.dart
@@ -0,0 +1,118 @@
+// Copyright 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+@TestOn('vm')
+
+import 'dart:async';
+import 'dart:io';
+
+import 'package:http/http.dart' as http;
+import 'package:shelf/shelf.dart';
+import 'package:shelf/shelf_io.dart' as io;
+import 'package:test/test.dart';
+
+import 'package:shelf_router/shelf_router.dart';
+
+void main() {
+  // Create a server that listens on localhost for testing
+  io.IOServer server;
+
+  setUp(() async {
+    try {
+      server = await io.IOServer.bind(InternetAddress.loopbackIPv6, 0);
+    } on SocketException catch (_) {
+      server = await io.IOServer.bind(InternetAddress.loopbackIPv4, 0);
+    }
+  });
+
+  tearDown(() => server.close());
+
+  Future<String> get(String path) => http.read(server.url.toString() + path);
+
+  test('get sync/async handler', () async {
+    var app = Router();
+
+    app.get('/sync-hello', (Request request) {
+      return Response.ok('hello-world');
+    });
+
+    app.get('/async-hello', (Request request) async {
+      return Future.microtask(() {
+        return Response.ok('hello-world');
+      });
+    });
+
+    // check that catch-alls work
+    app.all('/<path|[^]*>', (Request request) {
+      return Response.ok('not-found');
+    });
+
+    server.mount(app.handler);
+
+    expect(await get('/sync-hello'), 'hello-world');
+    expect(await get('/async-hello'), 'hello-world');
+    expect(await get('/wrong-path'), 'not-found');
+  });
+
+  test('params', () async {
+    var app = Router();
+
+    app.get(r'/user/<user>/groups/<group|\d+>', (Request request) {
+      final user = params(request, 'user');
+      final group = params(request, 'group');
+      return Response.ok('$user / $group');
+    });
+
+    server.mount(app.handler);
+
+    expect(await get('/user/jonasfj/groups/42'), 'jonasfj / 42');
+  });
+
+  test('params by arguments', () async {
+    var app = Router();
+
+    app.get(r'/user/<user>/groups/<group|\d+>',
+        (Request request, String user, String group) {
+      return Response.ok('$user / $group');
+    });
+
+    server.mount(app.handler);
+
+    expect(await get('/user/jonasfj/groups/42'), 'jonasfj / 42');
+  });
+
+  test('mount(Router)', () async {
+    var api = Router();
+    api.get('/user/<user>/info', (Request request, String user) {
+      return Response.ok('Hello $user');
+    });
+
+    var app = Router();
+    app.get('/hello', (Request request) {
+      return Response.ok('hello-world');
+    });
+
+    app.mount('/api/', api);
+
+    app.all('/<_|[^]*>', (Request request) {
+      return Response.notFound('catch-all-handler');
+    });
+
+    server.mount(app.handler);
+
+    expect(await get('/hello'), 'hello-world');
+    expect(await get('/api/user/jonasfj/info'), 'Hello jonasfj');
+    expect(get('/api/user/jonasfj/info-wrong'), throwsA(anything));
+  });
+}
diff --git a/pkgs/shelf_router_generator/CHANGELOG.md b/pkgs/shelf_router_generator/CHANGELOG.md
new file mode 100644
index 0000000..ed39b04
--- /dev/null
+++ b/pkgs/shelf_router_generator/CHANGELOG.md
@@ -0,0 +1,5 @@
+# Changelog
+
+## 0.7.0
+
+ * Initial release
diff --git a/pkgs/shelf_router_generator/LICENSE b/pkgs/shelf_router_generator/LICENSE
new file mode 100644
index 0000000..7a4a3ea
--- /dev/null
+++ b/pkgs/shelf_router_generator/LICENSE
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
\ No newline at end of file
diff --git a/pkgs/shelf_router_generator/README.md b/pkgs/shelf_router_generator/README.md
new file mode 100644
index 0000000..00fa3a1
--- /dev/null
+++ b/pkgs/shelf_router_generator/README.md
@@ -0,0 +1,54 @@
+Shelf Router Generator
+======================
+
+[Shelf](https://pub.dartlang.org/packages/shelf) makes it easy to build web
+applications in Dart by composing request handlers. The `shelf_router` package
+offers a request router for Shelf. this package enables generating a
+`shelf_route.Router` from annotations in code.
+
+**Disclaimer:** This is not an officially supported Google product.
+
+## Example
+
+```dart
+import 'package:shelf/shelf.dart';
+import 'package:shelf_router/shelf_router.dart';
+
+part 'userservice.g.dart';
+
+class UserService {
+  final DatabaseConnection connection;
+  UserService(this.connection);
+
+  @Route.get('/users/')
+  Future<Response> listUsers(Request request) async {
+    return Response.ok('["user1"]');
+  }
+
+  @Route.get('/users/<userId>')
+  Future<Response> fetchUser(Request request, String userId) async {
+    if (userId == 'user1') {
+      return Response.ok('user1');
+    }
+    return Response.notFound('no such user');
+  }
+
+  // Create router using the generate function defined in 'userservice.g.dart'.
+  Router get router => _$UserServiceRouter(this);
+}
+
+void main() async {
+  // You can setup context, database connections, cache connections, email
+  // services, before you create an instance of your service.
+  var connection = await DatabaseConnection.connect('localhost:1234');
+
+  // Create an instance of your service, usine one of the constructors you've
+  // defined.
+  var service = UserService(connection);
+  // Service request using the router, note the router can also be mounted.
+  var router = service.router;
+  var server = await io.serve(router.handler, 'localhost', 8080);
+}
+```
+
+
diff --git a/pkgs/shelf_router_generator/analysis_options.yaml b/pkgs/shelf_router_generator/analysis_options.yaml
new file mode 100644
index 0000000..108d105
--- /dev/null
+++ b/pkgs/shelf_router_generator/analysis_options.yaml
@@ -0,0 +1 @@
+include: package:pedantic/analysis_options.yaml
diff --git a/pkgs/shelf_router_generator/build.yaml b/pkgs/shelf_router_generator/build.yaml
new file mode 100644
index 0000000..d6561c0
--- /dev/null
+++ b/pkgs/shelf_router_generator/build.yaml
@@ -0,0 +1,14 @@
+targets:
+  $default:
+    builders:
+      shelf_router_generator|shelf_router:
+        enabled: true
+
+builders:
+  shelf_router:
+    import: "package:shelf_router_generator/builder.dart"
+    builder_factories: ["shelfRouter"]
+    build_extensions: {".dart": [".shelf_router.g.part"]}
+    auto_apply: dependents
+    build_to: cache
+    applies_builders: ["source_gen|combining_builder"]
diff --git a/pkgs/shelf_router_generator/example/main.dart b/pkgs/shelf_router_generator/example/main.dart
new file mode 100644
index 0000000..837557b
--- /dev/null
+++ b/pkgs/shelf_router_generator/example/main.dart
@@ -0,0 +1,84 @@
+// Copyright 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'dart:async' show Future;
+import 'package:shelf_router/shelf_router.dart';
+import 'package:shelf/shelf.dart';
+import 'package:shelf/shelf_io.dart' as shelf_io;
+
+// Generated code will be written to 'main.g.dart'
+part 'main.g.dart';
+
+class Service {
+  // A handler is annotated with @Route.<verb>('<route>'), the '<route>' may
+  // embed URL-parameters, and these may be taken as parameters by the handler.
+  // But either all URL-parameters or none of the URL parameters must be taken
+  // as parameters by the handler.
+  @Route.get('/say-hi/<name>')
+  Response _hi(Request request, String name) {
+    return Response.ok('hi $name');
+  }
+
+  // Embedded URL parameters may also be associated with a regular-expression
+  // that the pattern must match.
+  @Route.get('/user/<userId|[0-9]+>')
+  Response _user(Request request, String userId) {
+    return Response.ok('User has the user-number: $userId');
+  }
+
+  // Handlers can be asynchronous (returning `FutureOr` is also allowed).
+  @Route.get('/wave')
+  Future<Response> _wave(Request request) async {
+    await Future.delayed(Duration(milliseconds: 100));
+    return Response.ok('_o/');
+  }
+
+  // Other routers can be mounted...
+  @Route.mount('/api/')
+  Router get _api => Api().router;
+
+  // You can catch all verbs and use a URL-parameter with a regular expression
+  // that matches everything to catch app.
+  @Route.all('/<ignored|.*>')
+  Response _404(Request request) => Response.notFound('Page not found');
+
+  // The generated function _$ServiceRouter can be used to get a [Handler]
+  // for this object. This can be used with [shelf_io.serve].
+  Handler get handler => _$ServiceRouter(this).handler;
+}
+
+class Api {
+  // A handler can have more that one route :)
+  @Route.get('/messages')
+  @Route.get('/messages/')
+  Future<Response> _messages(Request request) async {
+    return Response.ok('[]');
+  }
+
+  // This nested catch-all, will only catch /api/.* when mounted above.
+  // Notice that ordering if annotated handlers and mounts is significant.
+  @Route.all('/<ignored|.*>')
+  Response _404(Request request) => Response.notFound('null');
+
+  // The generated function _$ApiRouter can be used to expose a [Router] for
+  // this object.
+  Router get router => _$ApiRouter(this);
+}
+
+// Run shelf server and host a [Service] instance on port 8080.
+void main() async {
+  final service = Service();
+  final server = await shelf_io.serve(service.handler, 'localhost', 8080);
+  print('Server running on localhost:${server.port}');
+}
diff --git a/pkgs/shelf_router_generator/example/main.g.dart b/pkgs/shelf_router_generator/example/main.g.dart
new file mode 100644
index 0000000..8d7a50b
--- /dev/null
+++ b/pkgs/shelf_router_generator/example/main.g.dart
@@ -0,0 +1,25 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'main.dart';
+
+// **************************************************************************
+// ShelfRouterGenerator
+// **************************************************************************
+
+Router _$ServiceRouter(Service service) {
+  final router = Router();
+  router.add('GET', '/say-hi/<name>', service._hi);
+  router.add('GET', '/user/<userId|[0-9]+>', service._user);
+  router.add('GET', '/wave', service._wave);
+  router.mount('/api/', service._api);
+  router.all('/<ignored|.*>', service._404);
+  return router;
+}
+
+Router _$ApiRouter(Api service) {
+  final router = Router();
+  router.add('GET', '/messages', service._messages);
+  router.add('GET', '/messages/', service._messages);
+  router.all('/<ignored|.*>', service._404);
+  return router;
+}
diff --git a/pkgs/shelf_router_generator/lib/builder.dart b/pkgs/shelf_router_generator/lib/builder.dart
new file mode 100644
index 0000000..d61303b
--- /dev/null
+++ b/pkgs/shelf_router_generator/lib/builder.dart
@@ -0,0 +1,34 @@
+// Copyright 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/// This library provides a [Builder] for generating functions that can create
+/// a [shelf_router.Router] based on annotated members.
+///
+/// This is **not intended** for consumption, this library should be used by
+/// running `pub run build_runner build`. Using this library through other means
+/// is not supported and may break arbitrarily.
+library builder;
+
+import 'package:build/build.dart';
+import 'package:source_gen/source_gen.dart';
+import 'package:shelf_router/shelf_router.dart' as shelf_router;
+import 'src/shelf_router_generator.dart';
+
+/// A [Builder] that generates a `_$<className>Router(<className> service)`
+/// function for each class `<className>` containing a member annotated with
+/// [shelf_router.Route].
+Builder shelfRouter(BuilderOptions _) => SharedPartBuilder(
+      [ShelfRouterGenerator()],
+      'shelf_router',
+    );
diff --git a/pkgs/shelf_router_generator/lib/src/shelf_router_generator.dart b/pkgs/shelf_router_generator/lib/src/shelf_router_generator.dart
new file mode 100644
index 0000000..c26af53
--- /dev/null
+++ b/pkgs/shelf_router_generator/lib/src/shelf_router_generator.dart
@@ -0,0 +1,301 @@
+// Copyright 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'dart:async' show Future;
+import 'package:analyzer/dart/element/element.dart'
+    show ClassElement, ElementKind, ExecutableElement;
+import 'package:analyzer/dart/element/type.dart' show ParameterizedType;
+import 'package:build/build.dart' show BuildStep, log;
+import 'package:code_builder/code_builder.dart' as code;
+import 'package:http_methods/http_methods.dart' show isHttpMethod;
+import 'package:meta/meta.dart';
+import 'package:source_gen/source_gen.dart' as g;
+import 'package:shelf/shelf.dart' as shelf;
+import 'package:shelf_router/shelf_router.dart' as shelf_router;
+import 'package:shelf_router/src/router_entry.dart' show RouterEntry;
+
+// Type checkers that we need later
+final _routeType = g.TypeChecker.fromRuntime(shelf_router.Route);
+final _routerType = g.TypeChecker.fromRuntime(shelf_router.Router);
+final _responseType = g.TypeChecker.fromRuntime(shelf.Response);
+final _requestType = g.TypeChecker.fromRuntime(shelf.Request);
+final _stringType = g.TypeChecker.fromRuntime(String);
+
+/// A representation of a handler that was annotated with [Route].
+class _Handler {
+  final String verb, route;
+  final ExecutableElement element;
+
+  _Handler(this.verb, this.route, this.element);
+}
+
+/// Find members of a class annotated with [shelf_router.Route].
+List<ExecutableElement> getAnnotatedElementsOrderBySourceOffset(
+    ClassElement cls) {
+  return <ExecutableElement>[]
+    ..addAll(cls.methods.where(_routeType.hasAnnotationOfExact))
+    ..addAll(cls.accessors.where(_routeType.hasAnnotationOfExact))
+    ..sort((a, b) => (a.nameOffset ?? -1).compareTo(b.nameOffset ?? -1));
+}
+
+/// Generate a `_$<className>Router(<className> service)` method that returns a
+/// [shelf_router.Router] configured based on annotated handlers.
+code.Method _buildRouterMethod({
+  @required ClassElement classElement,
+  @required List<_Handler> handlers,
+}) =>
+    code.Method(
+      (b) => b
+        ..name = '_\$${classElement.name}Router'
+        ..requiredParameters.add(
+          code.Parameter((b) => b
+            ..name = 'service'
+            ..type = code.refer(classElement.name)),
+        )
+        ..returns = code.refer('Router')
+        ..body = code.Block(
+          (b) => b
+            ..addExpression(
+                code.refer('Router').newInstance([]).assignFinal('router'))
+            ..statements.addAll(handlers.map((h) => _buildAddHandlerCode(
+                  router: code.refer('router'),
+                  service: code.refer('service'),
+                  handler: h,
+                )))
+            ..addExpression(code.refer('router').returned),
+        ),
+    );
+
+/// Generate the code statement that adds [handler] from [service] to [router].
+code.Code _buildAddHandlerCode({
+  @required code.Reference router,
+  @required code.Reference service,
+  @required _Handler handler,
+}) {
+  switch (handler.verb) {
+    case r'$mount':
+      return router.property('mount').call([
+        code.literal(handler.route),
+        service.property(handler.element.name),
+      ]).statement;
+    case r'$all':
+      return router.property('all').call([
+        code.literal(handler.route),
+        service.property(handler.element.name),
+      ]).statement;
+    default:
+      return router.property('add').call([
+        code.literal(handler.verb.toUpperCase()),
+        code.literal(handler.route),
+        service.property(handler.element.name),
+      ]).statement;
+  }
+}
+
+class ShelfRouterGenerator extends g.Generator {
+  @override
+  Future<String> generate(g.LibraryReader library, BuildStep step) async {
+    // Create a map from ClassElement to list of annotated elements sorted by
+    // offset in source code, this is not type checked yet.
+    final classes = <ClassElement, List<_Handler>>{};
+    for (final cls in library.classes) {
+      final elements = getAnnotatedElementsOrderBySourceOffset(cls);
+      if (elements.isEmpty) {
+        continue;
+      }
+      log.info('found shelf_router.Route annotations in ${cls.name}');
+
+      classes[cls] = elements
+          .map((e) => _routeType.annotationsOfExact(e).map((a) => _Handler(
+                a.getField('verb').toStringValue(),
+                a.getField('route').toStringValue(),
+                e,
+              )))
+          .expand((i) => i)
+          .toList();
+    }
+    if (classes.isEmpty) {
+      return null; // nothing to do if nothing was annotated
+    }
+
+    // Run type check to ensure method and getters have the right signatures.
+    for (final handler in classes.values.expand((i) => i)) {
+      // If the verb is $mount, then it's not a handler, but a mount.
+      if (handler.verb.toLowerCase() == r'$mount') {
+        _typeCheckMount(handler);
+      } else {
+        _typeCheckHandler(handler);
+      }
+    }
+
+    // Build library and emit code with all generate methods.
+    final methods = classes.entries.map((e) => _buildRouterMethod(
+          classElement: e.key,
+          handlers: e.value,
+        ));
+    return code.Library((b) => b.body.addAll(methods))
+        .accept(code.DartEmitter())
+        .toString();
+  }
+}
+
+/// Type checks for the case where [shelf_router.Route] is used to annotate
+/// shelf request handler.
+void _typeCheckHandler(_Handler h) {
+  if (h.element.isStatic) {
+    throw g.InvalidGenerationSourceError(
+        'The shelf_router.Route annotation cannot be used on static members',
+        element: h.element);
+  }
+
+  // Check the verb, note that $all is a special value for handling all verbs.
+  if (!isHttpMethod(h.verb) && h.verb != r'$all') {
+    throw g.InvalidGenerationSourceError(
+        'The verb "${h.verb}" used in shelf_router.Route annotation must be '
+        'a valid HTTP method',
+        element: h.element);
+  }
+
+  // Check that this shouldn't have been annotated with Route.mount
+  if (h.element.kind == ElementKind.GETTER) {
+    throw g.InvalidGenerationSourceError(
+        'Only the shelf_router.Route.mount annotation can only be used on a '
+        'getter, and only if it returns a shelf_router.Router',
+        element: h.element);
+  }
+
+  // Check that this is indeed a method
+  if (h.element.kind != ElementKind.METHOD) {
+    throw g.InvalidGenerationSourceError(
+        'The shelf_router.Route annotation can only be used on request '
+        'handling methods',
+        element: h.element);
+  }
+
+  // Check the route can parse
+  List<String> params;
+  try {
+    params = RouterEntry(h.verb, h.route, () => null).params;
+  } on ArgumentError catch (e) {
+    throw g.InvalidGenerationSourceError(
+      e.toString(),
+      element: h.element,
+    );
+  }
+
+  // Ensure that the first parameter is shelf.Request
+  if (h.element.parameters.isEmpty) {
+    throw g.InvalidGenerationSourceError(
+        'The shelf_router.Route annotation can only be used on shelf request '
+        'handlers accept a shelf.Request parameter',
+        element: h.element);
+  }
+  for (final p in h.element.parameters) {
+    if (p.isOptional) {
+      throw g.InvalidGenerationSourceError(
+          'The shelf_router.Route annotation can only be used on shelf '
+          'request handlers accept a shelf.Request parameter and/or a '
+          'shelf.Request parameter and all string parameters in the route, '
+          'optional parameters are not permitted',
+          element: p);
+    }
+  }
+  if (!_requestType.isExactlyType(h.element.parameters.first.type)) {
+    throw g.InvalidGenerationSourceError(
+        'The shelf_router.Route annotation can only be used on shelf request '
+        'handlers accept a shelf.Request parameter as first parameter',
+        element: h.element);
+  }
+  if (h.element.parameters.length > 1) {
+    if (h.element.parameters.length != params.length + 1) {
+      throw g.InvalidGenerationSourceError(
+          'The shelf_router.Route annotation can only be used on shelf '
+          'request handlers accept a shelf.Request parameter and/or a '
+          'shelf.Request parameter and all string parameters in the route',
+          element: h.element);
+    }
+    for (int i = 0; i < params.length; i++) {
+      final p = h.element.parameters[i + 1];
+      if (p.name != params[i]) {
+        throw g.InvalidGenerationSourceError(
+            'The shelf_router.Route annotation can only be used on shelf '
+            'request handlers accept a shelf.Request parameter and/or a '
+            'shelf.Request parameter and all string parameters in the route, '
+            'the "${p.name}" parameter should be named "${params[i]}"',
+            element: p);
+      }
+      if (!_stringType.isExactlyType(p.type)) {
+        throw g.InvalidGenerationSourceError(
+            'The shelf_router.Route annotation can only be used on shelf '
+            'request handlers accept a shelf.Request parameter and/or a '
+            'shelf.Request parameter and all string parameters in the route, '
+            'the "${p.name}" parameter is not of type string',
+            element: p);
+      }
+    }
+  }
+
+  // Check the return value of the method.
+  var returnType = h.element.returnType;
+  // Unpack Future<T> and FutureOr<T> wrapping of responseType
+  if (returnType.isDartAsyncFuture || returnType.isDartAsyncFutureOr) {
+    returnType = (returnType as ParameterizedType).typeArguments.first;
+  }
+  if (!_responseType.isAssignableFromType(returnType)) {
+    throw g.InvalidGenerationSourceError(
+        'The shelf_router.Route annotation can only be used on shelf request '
+        'handlers that return shelf.Response, Future<shelf.Response> or '
+        'FutureOr<shelf.Response>, and not "${h.element.returnType}"',
+        element: h.element);
+  }
+}
+
+/// Type checks for the case where [shelf_router.Route.mount] is used to annotate
+/// a getter that returns a [shelf_router.Router].
+void _typeCheckMount(_Handler h) {
+  if (h.element.isStatic) {
+    throw g.InvalidGenerationSourceError(
+        'The shelf_router.Route annotation cannot be used on static members',
+        element: h.element);
+  }
+
+  // Check that this should have been annotated with Route.mount
+  if (h.element.kind != ElementKind.GETTER) {
+    throw g.InvalidGenerationSourceError(
+        'The shelf_router.Route.mount annotation can only be used on a '
+        'getter that returns shelf_router.Router',
+        element: h.element);
+  }
+
+  // Sanity checks for the prefix
+  if (!h.route.startsWith('/') || !h.route.endsWith('/')) {
+    throw g.InvalidGenerationSourceError(
+        'The prefix "${h.route}" in shelf_router.Route.mount(prefix) '
+        'annotation must begin and end with a slash',
+        element: h.element);
+  }
+  if (h.route.contains('<')) {
+    throw g.InvalidGenerationSourceError(
+        'The prefix "${h.route}" in shelf_router.Route.mount(prefix) '
+        'annotation cannot contain <',
+        element: h.element);
+  }
+
+  if (!_routerType.isAssignableFromType(h.element.returnType)) {
+    throw g.InvalidGenerationSourceError(
+        'The shelf_router.Route.mount annotation can only be used on a '
+        'getter that returns shelf_router.Router',
+        element: h.element);
+  }
+}
diff --git a/pkgs/shelf_router_generator/mono_pkg.yaml b/pkgs/shelf_router_generator/mono_pkg.yaml
new file mode 100644
index 0000000..1b2bf49
--- /dev/null
+++ b/pkgs/shelf_router_generator/mono_pkg.yaml
@@ -0,0 +1,8 @@
+dart:
+- stable
+stages:
+- analyze:
+  - dartanalyzer
+  - dartfmt
+- unit_test:
+  - test
diff --git a/pkgs/shelf_router_generator/pubspec.yaml b/pkgs/shelf_router_generator/pubspec.yaml
new file mode 100644
index 0000000..445b15b
--- /dev/null
+++ b/pkgs/shelf_router_generator/pubspec.yaml
@@ -0,0 +1,28 @@
+name: shelf_router_generator
+version: 0.7.0
+description: |
+  A package:build compatible builder for generating request routers for the
+  shelf web-framework based on source annotations.
+homepage: https://github.com/google/dart-neats/tree/master/shelf_router_generator
+repository: https://github.com/google/dart-neats.git
+issue_tracker: https://github.com/google/dart-neats/labels/pkg:shelf_router_generator
+dependencies:
+  build: ^1.0.0
+  build_config: ^0.3.1
+  source_gen: ^0.9.1
+  analyzer:  ^0.35.0
+  shelf_router: ^1.0.0
+  code_builder: ^3.2.0
+  shelf: ^0.7.3
+  meta: ^1.1.7
+dependency_overrides:
+  shelf_router:
+    path: ../shelf_router
+  http_methods:
+    path: ../http_methods
+dev_dependencies:
+  test: ^1.5.3
+  http: ^0.12.0+1
+  build_verify: ^1.1.1
+  build_runner: ^1.2.8
+  pedantic: ^1.4.0
\ No newline at end of file
diff --git a/pkgs/shelf_router_generator/test/ensure_build_test.dart b/pkgs/shelf_router_generator/test/ensure_build_test.dart
new file mode 100644
index 0000000..cad26fe
--- /dev/null
+++ b/pkgs/shelf_router_generator/test/ensure_build_test.dart
@@ -0,0 +1,22 @@
+// Copyright 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'package:build_verify/build_verify.dart';
+import 'package:test/test.dart';
+
+void main() {
+  test('ensure_build', () {
+    expectBuildClean(packageRelativeDirectory: 'shelf_router_generator');
+  });
+}
diff --git a/pkgs/shelf_router_generator/test/server/api.dart b/pkgs/shelf_router_generator/test/server/api.dart
new file mode 100644
index 0000000..a8247f6
--- /dev/null
+++ b/pkgs/shelf_router_generator/test/server/api.dart
@@ -0,0 +1,33 @@
+// Copyright 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'dart:async' show Future;
+import 'package:shelf/shelf.dart';
+import 'package:shelf_router/shelf_router.dart';
+
+part 'api.g.dart';
+
+class Api {
+  @Route.get('/time')
+  Response _time(Request request) {
+    return Response.ok('it is about now');
+  }
+
+  @Route.get('/to-uppercase/<word|.*>')
+  Future<Response> _toUpperCase(Request request, String word) async {
+    return Response.ok(word.toUpperCase());
+  }
+
+  Router get router => _$ApiRouter(this);
+}
diff --git a/pkgs/shelf_router_generator/test/server/api.g.dart b/pkgs/shelf_router_generator/test/server/api.g.dart
new file mode 100644
index 0000000..9c617f4
--- /dev/null
+++ b/pkgs/shelf_router_generator/test/server/api.g.dart
@@ -0,0 +1,14 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'api.dart';
+
+// **************************************************************************
+// ShelfRouterGenerator
+// **************************************************************************
+
+Router _$ApiRouter(Api service) {
+  final router = Router();
+  router.add('GET', '/time', service._time);
+  router.add('GET', '/to-uppercase/<word|.*>', service._toUpperCase);
+  return router;
+}
diff --git a/pkgs/shelf_router_generator/test/server/server.dart b/pkgs/shelf_router_generator/test/server/server.dart
new file mode 100644
index 0000000..19b68b3
--- /dev/null
+++ b/pkgs/shelf_router_generator/test/server/server.dart
@@ -0,0 +1,37 @@
+// Copyright 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'dart:async' show Future;
+import 'dart:io' show HttpServer;
+import 'package:shelf/shelf_io.dart' as shelf_io;
+import 'service.dart';
+
+class Server {
+  final _service = Service();
+  HttpServer _server;
+
+  Future<void> start() async {
+    _server = await shelf_io.serve(_service.router.handler, 'localhost', 0);
+  }
+
+  Future<void> stop() {
+    return _server.close();
+  }
+
+  Uri get uri => Uri(
+        scheme: 'http',
+        host: 'localhost',
+        port: _server.port,
+      );
+}
diff --git a/pkgs/shelf_router_generator/test/server/service.dart b/pkgs/shelf_router_generator/test/server/service.dart
new file mode 100644
index 0000000..1803972
--- /dev/null
+++ b/pkgs/shelf_router_generator/test/server/service.dart
@@ -0,0 +1,53 @@
+// Copyright 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'dart:async' show Future, FutureOr;
+import 'package:shelf/shelf.dart';
+import 'package:shelf_router/shelf_router.dart';
+import 'api.dart';
+
+part 'service.g.dart';
+
+class Service {
+  @Route.get('/say-hello')
+  @Route.get('/say-hello/')
+  Response _sayHello(Request request) {
+    return Response.ok('hello world');
+  }
+
+  @Route.get('/wave')
+  FutureOr<Response> _wave(Request request) async {
+    await Future.delayed(Duration(milliseconds: 50));
+    return Response.ok('_o/');
+  }
+
+  @Route.get('/greet/<user>')
+  Future<Response> _greet(Request request, String user) async {
+    return Response.ok('Greetings, $user');
+  }
+
+  @Route.get('/hi/<user>')
+  Future<Response> _hi(Request request) async {
+    final name = params(request, 'user');
+    return Response.ok('hi $name');
+  }
+
+  @Route.mount('/api/')
+  Router get _api => Api().router;
+
+  @Route.all('/<_|.*>')
+  Response _index(Request request) => Response.ok('nothing-here');
+
+  Router get router => _$ServiceRouter(this);
+}
diff --git a/pkgs/shelf_router_generator/test/server/service.g.dart b/pkgs/shelf_router_generator/test/server/service.g.dart
new file mode 100644
index 0000000..b08a5f1
--- /dev/null
+++ b/pkgs/shelf_router_generator/test/server/service.g.dart
@@ -0,0 +1,19 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'service.dart';
+
+// **************************************************************************
+// ShelfRouterGenerator
+// **************************************************************************
+
+Router _$ServiceRouter(Service service) {
+  final router = Router();
+  router.add('GET', '/say-hello', service._sayHello);
+  router.add('GET', '/say-hello/', service._sayHello);
+  router.add('GET', '/wave', service._wave);
+  router.add('GET', '/greet/<user>', service._greet);
+  router.add('GET', '/hi/<user>', service._hi);
+  router.mount('/api/', service._api);
+  router.all('/<_|.*>', service._index);
+  return router;
+}
diff --git a/pkgs/shelf_router_generator/test/server_test.dart b/pkgs/shelf_router_generator/test/server_test.dart
new file mode 100644
index 0000000..85b0852
--- /dev/null
+++ b/pkgs/shelf_router_generator/test/server_test.dart
@@ -0,0 +1,56 @@
+// Copyright 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'package:test/test.dart';
+import 'package:meta/meta.dart';
+import 'package:http/http.dart' as http;
+import 'server/server.dart';
+
+void main() {
+  final server = Server();
+  setUpAll(() => server.start());
+  tearDownAll(() => server.stop());
+
+  testGet({
+    @required String path,
+    @required String result,
+  }) =>
+      test('GET $path', () async {
+        final result = await http.read(server.uri.resolve(path));
+        expect(result, equals(result));
+      });
+
+  // Test simple handlers
+  testGet(path: '/say-hello', result: 'hello world');
+  testGet(path: '/say-hello/', result: 'hello world');
+  testGet(path: '/wave', result: '_o/');
+  testGet(path: '/greet/jonasfj', result: 'Greetings, jonasfj');
+  testGet(path: '/greet/sigurdm', result: 'Greetings, sigurdm');
+  testGet(path: '/hi/jonasfj', result: 'hi jonasfj');
+  testGet(path: '/hi/sigurdm', result: 'hi sigurdm');
+
+  // Test /api/
+  testGet(path: '/api/time', result: 'it is about now');
+  testGet(path: '/api/to-uppercase/wEiRd%20Word', result: 'WEIRD WORD');
+  testGet(path: '/api/to-uppercase/wEiRd Word', result: 'WEIRD WORD');
+
+  // Test the catch all handler
+  testGet(path: '/', result: 'nothing-here');
+  testGet(path: '/wrong-path', result: 'nothing-here');
+  testGet(path: '/hi/sigurdm/ups', result: 'nothing-here');
+  testGet(path: '/api/to-uppercase/too/many/slashs', result: 'nothing-here');
+  testGet(path: '/api/', result: 'nothing-here');
+  testGet(path: '/api/time/', result: 'nothing-here'); // notice the extra slash
+  testGet(path: '/api/tim', result: 'nothing-here');
+}