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}"');
-
Permalink to
autoCompress
source code: autoCompress -
Permalink to implementation of other Response static methods
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