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? :-)
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);
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();
}
});
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.
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.
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;
}
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}) {
...
}
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();
}
}
}
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 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.
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:
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.
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.
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);
}
}
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);
}
}
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;
}
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
Well since I made fukiya with the same goal, you are more than welcome to contribute or take anything from it if it helps you. Just look up Fukiya here on github. Also if you want a form parser, you are welcome to use my Formler library. I use it as middleware in my setup.