Skip to content

Instantly share code, notes, and snippets.

@adam-singer
Forked from sma/dexpess.md
Created May 10, 2013 23:42
Show Gist options
  • Save adam-singer/5558262 to your computer and use it in GitHub Desktop.
Save adam-singer/5558262 to your computer and use it in GitHub Desktop.

Dexpress

I like Express for Node.js. I'd like to have a similar framework for Dart.

Let's create it. How hard can it be? :-)

Middleware is Everywhere

Express really builds on the idea of middleware, a function that takes a request object, a response object and an optional continuation function. If called, the request processing continues. The request object describes the HTTP request the middleware shall process and the response object is used to repare the HTTP response. There's also an error-handling middleware variant that needs a fourth argument (in front of all other) but let's ignore that right now.

Here is the canonical "Hello, World!" example:

var app = express();
app.get('/', function (req, res) { res.send("Hello, World"); });
app.listen(3000);

The anonymous function passed to get is a middleware that will compute the result. get creates a route, which is a middleware that only calls its middleware if the request's method and path match. All routes are registered with a router, another middleware that will iterate over all routes if called. The router is encapsulated by an application which can be also used as middleware to compose complex application from simple ones.

Obviously, I need a Dart type to represent a middleware. I will call it Middleware. Because I don't think I can create a function type that has a third optional parameter (everything I tried either makes the Dart Analyser really unhappy or throws a runtime exception), I will force the user to always declare three parameters.

typedef void Next();
typedef void Middleware(Request req, Response res, Next next);

Alternative Idea

I was playing around with the idea to create some kind of triple object so that I have just one parameter, but I'm not sure that this is really an advantage...

app.get('/', (m) {
  if (m.req.param("foo") == "bar") {
    m.res.send("...");
  } else {
    m.next();
  }
});

The Request

This object should encapsulate a Dart HttpRequest and provide an Express-compatible API. It probably needs to know its Application (see below).

class Request {
  final HttpRequest _request;
  final Application app;

  Request(this._request, this.app);

  String get method => _request.method;
  String get path => _request.uri.path;

  String header(String name) => _request.headers.value(name);

  ...
}

I will add more methods later.

The Response

Likewise, this object should encapsulate a Dart HttpResponse. I think, it will need to know the Request. Otherwise, send and redirect cannot be implemented the way Express does it. And via the request, it knows the application. We'll see how this plays out.

class Response {
  final HttpResponse _response;
  final Request req;

  Response(req) : _response = req._request.response, req = req;

  Application get app => req.app;

  ...
}

The constructor assignment looks funny, but that's how I need to initialize the final variables, I guess. It feels a little bit dirty to access the _request property here.

Sending a Response

The send method is a good example for the tendency of Express, to make the first parameter optional which is something I cannot express in Dart (even if DartEditor accepts that code which is everything but helpful because for a short moment I thought I could actually have an optional first parameter). So I have to use dynamic types here and inspect the arguments:

  // send(status) or send(body) or send(status, body)
  void send(status, [body]) {
    if (body == null && status is! int) { 
      body = status;
      status = HttpStatus.OK;
    }

    this.status(status);
    
    if (body is List<int>) { // Dart's "Buffer" type
      data(body);
    } else if (body is String) {
      html(body);
    } else if (body is Object) {
      json(body);
    } else {
      end();
    }
  }

If a List<int> is passed, I forward this to Dart's add method, unchanged. Otherwise send will either generate an HTML response a a JSON response. Here are those methods:

  void data(List<int> data) {
    if (_noContentType) {
      contentType("application/octet-stream");
    }
    _response.add(data);
    end();
  }

  void html(object) {
    if (_noContentType) {
      contentType("text/html;charset=utf-8");
    }
    _response.write(object);
    end();
  }

  void json(object) {
    if (_noContentType) {
      contentType("application/json;charset=utf-8");
    }
    _response.write(JSON.stringify(object));
    end();
  }

My end method works slightly different. Express follows Node.js in that there is a writeHead method which starts a response and end which ends it. I could simply close the HttpResponse. However, I intent to overwrite end for the purpose of response wrapping, as seen later.

  void end() {
    _response.close();
  }

And then, there are some helpers:

  bool get _noContentType => _response.headers.contentType == null;

  void contentType(String contentType) {
    _response.headers.contentType = ContentType.parse(contentType);
  }

  void status(int status) {
    _response.statusCode = status;
  }

Redirecting

The redirect method also supports an optional first paramter. In addition to URLs, it supports "back" to go back to the referrer - if there is one:

  // redirect(url) or redirect(status, url)
  void redirect(status, [url]) {
      if (url == null && status is! int) {
          url = status;
          status = HttpStatus.FOUND;
      }
      if (url == "back") {
          url = req.header("Referrer");
          if (url == null) {
              url = "/";
          }
      }
      header("Location", url);
      send(status, req.method == "HEAD" ? null : "Redirecting to $url");
  }

  void header(String name, Object value) {
    _response.headers.set(name, value);
  }

The Dart way would probably be to use named parameters like this, but to allow the numeric status before the URL and stay close to Express, I need to make both parameters optional.

// either...
void redirect({int status:302, String url}) {
  ...
}
// or...
void redirect(String url, {int status:302}) {
  ...
}

The Route

Here's the object which will apply a middleware only if the request has a certain method and a certain path. Express will match regular expressions, but that's easy to add later. Right now, I'm only interested in the principle of dispatching middleware:

class Route {
    final String method;
    final String path;
    final Middleware mw;

    Route(this.method, this.path, this.mw);

    // I'm a Middleware, too
    void call(Request req, Response res, Next next) {
        if (req.method == method && req.path == path) {
            mw(req, res, next);
        } else {
            next();
        }
    }
}

The Router

This name might be a misnomer. My router is actually just a list of middleware that is applied one after the other. Perhaps it's a RouteList but that isn't much better, so I'll stick with Router which is the name Express uses, I think:

class Router {
    final List<Middleware> _mws;

    Router(this._mws);

    void add(Middleware mw) { _mws.add(mw); }

    // I'm a Middleware, too
    void call(Request req, Response res, Next next) {
        void step(int i, Next next) {
            if (i < _mws.length) {
                _mws[i](req, res, () => step(i + 1, next));
            } else {
                next();
            }
        }
        step(0, next);
    }
}

Come one, admit it, you love CPS, don't you? The call method will step through and call each middleware function as long as they call next. The chain is broken if a middleware decides to not call its continuation function and hopefully has prepared the final response.

The Application

The last piece in this puzzle is the Application object which can register URL handler middleware, combine other middleware (for example other applications) and last but not least can start a server. Here it is:

class Application {
    final Router router = new Router([]);

    // I'm a Middleware, too
    void call(Request req, Response res, Next next) { router(req, res, next); }

    void use(Middleware mw) { router.add(mw); }

    void get(String path, mw) { _route("GET", path, mw); }
    void post(String path, mw) { _route("POST", path, mw); }
    void all(String path, mw) { METHODS.forEach((m) => _route(m, path, mw)); }
    void _route(String method, String path, mw) {
        if (mw is List) { mw = new Router(mw as List); }
        if (mw is! Middleware) { throw "$mw must be a middleware"; }
        router.add(new Route(method, path, mw));
    }
    static final METHODS = ["GET", "POST"];

    void listen(int port) {
        HttpServer.bind('0.0.0.0', port).then((HttpServer server) => server.listen(_dispatch));
    }
    void _dispatch(HttpRequest request) {
        Request req = new Request(request, this);
        Response res = new Response(req);
        router(req, res, () => res.send(404, "Cannot ${req.method} ${req.path}"));
    }   
}

Application dexpress() { return new Application(); }

As theorized before, an application encapsulates a router, that is a list of middleware functions. I can use use to add another middleware. Order is important here. I can register URL handler with get, post or all (more methods could be added easily). I don't statically type the middleware argument, because I want to support not only a single middleware but a list of middleware functions. Unfortunately, Dart don't support variable argument lists, so I need to use an explicit list here. Ugly, but we have to live with that. The _dispatch method wraps Dart's HttpRequest (which contains a HttpResponse) as Request and Response pair and starts the processing. If no middleware accepts the request and the final continuation function is reached, a 404 is sent.

The First Test

This is basically the same code as in the first example, but with a third parameter _:

var app = dexpress();
app.get('/', (req, res, _) { res.send("Hello, World"); });
app.listen(3000);

Most Express methods will return this to support chaining. Thanks to Dart's .. I can rewrite the above code without any special return values like this:

dexpress()
    ..get('/', (req, res, _) { res.send("Hello, World"); })
    ..listen(3000);

Looking nice, isn't it?

Let's have a look at some simple examples:

A logging middleware

void logger(req, res, next) {
    print("${req.method} ${req.path}"); next();
}

dexpress()
    ..use(logger)
    ..get('/', (req, res, _) { res.send("Hello, World"); })
    ..listen(3000);

If I'm also interested in the result, things get more complicate. I need to wrap the Response and overwrite end so that I get a chance to do something after the request processing as ended.

Basic authentication

Middleware authenticate(user, pass) {
  String base64decode(String data) {
    return new String.fromCharCodes(CryptoUtils.base64StringToBytes(data));
  }

  void unauthorized(res) {
    res.header('WWW-Authenticate', 'Basic real="Authorization Required"');
   res.send(401, 'Unauthorized');
  }

  return (req, res, next) {
    String authorization = req.header('Authorization');
    if (authorization == null) {
      unauthorized(res);
      return;
    }
    List<String> parts = authorization.split(' ');
    if (parts.length != 2 || parts[0] != 'Basic') {
      res.send(400, 'Bad request');
      return;
    }
    List<String> credentials = base64decode(parts[1]).split(':');
    if (credentials.length != 2) {
      res.send(400, 'Bad request');
      return;
    }
    if (user != credentials[0] || pass != credentials[1]) {
      unauthorized(res);
      return;
    }
    next();
  };
}

You need to import dart:crypto for the base64 decoder.

Templates

Express can register a a template engine with an application and then render content using that engine. Here's an example that renders text through markdown (Dart team, please fix your Markdown renderer) and replaces stuff in {{ ... }} with values provided as template data:

app.engine('md', (String path, Map data, void callback(String content)) {
    new File(path).readAsString().then((content) {
        var html = markdown(content);
        html = html.replaceAllMapped(new RegExp(r'\{\{(.*?)\}\}'), (Match m) {
            return data[m.group(1)];
        });
        callback(html);
    }, onError: (_) => callback(null));
});
app.get('/foo', (req, res, _) => res.render('foo.md', {'name': req.path}));
app.get('/bar', (req, res, _) => res.render('bar.md', {'name': req.path}));

Note: This could be more generic if my Route would use regular expressions.

Here's a simple implementation of engine and render:

class Application {
    Map<String, Function> _engines = {};
    ...

    void engine(String extension, void engine(String path, Map data, fn)) {
        _engines[extension] = engine;
    }

    void render(String name, Map data, void callback(String content)) {
        String extension = name.substring(name.lastIndexOf('.') + 1);
        Function engine = _engines[extension];
        if (engine != null) {
            engine("views/$name", data, callback);
        } else {
            callback(null);
        }
    }
}

I also need to add render to the Response class:

class Response {
    void render(String name, Map data, [void callback(String content)]) {
        if (callback == null) {
            callback = (String content) { send(content); };
        }
        app.render(name, data, callback);
    }
}

Once Again With Futures

Using callbacks isn't really the Dart way, but it should do the trick. I haven't really thought about error handling, though. I would expect futures to make things easier. An engine is then a function taking a path and data and returning a future with the processed content as a string:

typedef Future<String> Engine(String path, Map data);

Here is the alternative implementation:

class Application {
    Map<String, Engine> _engines;
    ...
    
    void engine(String extension, Engine engine) {
        _engines[extension] = engine;
    }

    // actually, I'm an Engine, too
    Future<String> render(String name, Map data) {
        String extension = name.substring(name.lastIndexOf('.') + 1);
        Engine engine = _engines[extension];
        if (engine != null) {
            return engine("views/$name", data);
        }
        return new Future.complete(null);
    }
}

And of course, I need to provide a different engine implementation:

app.engine("md", (String path, Map data) {
    Completer c = new Completer();
    new File(path).readAsString().then((content) {
        var html = markdown(content);
        html = html.replaceAllMapped(new RegExp(r'\{\{(.*?)\}\}'), (Match m) {
            return data[m.group(1)];
        });
        c.complete(html);
    });
    return c.future;
});

Last but not least, rendering need to use the Future:

class Response {
    ...
    void render(String name, Map data, [void callback(String content)]) {
        if (callback == null) {
            callback = (String content) { send(content); }
        }
        app.render("views/$name", data).then(callback);
    }
}

Composition

When use-ing middleware, Express allows an optional route argument. This way, we can combine applications like this:

var app = dexpress().get("/bar", ...);
dexpess()
  ..use("/foo", app)
  ..listen(3000);

The ...-middleware will then be called by accessing the URL /foo/bar.

Here's the modified implementation:

class Application {
  final Router router = new Router([]);
  String route = "/";
  
  void use(route, [mw]) {
    if (mw == null) {
      mw = route;
      route = "/";
    }
    if (mw is! Middleware) {
      throw "$mw must be a middleware";
    }
    if (route is! String) {
      throw "$route must be a string";
    }
    if (mw is! Application) {
      mw = new Application()..router.add(mw);
    }
    (mw as Application).route = route;
    router.add(mw);
  }

  void call(Request req, Response res, Next next) {
    String path = req.path;
    if (path.startsWith(route)) {
      if (route != "/") {
        req.path = path.substring(route.length);
      }
      router(req, res, next);
      req.path = path;
    } else {
      next();
    }
  }

  ...
}

And Request needs a modifiable path:

class Request {
  final HttpRequest _request;
  final Application app;
  String path;

  Request(this._request, this.app) {
    path = fullPath;
  }

  String get method => _request.method;
  String get fullPath => _request.uri.path;
}

What's left

Express, building upon Connect, comes with a ton of middleware and a lot of additional goodies and all that would be needed for a complete Express port to Dart.

But I'll leave that for another day.

Stefan

// Copyright 2013 Stefan Matthias Aust. Licensed under MIT (http://opensource.org/licenses/MIT)
import 'dart:io';
import 'dart:json' as JSON;
/**
* A continuation function.
*/
typedef void Next();
/**
* A middleware function taking a [req]uest and generating a [res]ponse and/or calling [next].
*/
typedef void Middleware(Request req, Response res, Next next);
/**
* A middleware function taking a [req]uest and generating a [res]ponse.
* Like [Middleware], but without a [Next] continuation function.
*/
typedef void MiddlewareWithoutNext(Request req, Response res);
Middleware _assertMiddleware(mw) {
if (mw is MiddlewareWithoutNext) {
return (Request req, Response res, Next next) => mw(req, res);
}
if (mw is Middleware) {
return mw;
}
throw "$mw must be a middleware";
}
/**
* Encapsulates a Dart [HttpRequest].
*/
class Request {
final HttpRequest _request;
final Application app;
String path;
Request(this._request, this.app) {
path = fullPath;
}
String get method => _request.method;
String get fullPath => _request.uri.path;
String header(String name) => _request.headers.value(name);
}
/**
* Encapsulates a Dart [HttpResponse].
*/
class Response {
final HttpResponse _response;
final Request req;
Response(req) : req = req, _response = req._request.response;
Application get app => req.app;
/**
* Sends a generic response.
*
* Variants:
* send(status)
* send(status, body)
* send(body)
*
* [status] is an integer (see [HttpStatus]).
* If [body] is a String, the content type defaults to "text/html".
* If [body] is a List<int>, the content type defaults to "application/octet-stream".
* If [body] is any other object (most likely a Map or boolean) it will be JSONified
* and the content type defaults to "application/json".
*/
void send(status, [body]) {
if (body == null && status is! int) {
body = status;
status = HttpStatus.OK;
}
this.status(status);
if (body is List<int>) { // Dart's "Buffer" type
data(body);
} else if (body is String) {
html(body);
} else if (body is Object) {
json(body);
} else {
_response.write(_response.reasonPhrase);
end();
}
}
/**
* Sends a binary response.
*/
void data(List<int> data) {
if (noContentType) {
contentType("application/octet-stream");
}
_response.add(data);
end();
}
/**
* Sends an HTML response.
*/
void html(object) {
if (noContentType) {
contentType("text/html;charset=utf-8");
}
_response.write(object);
end();
}
/**
* Sends a JSON response.
*/
void json(object) {
if (noContentType) {
contentType("application/json;charset=utf-8");
}
_response.write(JSON.stringify(object));
end();
}
/**
* Finishes the processing.
*/
void end() {
_response.close();
}
/**
* Returns whether a content type was already set or not.
*/
bool get noContentType => _response.headers.contentType == null;
/**
* Sets the content type (and optionally the receiver's encoding).
*/
void contentType(String contentType) {
_response.headers.contentType = ContentType.parse(contentType);
}
/**
* Sets the status code.
*/
void status(int status) {
_response.statusCode = status;
}
/**
* Sends a redirect (302 FOUND) response.
*
* Variants:
* redirect(url)
* redirect(status, url)
*
* [status] is an integer.
* [url] is a String.
*/
void redirect(status, [url]) {
if (url == null && status is! int) {
url = status;
status = HttpStatus.FOUND;
}
if (url == "back") {
url = req.header("Referrer");
if (url == null) {
url = "/";
}
}
header("Location", url);
send(status, req.method == "HEAD" ? null : "Redirecting to $url");
}
/**
* Sets a header value.
*/
void header(String name, Object value) {
_response.headers.set(name, value);
}
void render(String name, Map data, [void callback(String content)]) {
if (callback == null) {
callback = (String content) { send(content); };
}
app.render(name, data, callback);
}
}
/**
* A middleware that only calls its middleware if the request matches the given method and path.
*/
class Route {
final String method;
final String path;
final Middleware mw;
Route(this.method, this.path, this.mw);
/**
* Applies the receiver as a [Middleware].
*/
void call(Request req, Response res, Next next) {
if (req.method == method && req.path == path) {
mw(req, res, next);
} else {
next();
}
}
}
/**
* A middleware that applies a list of middleware functions as long as they call their continuation function.
*/
class Router {
final List<Middleware> _mws;
Router(this._mws);
void add(Middleware mw) { _mws.add(mw); }
/**
* Applies the receiver as a [Middleware].
*/
void call(Request req, Response res, Next next) {
void pass(int i, Next next) {
if (i < _mws.length) {
_mws[i](req, res, () => pass(i + 1, next));
} else {
next();
}
}
pass(0, next);
}
}
/**
* An application middleware.
*
* Example:
* var app = new Application();
* app.get("/", (req, res, _) { res.send("Hello, World!"); });
* app.listen(3000);
*/
class Application {
final Map<String, Function> _engines = {};
final Router router = new Router([]);
String route = "/";
/**
* Uses the given middleware.
*
* Variants:
* use(middleware)
* use(route, middleware)
*
* [route] must be a String and defaults to `/`.
* [middleware] must be a middleware or an exception is thrown.
*/
void use(route, [middleware]) {
if (middleware == null) {
middleware = route;
route = "/";
}
if (middleware is! Middleware) {
throw "$middleware must be a middleware";
}
if (route is! String) {
throw "$route must be a string";
}
if (middleware is! Application) {
middleware = new Application()..router.add(middleware);
}
(middleware as Application).route = route;
router.add(middleware);
}
/**
* Applies the receiver as a [Middleware].
*/
void call(Request req, Response res, Next next) {
String path = req.path;
if (path.startsWith(route)) {
if (route != "/") {
req.path = path.substring(route.length);
if (req.path == "") {
req.path = "/";
}
}
router(req, res, next);
req.path = path;
} else {
next();
}
}
void get(String path, mw) { _route("GET", path, mw); }
void put(String path, mw) { _route("PUT", path, mw); }
void post(String path, mw) { _route("POST", path, mw); }
void delete(String path, mw) { _route("DELETE", path, mw); }
void all(String path, mw) { _METHODS.forEach((method) => _route(method, path, mw)); }
void _route(String method, String path, mw) {
if (mw is List<Middleware>) {
mw = new Router(mw.map(_assertMiddleware));
}
router.add(new Route(method, path, _assertMiddleware(mw)));
}
static const _METHODS = const ["GET", "POST", "PUT", "DELETE"];
/**
* Starts up an HTTP server which listens on the given [port] which defaults to 3000.
*/
void listen([int port=3000]) {
HttpServer.bind('0.0.0.0', port).then((HttpServer server) => server.listen(_dispatch));
}
void _dispatch(HttpRequest request) {
Request req = new Request(request, this);
Response res = new Response(req);
router(req, res, () => res.send(HttpStatus.NOT_FOUND, "Cannot ${req.method} ${req.path}"));
}
void engine(String extension, void engine(String path, Map data, fn)) {
_engines[extension] = engine;
}
void render(String name, Map data, void callback(String content)) {
String extension = name.substring(name.lastIndexOf('.') + 1);
Function engine = _engines[extension];
if (engine != null) {
engine("views/$name", data, callback);
} else {
callback(null);
}
}
}
/**
* Returns a new application middleware object.
*/
Application dexpress() {
return new Application();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment