Photo by Museums Victoria on Unsplash

This article is mostly a gathering of examples from Shelf and its official add-ons. Where I add my own comments that helped me understand the project and building a web server with Shelf, more about that in the end of the blog post.

Why Shelf

Shelf, is a Web Server Middleware, not a full blown server framework like Django, it is modular, and you add add-ons for the functionalities you need. This modular structure gives the community an easy way to expand the project.

While other Dart for backend solutions exist. At the time of writing, two of the most promising Dart server frameworks, Aqueduct and Angel, have stopped their development. Relying on the community to grab where they left and continuing the work done. While from Angel came Angel3 and from Aqueduct came Conduit, I prefer to use a more stable solution, stable as maintained by a team with more resources. So, never forget to support the open-source community projects!

Shelf, is maintained by the Dart team itself but still lacks functionalities, again because is not a server framework. If we want Shelf to compete with other server frameworks, such as Django (Python) it is necessary quite a bit of work. However, at the current status, we can create a web server that suits our needs.

These are the official add-ons mantained by the Dart team:

  • Shelf (core)
  • Shelf Router
  • Shelf Router Generator
  • Shelf Static
  • Shelf Websocket
  • Shelf CORS Headers

Starting a backend Project

I am assuming you have Dart installed on your machine, if not you can follow the instructions at https://dart.dev/get-dart.

To run a server you need to create a new Dart project, I’ll be using the terminal and the following command

dart create shelf_core_example

This will create a folder with all the code inside shelf_core_example if you move into the new folder created you will notice the following file structure

 CHANGELOG.md
 analysis_options.yaml
 pubspec.lock
 pubspec.yaml
 README.md
 bin/shelf_core_example.dart  # Same name as the project name created

The file shelf_core_example.dart has a simple main file.

void main(List<String> arguments) {
  print('Hello world!');
}

You can run the existing file, with a simple command:

dart run bin/shelf_core_example.dart

You will be prompted with the following message

$ dart run bin/shelf_core_example.dart
Hello World!

Adding Shelf

package: https://pub.dev/packages/shelf

Now that you have the project created, to use Shelf we simply add a Dart package dependency.

In the project root you can add it by running the following:

dart pub add shelf

Now you are ready to work on your dart server! This command will also be used to add the other Shelf add-ons we will later add.

Shelf Example

import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart' as shelf_io; // For an easier evocation of its methods

void main() async {
  var handler =
      const Pipeline().addHandler(_echoRequest);

  // Starts the server, in 'localhost' and with port 8080.
  var server = await shelf_io.serve(handler, 'localhost', 8080);

  // From the source documentation
  /**
   * Whether the [HttpServer] should compress the content, if possible.
   *
   * The content can only be compressed when the response is using
   * chunked Transfer-Encoding and the incoming request has `gzip`
   * as an accepted encoding in the Accept-Encoding header.
   *
   * The default value is `false` (compression disabled).
   * To enable, set `autoCompress` to `true`.
   */
  server.autoCompress = true;

  print('Serving at http://${server.address.host}:${server.port}');
}

// Method Handler that replies with the path called.
// The return =Response.ok= is our helper static function to return a 200 code result, but we have many other, such as, Response.found() for 302 redirects or Response.notFound() for 404 not founds and all other codes that you can expect.
Response _echoRequest(Request request) =>
    Response.ok('Request for "${request.url}"');

Adding Shelf Add-ons

The core library for Shelf, gives you the main functionalities of a server. The following example shows the barebones of a simplest server you can start running with Shelf.

Shelf Router

package: https://pub.dev/packages/shelf%5Frouter

While Shelf (core) lacks the functionality of setting Routes, we use the add-on shelf_router which will give you the router instance, with all the REST methods, such as GET and POST and all other calls that make up REST.

dart pub add shelf_router

Shelf Router Example

server.dart

import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart' as shelf_io;
import 'service.dart';

/// Similar with the previous example but here we create the routing in our new class 'Service' and we call its handler.
void main() async {
  final service = Service();

  final server = await shelf_io.serve(service.handler, 'localhost', 8080);

  print('Serving at http://${server.address.host}:${server.port}');
}

service.dart

import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.dart';
import 'api.dart';

/// Class used to set up all the routing for your server
class Service {

  // The handler/middleware that will be used by the server, all the routing for the server will be implemented here.
  Handler get handler {
    final router = Router();

    // GET
    // Replies with the text inserted in the path, example: http://localhost:8080/say-hi/Filipe will prompt "hi Filipe".
    router.get('/say-hi/<name>', (Request request, String name) {
      return Response.ok('hi $name');
    });

    // GET
    // You can use regex to limit the capture of the path arguments, in this case the userId param needs to be only digits. Note how you can also used 'async'
    // With the server running try to open http://localhost:8080/user/132 and http://localhost:8080/user/a123
    router.get('/user/<userId|[0-9]+>', (Request request) async {
      await Future.delayed(Duration(milliseconds: 100));
      return Response.ok('_o/');
    });

    // You can also embed other routers, in this case it will help organizer your routers
    // Note: This needs be before the catch 'router.all' that follows
    router.mount('/api/', Api().router);

    // A catch all of the non implemented routing, useful for showing 404 page now found
    // With the server running try to open http://localhost:8080/abc
    router.all('/<ignored|.*>', (Request request) {
      return Response.notFound('Page not found');
    });

    return router;
  }
}

api.dart

import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.dart';
import 'service.dart'

/// Decoupled endpoints for a easier organization of all endpoints
class Api {
  Future<Response> _messages(Request request) async {
    return Response.ok('[]');
  }

  Router get router {
    final router = Router();

    router.get('/messages', _messages);
    router.get('/messages/', _messages);
    router.post('/messages/', _messages);
    router.update('/messages/', _messages);
    router.delete('/messages/', _messages);

    router.all('/<ignored|.*>', (Request request) => Response.notFound('null'));
    return router;
  }
}

With the previous code setup you can now run the server in the root of the project simply by executing

dart run bin/server.dart

Shelf Router Generator

package: https://pub.dev/packages/shelf_router_generator

To reduce boilerplate, the add-on shelf_router_generator helps us with the code generation. The add-on gives us annotations that replaces the need to call the ‘router’ instance.

Shelf router generator leverages build_runner to generate code, check the bullet Dependency for more information.

dart pub add shelf_router_generator

Dependency

package: https://pub.dev/packages/build%5Frunner

To learn on how to use build_runner check check the package URL.

Add the build_runner has dev_dependencies

dart pub add builder_runner

Shelf Router Generator Example

server.dart

import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.dart';
import 'package:shelf/shelf_io.dart' as shelf_io;
import 'service.dart';

part 'server.g.dart';

void main() async {
  final service = Service();
  final server = await shelf_io.serve(service.handler, 'localhost', 8080);

  print('Serving at http://${server.address.host}:${server.port}');
}

service.dart

class Service {
  @Route.get('/say-hi/<name>')
  Response _hi(Request request, String name) => Response.ok('hi $name');

  @Route.all('/<ignored|.*>')
  Response _notFound(Request request) => Response.notFound('Page not found');

  Handler get handler => _$ServiceRouter(this);
}

You need run once the build_runner and only then the dart run.

Shelf Static

If you need server side processing, to show an html page, you will need to add this add-on so that so you can present the users to

package: https://pub.dev/packages/shelf_static

dart pub add shelf_static

Shelf Static Example

import 'package:shelf_static/shelf_static.dart';
import 'package:shelf/shelf_io.dart' as shelf_io;

void main() async {
  final handler = createStaticHandler('files', defaultDocument: 'index.html');

  final server = await shelf_io.serve(handler, 'localhost', 8080);

  print('Serving at http://${server.address.host}:${server.port}');
}

Shelf Websocket

package: https://pub.dev/packages/shelf_web_socket/

With Shelf, you don’t necessary need to work with HTTP, you also have the possibility to have a socket open through shelf_web_socket might be an add-on that not everyone uses, but it is interesting to know that it has.

dart pub add self_web_socket

Shelf WebSocket Example

import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart' as shelf_io; // For an easier evocation of its methods
import 'package:shelf_web_socket/shelf_web_socket.dart';

void main() {
  var handler = webSocketHandler((webSocket) {
    webSocket.stream.listen((message) {
      webSocket.sink.add('Echo: $message');
    });
  });

  shelf_io.serve(handler, 'localhost', 8080).then((server) {
    print('Serving at ws://${server.address.host}:${server.port}');
  });
}

To test this server with Web Socket I used the “Simple WebSocket Client” plugin for Chromium https://chrome.google.com/webstore/detail/simple-websocket-client/pfdhoblngboilpfeibdedpjgfnlcodoo?hl=en

Shelf CORS

package: https://pub.dev/packages/shelf_cors_headers

Last but not least, we have this add-on to help us change the headers to have CORS working locally. Exposes a method called corsHeaders() that returns a Middleware used in the Shelf Pipeline.

Do not confuse this add-on with shelf_cors that is not available for Dart 2.0

dart pub add shelf_cors_headers

Shelf CORS Headers Example

import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart' as shelf_io;
import 'package:shelf_cors_headers/shelf_cors_headers.dart';

void main() async {
  final overrideHeaders = {
    ACCESS_CONTROL_ALLOW_HEADERS: 'example.com',
    'Content-Type': 'application/json;charset=utf-8',
  };

  final handler = const Pipeline()
    .addMiddleware(
      corsHeaders( // Middleware from 'shelf_cors_headers'
          headers: overrideHeaders,
      ),
    )
    .addHandler(_echoRequest);

  final server = await shelf_io.serve(handler, 'localhost', 8080);

  print('Serving at http://${server.address.host}:${server.port}');
}

Response _echoRequest(Request request) {
  return Response.ok('Request for "${request.url}"');
}

Conclusion

While we wait for other Dart server frameworks to mature, we can always rely on Shelf to implement our project. There is still a lot needed to implement and few add-ons available that extends Shelf (https://pub.dev/packages?q=shelf)

Next article on the topic of Dart Backend will be about integrating Stripe into a server to pay recurrent or by metered API consumption. If I see myself using other Shelf add-on I’ll write a new blog post