Marathon

A framework and collection of packages for writing HTTP servers, built on top of the shelf package.

Base

The "base" or skeleton of this library should expose an API by which both first-party and third-party can hook into the server via middleware or handlers. An example of important middleware might be session management or authenticators; given the dynamic nature of these tasks, we can't possibly encapsulate every possibility in this library.

This API should integrate cleanly with Shelf. Shelf will handle the "dirty work" of actually receiving and responding to HTTP requests; rewriting Shelf's functionality in this library is transparently useless since Google officially maintains that library and it's relatively stable.

The base should also feature routing. Since the majority of people writing an HTTP server need some form of routing, it's reasonable to bundle it in the main library. Routing should use shelf_router underneath, for much the same reasons listed above for the main shelf package. However, we do have to overlay shelf_router with our own syntax to allow us a space to define hooks.

An example syntax, using only the base package, might look like this:

// in main.dart
import "package:marathon/marathon.dart";
import "service.dart";

void main (List<String> args) async {
  // The Router class is defined in shelf_router.
  var router = Router(); 
  
  // Extension method from Marathon, that adds routes from EchoService to shelf_router.
  router.register(EchoService()); 
  
  await serve(router.handler, 'localhost', 8080); // defined in shelf_io
}
// in service.dart
import "package:marathon/marathon.dart";

// RouteGroup is an abstract class defined in marathon.
//
// It should have a abstract property:
//   List<Handler> get routes
// Which tells the register method which routes to add.
class EchoService extends RouteGroup {

  // This handler will handle requests starting with /echo.
  @override
  String get basePath => '/echo'; 
  
  // Handler is a convenience typedef for FutureOr<Response> Function(). This 
  // is exported from shelf.
  //
  // Marathon.get is our "hook" here. We can override/replace it downstream to
  // inject resources, such as session id's, authenticated data, ORMs, whatever.
  RouteHandler getName = RouteHandler('/<name>').get.handle((Request r, String name) {
    return Response.ok('echo $name'); 
  }
  
  // Marathon should expose a different getter for each HTTP method.
  RouteHandler postName = RouteHandler('/<name>').post.handle((Request r, String name) {
    return Response.ok('echo $name'); 
  }
  
  // Marathon should also be able to "chain" getters to lump
  // HTTP methods together. In this example, we handle both
  // get and post requests in one handler.
  RouteHandler doubleName = RouteHandler('/double/<name>').get.post.handle((Request r, String name) {
    return Response.ok('echo $name$name');
  }
  
  // List of handlers that will be registered from this class.
  @override
  List<RouteHandler> routes = [getName, postName, doubleName];
}

In this example, we handle requests made to localhost on port 8080. If the request is a get or post request to "/echo/<somestringhere>", we return an OK Response containing the message "echo <somestringhere>".

Now, suppose we wanted to add some Middleware that mutated the double request and passed a doubled name in:

// in service.dart
import "package:marathon/marathon.dart";

class EchoService extends RouteGroup {
  // leaving out old stuff...
  
  // We can add middleware really easily to our routes.
  Handler doubleName = Route('/double/<name>').get.post
    .middleware(requestHandler: (Request r, String name) {
      r.context['doubled'] = name + name;
      return null; // passes onto the handler
    })
    .handle((Request r, String name) {
      return Response.ok('echo ${r.context['doubled']}');
    });
  
  // List of handlers that will be registered from this class.
  @override
  List<Handler> routes = [getName, postName, doubleName];
}

This is fine for individual Handlers, but the syntax is repetitive and verbose when used across every handler in a RouteGroup. If every handler in a RouteGroup uses the same logic, we can do something like:

// in service.dart
import "package:marathon/marathon.dart";

// Note that RouteGroup also needs an abstract property for
// wrapEach, for much the same reasons as routes.
class EchoService extends RouteGroup {
  // leaving out old stuff...
      
  // List of handlers that will be registered from this class.
  @override
  List<Handler> routes = [getName, postName, doubleName];
  
  // Logging middleware (logRequests defined in shelf).
  Middleware logger = logRequests();
  
  // Creates a middleware hierarchy:
  // firstElementFromWrapEach (
  //   secondElementFromWrapEach (
  //     ...toLastElement (
  //          routeHandler()
  //     )
  //   )
  // )
  @override
  List<Middleware> wrapEach = [logger];
}

This can wrap every route in a group with Middleware. This is a powerful tool, but also somewhat easy to abuse. Regardless, the provided functionality is immensely useful for plugging into Marathon, since the vast majority of plugins will be Middleware in some way.

Last updated