DEV Community

Cover image for Dart on Server
Andrey Unger
Andrey Unger

Posted on

Dart on Server

Dart is practically not inferior to the NodeJS on the server in terms of performance, but its ecosystem is rather small by comparison. But still, sometimes you come across something interesting.

Today we will talk about a new HTTP server on dart - Dia. This is a small simple web server inspired by KoaJS. The main idea of the project: the context of the http request, which passes the queue of the middleware.

To install it, add this to your pubspec.yaml:

dependencies:
  dia: ^0.0.7
Enter fullscreen mode Exit fullscreen mode

It is generally very simple to use:

import 'package:dia/dia.dart';

main() {
  /// Create instance of Dia
  final app = App();

  /// Add middleware
  app.use((ctx, next) async {
    /// write response on all query
    ctx.body = 'Hello world!';
  });

  /// Listen localhost:8080
  app
      .listen('localhost', 8080)
      .then((info) => print('Server started on http://localhost:8080'));
}
Enter fullscreen mode Exit fullscreen mode

The main brick of the application is middleware. This is an asynchronous function that takes a Context and a reference to the next middleware as arguments.

The Сontext contains getters and setters for the dart.io.HttpRequest and dart.io.HttpResponse fields. If you are familiar with koa, then you will understand the basic idea of the context. Most importantly, you can use your own extended context with additional fields, such as the current user.

class MyContext extends Context{
  User? user;

  MyContext(HttpRequest request): super(request);
}


main() {
  /// Create instance of Dia
  final app = App<MyContext>();

  app.use((ctx, next) async {
    ctx.user = new User('test');
    await next();
  });

  app.use((ctx, next) async {
    if(ctx.user==null){
      ctx.trowError(401);
    }
  });

  /// Add middleware
  app.use((ctx, next) async {
    /// write response on all query
    ctx.body = 'Hello world!';
  });

  /// Listen localhost:8080
  app
      .listen('localhost', 8080)
      .then((info) => print('Server started on http://localhost:8080'));
}
Enter fullscreen mode Exit fullscreen mode

The next - a link to the next middleware in the queue. When a middleware does not return the final result, but only changes the context, it often needs to wait for the next middleware to complete. This is necessary, for example, to add logging or error handling.

app.use((ctx, next) async {
  final start = DateTime.now();
  await next();
  final diff = DateTime.now().difference(start).inMicroseconds;
  print('microseconds=$diff')
});
Enter fullscreen mode Exit fullscreen mode

There are ready-made middleware in the form of separate packages. This allows, without increasing the code base, to use only those of them that are necessary in the project. For example:

  • dia_cors - middleware for add CORS headers
  • dia_static - middleware for serving static files
  • dia_router - middleware like as koa_router, for work with url path
  • dia_body - middleware for parse request body.

Let us dwell on the last two of them in more detail.

The first one is dia_router - allows you to set middleware for specific URLs and specific http methods.

It requires a special Context with the Routing mixin. They contains additional parameters that can be passed to url

class ContextWithRouting extends Context with Routing {
  ContextWithRouting(HttpRequest request) : super(request);
}

void main() {
  /// create Dia app with Routing mixin on Context
  final app = App<ContextWithRouting>();

  /// create router
  final router = Router<ContextWithRouting>('/route');

  /// add handler to GET request
  router.get('/data/:id', (ctx, next) async {
    ctx.body = '${ctx.params}';
  });

  /// start server
  app
      .listen('localhost', 8080)
      .then((_) => print('Started on http://localhost:8080'));
}
Enter fullscreen mode Exit fullscreen mode

If we open the http://localhost:8080/data/12 in the browser, we will see {id: 12}

The second package that we will consider is dia_body - it allows you to receive the parameters passed with the http request. It also needs an extended context with the ParsedBody mixin.

class ContextWithBody extends Context with ParsedBody {
  ContextWithBody(HttpRequest request) : super(request);
}

void main() {
  final app = App<ContextWithBody>();

  app.use(body());

  app.use((ctx, next) async {
    ctx.body = ''' 
    query=${ctx.query}
    parsed=${ctx.parsed}
    files=${ctx.files}
    ''';
  });

  /// Start server listen on localhost:8080
  app
      .listen('localhost', 8080)
      .then((info) => print('Server started on http://localhost:8080'));
}
Enter fullscreen mode Exit fullscreen mode
  • ctx.query - contain url get parameters. ?param=value - {param: value}
  • ctx.parsed - contains json body or x-www-form-urlencoded or form-data params of http request
  • ctx.files - contains uploaded files. By default, files are loaded into the temporary system directory, but the path can be changed.

We can use the Context with all extensions and our fields at the same time:

class CustomContext extends Context with Routing, ParsedBody {
  User? user;
  ContextWithBody(HttpRequest request) : super(request);
}
Enter fullscreen mode Exit fullscreen mode

We can run the http server version as well:

  final serverContext = SecurityContext();
  serverContext
      .useCertificateChainBytes(await File(certificateChain).readAsBytes());
  serverContext.usePrivateKey(serverKey, password: 'password');

  final server = await app.listen(
      'localhost', 8444,
      securityContext: serverContext);
Enter fullscreen mode Exit fullscreen mode

All sources of the given packages are on GitHub and published under the MIT license. I would be glad to receive any feedback, issue and pull requests!

Top comments (0)